diff --git a/app/Http/Controllers/AnalysisController.php b/app/Http/Controllers/AnalysisController.php
new file mode 100644
index 0000000..5c0f78d
--- /dev/null
+++ b/app/Http/Controllers/AnalysisController.php
@@ -0,0 +1,187 @@
+ $this->getStats(fn() => Order::count()),
+ 'total_revenue' => $this->getStats(fn() => Order::sum('total')),
+ 'total_expenses' => $this->getStats(fn() => Expense::sum('amount')),
+ 'cogs' => $this->getStats(fn() => Order::sum('hpp')),
+ 'total_discount' => $this->getStats(fn() => Order::sum('discount')),
+ 'total_purchases' => $this->getStats(fn() => Purchase::sum('total')),
+ 'products_sold' => $this->getStats(fn() => OrderItem::sum('qty')),
+ 'total_customers' => $this->getStats(fn() => Order::distinct('customer_name')->count('customer_name')),
+ ];
+
+ $stats['aov'] = $this->calculateAov($stats['total_revenue'], $stats['total_sales']);
+ $stats['gross_profit'] = $this->calculateDiff($stats['total_revenue'], $stats['cogs']);
+ $stats['net_profit'] = $this->calculateDiff($stats['gross_profit'], $stats['total_expenses']);
+ $stats['profit_margin'] = $this->calculateMargin($stats['net_profit'], $stats['total_revenue']);
+
+ // Revenue vs Purchases per month (this year)
+ $revenueByMonth = DB::table('orders')
+ ->select(
+ DB::raw('MONTH(created_at) as month'),
+ DB::raw('SUM(total) as total')
+ )
+ ->groupBy(DB::raw('MONTH(created_at)'))
+ ->get();
+
+ $purchasesByMonth = DB::table('purchases')
+ ->select(
+ DB::raw('MONTH(created_at) as month'),
+ DB::raw('SUM(total) as total')
+ )
+ ->groupBy(DB::raw('MONTH(created_at)'))
+ ->get();
+
+ $monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];
+
+ $salesByMonth = collect(range(1, 12))->map(function ($month) use ($revenueByMonth, $purchasesByMonth, $monthNames) {
+ $revenueFound = $revenueByMonth->firstWhere('month', $month);
+ $purchasesFound = $purchasesByMonth->firstWhere('month', $month);
+
+ return [
+ 'month' => $monthNames[$month - 1],
+ 'sales' => $revenueFound ? (float) $revenueFound->total : 0,
+ 'purchase' => $purchasesFound ? (float) $purchasesFound->total : 0,
+ ];
+ });
+
+ $paymentMethods = DB::table('orders')
+ ->select('payment_method as name', DB::raw('COUNT(*) as total'))
+ ->groupBy('payment_method')
+ ->get()
+ ->map(function ($item) {
+ if ($item->name) {
+ $item->name = PaymentMethod::tryFrom($item->name)?->label() ?? $item->name;
+ }
+
+ return $item;
+ });
+
+ $orderStatuses = DB::table('orders')
+ ->select('order_status as name', DB::raw('COUNT(*) as total'))
+ ->groupBy('order_status')
+ ->get()
+ ->map(function ($item) {
+ if ($item->name) {
+ $item->name = OrderStatus::tryFrom($item->name)?->label() ?? $item->name;
+ }
+
+ return $item;
+ });
+
+ $orderChannels = DB::table('orders')
+ ->select('order_channel as name', DB::raw('COUNT(*) as total'))
+ ->groupBy('order_channel')
+ ->get()
+ ->map(function ($item) {
+ if ($item->name) {
+ $item->name = OrderChannel::tryFrom($item->name)?->label() ?? $item->name;
+ }
+
+ return $item;
+ });
+
+ $topProducts = DB::table('order_items')
+ ->join('products', 'order_items.product_id', '=', 'products.id')
+ ->select('products.name as name', DB::raw('SUM(order_items.qty) as total'))
+ ->groupBy('products.name')
+ ->orderByDesc('total')
+ ->limit(5)
+ ->get();
+
+ $topCustomers = DB::table('orders')
+ ->select('customer_name as name', DB::raw('SUM(total) as total'))
+ ->whereNotNull('customer_name')
+ ->groupBy('customer_name')
+ ->orderByDesc('total')
+ ->limit(5)
+ ->get();
+
+ $ordersByHourRaw = DB::table('orders')
+ ->select(
+ DB::raw('HOUR(created_at) as hour'),
+ DB::raw('COUNT(*) as total')
+ )
+ ->groupBy(DB::raw('HOUR(created_at)'))
+ ->get();
+
+ $ordersByHour = collect(range(0, 23))->map(function ($hour) use ($ordersByHourRaw) {
+ $found = $ordersByHourRaw->firstWhere('hour', $hour);
+
+ return [
+ 'hour' => str_pad($hour, 2, '0', STR_PAD_LEFT) . ':00',
+ 'total' => $found ? (int) $found->total : 0,
+ ];
+ });
+
+ return Inertia::render('analysis', [
+ 'stats' => $stats,
+ 'salesByMonth' => $salesByMonth,
+ 'ordersByHour' => $ordersByHour,
+ 'paymentMethods' => $paymentMethods,
+ 'orderStatuses' => $orderStatuses,
+ 'orderChannels' => $orderChannels,
+ 'topProducts' => $topProducts,
+ 'topCustomers' => $topCustomers,
+ ]);
+ }
+
+ private function getStats(callable $callback): array
+ {
+ $value = $callback();
+
+ return [
+ 'value' => $value,
+ 'yesterday' => null,
+ 'change' => null,
+ ];
+ }
+
+ private function calculateAov($revenue, $sales): array
+ {
+ $value = $sales['value'] > 0 ? $revenue['value'] / $sales['value'] : 0;
+
+ return [
+ 'value' => $value,
+ 'yesterday' => null,
+ 'change' => null,
+ ];
+ }
+
+ private function calculateDiff($a, $b): array
+ {
+ return [
+ 'value' => $a['value'] - $b['value'],
+ 'yesterday' => null,
+ 'change' => null,
+ ];
+ }
+
+ private function calculateMargin($profit, $revenue): array
+ {
+ $value = $revenue['value'] > 0 ? ($profit['value'] / $revenue['value']) * 100 : 0;
+
+ return [
+ 'value' => round($value, 2),
+ 'yesterday' => null,
+ 'change' => null,
+ ];
+ }
+}
diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx
index 07ae19c..7f84a1d 100644
--- a/resources/js/components/app-sidebar.tsx
+++ b/resources/js/components/app-sidebar.tsx
@@ -1,5 +1,3 @@
-import { Link } from '@inertiajs/react';
-import { Boxes, DollarSign, History, LayoutGrid, List, ScrollText, Settings, ShoppingBag, ShoppingCart, User, Wallet } from 'lucide-react';
import AppLogo from '@/components/app-logo';
import { NavMain } from '@/components/nav-main';
import {
@@ -10,8 +8,10 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/ui/sidebar';
-import { dashboard } from '@/routes';
+import { analysis, dashboard } from '@/routes';
import category from '@/routes/category';
+import { Link } from '@inertiajs/react';
+import { BarChart3, Boxes, DollarSign, History, LayoutGrid, List, ScrollText, Settings, ShoppingBag, ShoppingCart, User, Wallet } from 'lucide-react';
import expense from '@/routes/expense';
import order from '@/routes/order';
@@ -28,6 +28,11 @@ const mainNavItems: NavItem[] = [
href: dashboard().url,
icon: LayoutGrid,
},
+ {
+ title: 'Analisa',
+ href: analysis().url,
+ icon: BarChart3,
+ },
];
const masterNavItems: NavItem[] = [
diff --git a/resources/js/components/cards/stat-card.tsx b/resources/js/components/cards/stat-card.tsx
index f3a9fd1..7a0ca95 100644
--- a/resources/js/components/cards/stat-card.tsx
+++ b/resources/js/components/cards/stat-card.tsx
@@ -20,7 +20,8 @@ export function StatCard({
isPercentage = false,
className
}: StatCardProps) {
- const isPositive = stat.change >= 0;
+ const hasComparison = stat.yesterday !== null;
+ const isPositive = stat.change !== null && stat.change >= 0;
return (