diff --git a/app/Http/Controllers/AnalysisController.php b/app/Http/Controllers/AnalysisController.php index a03e400..69220e9 100644 --- a/app/Http/Controllers/AnalysisController.php +++ b/app/Http/Controllers/AnalysisController.php @@ -44,7 +44,14 @@ public function __invoke(Request $request) 'total_discount' => $this->getStats(fn () => $applyFilter(Order::query())->sum('discount')), 'total_purchases' => $this->getStats(fn () => $applyFilter(Purchase::query())->sum('total')), 'products_sold' => $this->getStats(fn () => $applyFilter(OrderItem::query())->sum('qty')), - 'total_customers' => $this->getStats(fn () => $applyFilter(Order::query())->distinct('customer_name')->count('customer_name')), + 'total_customers' => $this->getStats(fn () => $applyFilter(Order::query())->whereNotNull('customer_name')->distinct('customer_name')->count('customer_name')), + 'repeat_customers' => $this->getStats(fn () => $applyFilter(DB::table('orders')) + ->whereNotNull('customer_name') + ->select('customer_name') + ->groupBy('customer_name') + ->havingRaw('COUNT(*) > 1') + ->get() + ->count()), ]; $stats['aov'] = $this->calculateAov($stats['total_revenue'], $stats['total_sales']); @@ -56,6 +63,14 @@ public function __invoke(Request $request) ]; $stats['profit_margin'] = $this->calculateMargin($stats['net_profit'], $stats['total_revenue']); + $totalCustomers = $stats['total_customers']['value']; + $repeatCustomers = $stats['repeat_customers']['value']; + $stats['repeat_order_percentage'] = [ + 'value' => $totalCustomers > 0 ? round(($repeatCustomers / $totalCustomers) * 100, 2) : 0, + 'yesterday' => null, + 'change' => null, + ]; + $isSqlite = DB::getDriverName() === 'sqlite'; $monthSelect = $isSqlite ? "CAST(strftime('%m', created_at) AS INTEGER)" : 'MONTH(created_at)'; diff --git a/resources/js/components/charts/knob-chart.tsx b/resources/js/components/charts/knob-chart.tsx new file mode 100644 index 0000000..b4fc2b6 --- /dev/null +++ b/resources/js/components/charts/knob-chart.tsx @@ -0,0 +1,129 @@ +"use client" + +import React, { useMemo } from "react" +import { + Label, + PolarGrid, + PolarRadiusAxis, + RadialBar, + RadialBarChart, +} from "recharts" + +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { ChartConfig, ChartContainer } from "@/components/ui/chart" + +export const KnobChart = React.memo(function KnobChart({ + title, + description, + percentage, + label, + footerText +}: { + title: string + description: string + percentage: number + label?: string + footerText?: string +}) { + const { chartData, chartConfig } = useMemo(() => { + return { + chartData: [ + { + name: "value", + value: percentage, + fill: "hsl(var(--primary))", + }, + ], + chartConfig: { + value: { + label: label || "Value", + }, + } satisfies ChartConfig, + } + }, [percentage, label]) + + return ( + + + {title} + {description} + + + + + + + + + + + + {footerText && ( + +
+ {footerText} +
+
+ )} +
+ ) +}) diff --git a/resources/js/pages/analysis.tsx b/resources/js/pages/analysis.tsx index d30796f..d11b9d7 100644 --- a/resources/js/pages/analysis.tsx +++ b/resources/js/pages/analysis.tsx @@ -16,6 +16,7 @@ import { RevenueVsProfitChart } from '../components/charts/revenue-vs-profit-cha import { RevenueVsPurchasesChart } from '../components/charts/revenue-vs-purchases-chart'; import { SalesByHourChart } from '../components/charts/sales-by-hour-chart'; import { TransactionVolumeChart } from '../components/charts/transaction-volume-chart'; +import { KnobChart } from '../components/charts/knob-chart'; export default function Analisa() { const { stats, salesByMonth, revenueVsProfit, transactionVolume, salesByHour, paymentMethods, orderStatuses, orderChannels, topProducts, topCustomers, topCategories, filters } = @@ -228,7 +229,7 @@ export default function Analisa() { {/* Activity & Volume Grid */} -
+
+