From 687f59a9fa337a78139068f99fe0e768e14e595c Mon Sep 17 00:00:00 2001 From: ferdiansyah783 Date: Thu, 7 Aug 2025 23:48:31 +0700 Subject: [PATCH] feat: analytics --- .../(private)/dashboards/crm/page.tsx | 426 ++++++++++++++---- .../dashboards/ecommerce/order/page.tsx | 348 ++++++++++++++ .../dashboards/ecommerce/product/page.tsx | 397 ++++++++++++++++ .../finance/payment-method/page.tsx | 355 +++++++++++++++ src/components/date-picker/PickerBasic.tsx | 60 +++ .../layout/vertical/VerticalMenu.tsx | 22 +- src/services/queries/analytics.ts | 69 +++ src/types/services/analytic.ts | 48 ++ src/utils/transform.ts | 7 + src/views/Login.tsx | 4 +- 10 files changed, 1651 insertions(+), 85 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/dashboards/ecommerce/order/page.tsx create mode 100644 src/app/[lang]/(dashboard)/(private)/dashboards/ecommerce/product/page.tsx create mode 100644 src/app/[lang]/(dashboard)/(private)/dashboards/finance/payment-method/page.tsx create mode 100644 src/components/date-picker/PickerBasic.tsx create mode 100644 src/services/queries/analytics.ts create mode 100644 src/types/services/analytic.ts diff --git a/src/app/[lang]/(dashboard)/(private)/dashboards/crm/page.tsx b/src/app/[lang]/(dashboard)/(private)/dashboards/crm/page.tsx index 4b7327e..2d43907 100644 --- a/src/app/[lang]/(dashboard)/(private)/dashboards/crm/page.tsx +++ b/src/app/[lang]/(dashboard)/(private)/dashboards/crm/page.tsx @@ -1,87 +1,355 @@ -// MUI Imports -import Grid from '@mui/material/Grid2' +'use client' -// Component Imports -import DistributedBarChartOrder from '@views/dashboards/crm/DistributedBarChartOrder' -import LineAreaYearlySalesChart from '@views/dashboards/crm/LineAreaYearlySalesChart' -import CardStatVertical from '@/components/card-statistics/Vertical' -import BarChartRevenueGrowth from '@views/dashboards/crm/BarChartRevenueGrowth' -import EarningReportsWithTabs from '@views/dashboards/crm/EarningReportsWithTabs' -import RadarSalesChart from '@views/dashboards/crm/RadarSalesChart' -import SalesByCountries from '@views/dashboards/crm/SalesByCountries' -import ProjectStatus from '@views/dashboards/crm/ProjectStatus' -import ActiveProjects from '@views/dashboards/crm/ActiveProjects' -import LastTransaction from '@views/dashboards/crm/LastTransaction' -import ActivityTimeline from '@views/dashboards/crm/ActivityTimeline' +import React, { useEffect, useState } from 'react' +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + BarChart, + Bar, + PieChart, + Pie, + Cell +} from 'recharts' +import { useSalesAnalytics } from '../../../../../../services/queries/analytics' +import Loading from '../../../../../../components/layout/shared/Loading' +import PickerBasic from '../../../../../../components/date-picker/PickerBasic' +import { formatDateDDMMYYYY } from '../../../../../../utils/transform' -// Server Action Imports -import { getServerMode } from '@core/utils/serverHelpers' +// Tabler icons component +const TablerIcon = ({ name = '', className = '' }) => -const DashboardCRM = async () => { - // Vars - const serverMode = await getServerMode() +const DashboardCRM = () => { + const today = new Date() + const monthAgo = new Date() + monthAgo.setDate(today.getDate() - 30) + + const [dateFrom, setDateFrom] = useState(monthAgo) + const [dateTo, setDateTo] = useState(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 return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ {/* Header */} +
+
+

Sales Analytics Dashboard

+ +
+
+
+ + Grouped by {analytics?.group_by} +
+
+ + {analytics?.data.length} data points +
+
+
+ + {/* Metrics Cards */} +
+ {metrics.map((metric, index) => ( +
+
+
+
+

{metric.title}

+

{metric.value}

+
+ + {metric.change} +
+
+
+ +
+
+
+
+ ))} +
+ + {/* Charts Section */} +
+ {/* Sales Trend Chart */} +
+
+
+ +

Sales Trend

+
+
+ + + + + `${(value / 1000).toFixed(0)}K`} stroke='#6b7280' /> + [ + 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)' + }} + /> + + + +
+
+
+ + {/* Sales Distribution */} +
+
+
+ +

Sales Distribution

+
+
+ + + `${name} ${(percent * 100).toFixed(0)}%`} + > + {performanceData?.map((entry: any, index: any) => ( + + ))} + + [formatCurrency(value), 'Sales']} + contentStyle={{ + backgroundColor: '#fff', + border: '1px solid #e5e7eb', + borderRadius: '8px', + boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' + }} + /> + + +
+
+
+
+ + {/* Orders & Items Chart */} +
+
+
+ +

Orders & Items Analysis

+
+
+ + + + + + + + + + +
+
+
+ + {/* Detailed Data Table */} +
+
+
+ +

Daily Performance Details

+
+
+ + + + + + + + + + + + + {analytics?.data.map((row: any, index: any) => ( + + + + + + + + + ))} + +
DateSalesOrdersItemsAvg Order ValueNet Sales
{formatLongDate(row.date)}{formatCurrency(row.sales)}{row.orders.toLocaleString()}{row.items.toLocaleString()} + {formatCurrency(row.orders > 0 ? Math.round(row.sales / row.orders) : 0)} + + {formatCurrency(row.net_sales)} +
+
+
+
+ + {/* Summary Footer */} +
+
+
+
+

{formatCurrency(analytics?.summary.net_sales)}

+

Net Sales

+
+
+

{analytics?.summary.total_orders}

+

Total Orders

+
+
+

{formatCurrency(analytics?.summary.total_tax)}

+

Total Tax

+
+
+

+ {formatCurrency(analytics?.summary.total_discount)} +

+

Total Discount

+
+
+
+
+
) } diff --git a/src/app/[lang]/(dashboard)/(private)/dashboards/ecommerce/order/page.tsx b/src/app/[lang]/(dashboard)/(private)/dashboards/ecommerce/order/page.tsx new file mode 100644 index 0000000..d37d8c3 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/dashboards/ecommerce/order/page.tsx @@ -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 = '' }) => + +const EcomerceOrderReport = () => { + const today = new Date() + const monthAgo = new Date() + monthAgo.setDate(today.getDate() - 30) + + const [dateFrom, setDateFrom] = useState(monthAgo) + const [dateTo, setDateTo] = useState(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 + + return ( +
+ {/* Header */} +
+
+

Sales Analytics

+
+
+ +
+
+ + {/* Metrics Cards */} +
+ {metrics.map((metric, index) => ( +
+
+
+
+

{metric.title}

+

{metric.value}

+
+ + {metric.change} +
+
+
+ +
+
+
+
+ ))} +
+ + {/* Charts Section */} +
+ {/* Sales Trend Chart */} +
+
+
+ +

Sales Trend

+
+
+ + + + + `${(value / 1000).toFixed(0)}K`} stroke='#6b7280' /> + [ + 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)' + }} + /> + + + +
+
+
+ + {/* Sales Distribution */} +
+
+
+ +

Sales Distribution

+
+
+ + + `${name} ${(percent * 100).toFixed(0)}%`} + > + {performanceData?.map((entry: any, index: any) => ( + + ))} + + [formatCurrency(value), 'Sales']} + contentStyle={{ + backgroundColor: '#fff', + border: '1px solid #e5e7eb', + borderRadius: '8px', + boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' + }} + /> + + +
+
+
+
+ + {/* Orders & Items Chart */} +
+
+
+ +

Orders & Items Analysis

+
+
+ + + + + + + + + + +
+
+
+ + {/* Detailed Data Table */} +
+
+
+ +

Daily Performance Details

+
+
+ + + + + + + + + + + + + {analytics?.data.map((row: any, index: any) => ( + + + + + + + + + ))} + +
DateSalesOrdersItemsAvg Order ValueNet Sales
{formatLongDate(row.date)}{formatCurrency(row.sales)}{row.orders.toLocaleString()}{row.items.toLocaleString()} + {formatCurrency(row.orders > 0 ? Math.round(row.sales / row.orders) : 0)} + + {formatCurrency(row.net_sales)} +
+
+
+
+ + {/* Summary Footer */} +
+
+
+
+

{formatCurrency(analytics?.summary.net_sales)}

+

Net Sales

+
+
+

{analytics?.summary.total_orders}

+

Total Orders

+
+
+

{formatCurrency(analytics?.summary.total_tax)}

+

Total Tax

+
+
+

+ {formatCurrency(analytics?.summary.total_discount)} +

+

Total Discount

+
+
+
+
+
+ ) +} + +export default EcomerceOrderReport diff --git a/src/app/[lang]/(dashboard)/(private)/dashboards/ecommerce/product/page.tsx b/src/app/[lang]/(dashboard)/(private)/dashboards/ecommerce/product/page.tsx new file mode 100644 index 0000000..24b0fbe --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/dashboards/ecommerce/product/page.tsx @@ -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 = '' }) => + +const EcomerceProductReport = () => { + const today = new Date() + const monthAgo = new Date() + monthAgo.setDate(today.getDate() - 30) + + const [dateFrom, setDateFrom] = useState(monthAgo) + const [dateTo, setDateTo] = useState(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 + + return ( +
+ {/* Header */} +
+
+

Product Analytics

+
+ + {/* Filters */} +
+ +
+
+ + {/* Summary Cards */} +
+ {metrics.map((metric, index) => ( +
+
+
+
+

{metric.title}

+

{metric.value}

+

{metric.subtitle}

+
+
+ +
+
+
+
+ ))} +
+ + {/* Charts Section */} +
+ {/* Product Revenue Chart */} +
+
+
+ +

Top Products by Revenue

+
+
+ + + + + `${(value / 1000).toFixed(0)}K`} stroke='#6b7280' /> + [ + 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)' + }} + /> + + + +
+
+
+ + {/* Category Distribution */} +
+
+
+ +

Category Distribution

+
+
+ + + `${name} ${(percent * 100).toFixed(0)}%`} + > + {categoryData.map((entry, index) => ( + + ))} + + [formatCurrency(value), 'Revenue']} + contentStyle={{ + backgroundColor: '#fff', + border: '1px solid #e5e7eb', + borderRadius: '8px', + boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' + }} + /> + + +
+
+
+
+ + {/* Quantity vs Orders Chart */} +
+
+
+ +

Quantity Sold vs Order Count

+
+
+ + + + + + { + 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)' + }} + /> + + + + +
+
+
+ + {/* Detailed Products Table */} +
+
+
+
+ +

Product Performance Details

+
+
Showing {filteredData?.length} products
+
+ +
+ + + + + + + + + + + + + {filteredData?.map((product, index) => ( + + + + + + + + + ))} + +
ProductCategory handleSort('quantity_sold')} + > +
+ Quantity Sold + +
+
handleSort('revenue')} + > +
+ Revenue + +
+
handleSort('average_price')} + > +
+ Avg Price + +
+
handleSort('order_count')} + > +
+ Orders + +
+
+
{product.product_name}
+
+ + {product.category_name} + + + {product.quantity_sold.toLocaleString()} + + {formatCurrency(product.revenue)} + {formatCurrency(product.average_price)}{product.order_count.toLocaleString()}
+
+
+
+
+ ) +} + +export default EcomerceProductReport diff --git a/src/app/[lang]/(dashboard)/(private)/dashboards/finance/payment-method/page.tsx b/src/app/[lang]/(dashboard)/(private)/dashboards/finance/payment-method/page.tsx new file mode 100644 index 0000000..aa9778c --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/dashboards/finance/payment-method/page.tsx @@ -0,0 +1,355 @@ +import React from 'react'; + +const TablerIcon = ({ 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 ; + case 'card': + case 'credit_card': + case 'debit_card': + return ; + default: + return ; + } + }; + + // 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 ( +
+
+
+ ); + }; + + // Chip Component + const Chip = ({ label, colorClasses }: any) => { + return ( + + {label} + + ); + }; + + return ( +
+ {/* Header */} +
+
+ +
+
+

+ Payment Analytics +

+
+ +

+ {formatDate(analyticsData.date_from)} - {formatDate(analyticsData.date_to)} +

+
+
+
+ + {/* Summary Cards */} +
+
+
+
+
+

+ Total Revenue +

+

+ {formatCurrency(summary.total_amount)} +

+
+
+ +
+
+
+
+ +
+
+
+
+

+ Total Orders +

+

+ {summary.total_orders.toLocaleString()} +

+
+
+ +
+
+
+
+ +
+
+
+
+

+ Total Payments +

+

+ {summary.total_payments.toLocaleString()} +

+
+
+ +
+
+
+
+ +
+
+
+
+

+ Avg Order Value +

+

+ {formatCurrency(summary.average_order_value)} +

+
+
+ +
+
+
+
+
+ + {/* Payment Methods Breakdown */} +
+
+
+ +

+ Payment Methods Breakdown +

+
+ +
+ + + + + + + + + + + + + + {paymentMethods.map((method, index) => { + const colors = getPaymentMethodColors(method.payment_method_type); + return ( + + + + + + + + + + ); + })} + +
Payment MethodTypeAmountOrdersPaymentsPercentageUsage
+
+ {getPaymentMethodIcon(method.payment_method_type)} + + {method.payment_method_name} + +
+
+ + + {formatCurrency(method.total_amount)} + + {method.order_count.toLocaleString()} + + {method.payment_count.toLocaleString()} + + + {method.percentage.toFixed(1)}% + + +
+ + + {method.percentage.toFixed(1)}% + +
+
+
+ + {paymentMethods.length === 0 && ( +
+ +

+ No payment method data available for the selected period. +

+
+ )} +
+
+ + {/* Additional Stats */} +
+
+

+ Key Insights +

+
+
+
+
+ + + Most Used Payment Method + +
+ + {paymentMethods.length > 0 ? paymentMethods[0].payment_method_name : 'N/A'} + +
+
+
+ + + Revenue per Payment + +
+ + {formatCurrency(summary.total_payments > 0 ? summary.total_amount / summary.total_payments : 0)} + +
+
+
+
+
+ + + Payment Success Rate + +
+ + {((summary.total_payments / summary.total_orders) * 100).toFixed(1)}% + +
+
+
+ + + Payment Methods Used + +
+ + {paymentMethods.length} + +
+
+
+
+
+
+ ); +}; + +export default PaymentMethodAnalytics; diff --git a/src/components/date-picker/PickerBasic.tsx b/src/components/date-picker/PickerBasic.tsx new file mode 100644 index 0000000..f1142c5 --- /dev/null +++ b/src/components/date-picker/PickerBasic.tsx @@ -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 = ({ + dateFrom, + dateTo, + onChangeDateFrom, + onChangeDateTo +}) => { + return ( + + + + } + /> + + + + + } + /> + + + ) +} + +export default PickerBasic diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index 34f3b8a..33173b7 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -83,15 +83,27 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { }> {dictionary['navigation'].crm} {dictionary['navigation'].analytics} - {dictionary['navigation'].eCommerce} + + {dictionary['navigation'].orders} + {dictionary['navigation'].products} + + + + {dictionary['navigation'].paymentMethods} + + }> {dictionary['navigation'].dashboard} {dictionary['navigation'].list} - {dictionary['navigation'].details} - {dictionary['navigation'].edit} + + {dictionary['navigation'].details} + + + {dictionary['navigation'].edit} + {dictionary['navigation'].add} {dictionary['navigation'].category} @@ -109,7 +121,9 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { {dictionary['navigation'].list} - {dictionary['navigation'].addjustment} + + {dictionary['navigation'].addjustment} + {dictionary['navigation'].settings} diff --git a/src/services/queries/analytics.ts b/src/services/queries/analytics.ts new file mode 100644 index 0000000..36825a9 --- /dev/null +++ b/src/services/queries/analytics.ts @@ -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({ + 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({ + 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 + } + }) +} diff --git a/src/types/services/analytic.ts b/src/types/services/analytic.ts new file mode 100644 index 0000000..04006b7 --- /dev/null +++ b/src/types/services/analytic.ts @@ -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[] +} diff --git a/src/utils/transform.ts b/src/utils/transform.ts index 719b3b9..56cf5df 100644 --- a/src/utils/transform.ts +++ b/src/utils/transform.ts @@ -15,3 +15,10 @@ export const formatDate = (dateString: string) => { minute: '2-digit' }) } + +export const formatDateDDMMYYYY = (date: Date) => { + const day = String(date.getDate()).padStart(2, '0') + const month = String(date.getMonth() + 1).padStart(2, '0') + const year = date.getFullYear() + return `${day}-${month}-${year}` + } diff --git a/src/views/Login.tsx b/src/views/Login.tsx index 0ff4be6..c3f609f 100644 --- a/src/views/Login.tsx +++ b/src/views/Login.tsx @@ -243,8 +243,8 @@ const Login = ({ mode }: { mode: SystemMode }) => { Forgot password? -
New on our platform?