dress/app/Http/Controllers/AnalysisController.php

296 lines
11 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Enums\OrderChannel;
use App\Enums\OrderStatus;
use App\Enums\PaymentMethod;
use App\Models\Expense;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Payroll;
use App\Models\Purchase;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
class AnalysisController extends Controller
{
public function __invoke(Request $request)
{
$period = $request->input('period', 'all');
$startDate = $request->input('start_date');
$endDate = $request->input('end_date');
$applyFilter = function ($query, $column = 'created_at') use ($period, $startDate, $endDate) {
if ($startDate && $endDate) {
return $query->whereBetween($column, [$startDate.' 00:00:00', $endDate.' 23:59:59']);
}
return match ($period) {
'week' => $query->whereBetween($column, [now()->startOfWeek(), now()->endOfWeek()]),
'month' => $query->whereMonth($column, now()->month)->whereYear($column, now()->year),
'year' => $query->whereYear($column, now()->year),
default => $query,
};
};
$stats = [
'total_sales' => $this->getStats(fn () => $applyFilter(Order::query())->count()),
'total_revenue' => $this->getStats(fn () => $applyFilter(Order::query())->sum('total')),
'total_expenses' => $this->getStats(fn () => $applyFilter(Expense::query())->sum('amount')),
'total_payrolls' => $this->getStats(fn () => $applyFilter(Payroll::query(), 'period_month')->sum('total_salary')),
'cogs' => $this->getStats(fn () => $applyFilter(Order::query())->sum('cogs')),
'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')),
];
$stats['aov'] = $this->calculateAov($stats['total_revenue'], $stats['total_sales']);
$stats['gross_profit'] = $this->calculateDiff($stats['total_revenue'], $stats['cogs']);
$stats['net_profit'] = [
'value' => $stats['gross_profit']['value'] - $stats['total_expenses']['value'] - $stats['total_payrolls']['value'],
'yesterday' => null,
'change' => null,
];
$stats['profit_margin'] = $this->calculateMargin($stats['net_profit'], $stats['total_revenue']);
$isSqlite = DB::getDriverName() === 'sqlite';
$monthSelect = $isSqlite ? "CAST(strftime('%m', created_at) AS INTEGER)" : 'MONTH(created_at)';
// Revenue vs Purchases per month
$revenueByMonth = $applyFilter(DB::table('orders'))
->select(
DB::raw("$monthSelect as month"),
DB::raw('SUM(total) as total')
)
->groupBy(DB::raw($monthSelect))
->get();
$purchasesByMonth = $applyFilter(DB::table('purchases'))
->select(
DB::raw("$monthSelect as month"),
DB::raw('SUM(total) as total')
)
->groupBy(DB::raw($monthSelect))
->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 = $applyFilter(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 = $applyFilter(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 = $applyFilter(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 = $applyFilter(DB::table('order_items'), 'order_items.created_at')
->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 = $applyFilter(DB::table('orders'), 'orders.created_at')
->select('customer_name as name', DB::raw('SUM(total) as total'))
->whereNotNull('customer_name')
->groupBy('customer_name')
->orderByDesc('total')
->limit(5)
->get();
$hourSelect = $isSqlite ? "CAST(strftime('%H', created_at) AS INTEGER)" : 'HOUR(created_at)';
$salesByHourRaw = $applyFilter(DB::table('orders'))
->select(
DB::raw("$hourSelect as hour"),
DB::raw('COUNT(*) as total')
)
->groupBy(DB::raw($hourSelect))
->get();
$salesByHour = collect(range(0, 23))->map(function ($hour) use ($salesByHourRaw) {
$found = $salesByHourRaw->firstWhere('hour', $hour);
return [
'hour' => str_pad($hour, 2, '0', STR_PAD_LEFT).':00',
'total' => $found ? (int) $found->total : 0,
];
});
// Revenue & Profit & Volume per month
$ordersByMonth = $applyFilter(DB::table('orders'))
->select(
DB::raw("$monthSelect as month"),
DB::raw('SUM(total) as revenue'),
DB::raw('SUM(cogs) as cogs'),
DB::raw('COUNT(*) as count')
)
->groupBy(DB::raw($monthSelect))
->get();
$expensesByMonth = $applyFilter(DB::table('expenses'))
->select(
DB::raw("$monthSelect as month"),
DB::raw('SUM(amount) as total')
)
->groupBy(DB::raw($monthSelect))
->get();
$monthSelectPeriod = $isSqlite ? "CAST(strftime('%m', period_month) AS INTEGER)" : 'MONTH(period_month)';
$payrollsByMonth = $applyFilter(DB::table('payrolls'), 'period_month')
->select(
DB::raw("$monthSelectPeriod as month"),
DB::raw('SUM(total_salary) as total')
)
->whereNull('deleted_at')
->groupBy(DB::raw($monthSelectPeriod))
->get();
$revenueVsProfit = collect(range(1, 12))->map(function ($month) use ($ordersByMonth, $expensesByMonth, $payrollsByMonth, $monthNames) {
$orderFound = $ordersByMonth->firstWhere('month', $month);
$expenseFound = $expensesByMonth->firstWhere('month', $month);
$payrollFound = $payrollsByMonth->firstWhere('month', $month);
$revenue = $orderFound ? (float) $orderFound->revenue : 0;
$cogs = $orderFound ? (float) $orderFound->cogs : 0;
$expense = $expenseFound ? (float) $expenseFound->total : 0;
$payroll = $payrollFound ? (float) $payrollFound->total : 0;
$profit = $revenue - $cogs - $expense - $payroll;
return [
'month' => $monthNames[$month - 1],
'revenue' => $revenue,
'profit' => $profit,
];
});
$transactionVolume = collect(range(1, 12))->map(function ($month) use ($ordersByMonth, $monthNames) {
$found = $ordersByMonth->firstWhere('month', $month);
$count = $found ? (int) $found->count : 0;
$revenue = $found ? (float) $found->revenue : 0;
$aov = $count > 0 ? $revenue / $count : 0;
return [
'month' => $monthNames[$month - 1],
'count' => $count,
'aov' => $aov,
];
});
$topCategories = $applyFilter(DB::table('order_items'), 'order_items.created_at')
->join('products', 'order_items.product_id', '=', 'products.id')
->join('category_product', 'products.id', '=', 'category_product.product_id')
->join('categories', 'category_product.category_id', '=', 'categories.id')
->select('categories.name', DB::raw('SUM(order_items.qty) as total'))
->whereNull('order_items.deleted_at')
->groupBy('categories.id', 'categories.name')
->orderByDesc('total')
->limit(5)
->get();
return Inertia::render('analysis', [
'stats' => $stats,
'salesByMonth' => $salesByMonth,
'revenueVsProfit' => $revenueVsProfit,
'transactionVolume' => $transactionVolume,
'salesByHour' => $salesByHour,
'paymentMethods' => $paymentMethods,
'orderStatuses' => $orderStatuses,
'orderChannels' => $orderChannels,
'topProducts' => $topProducts,
'topCustomers' => $topCustomers,
'topCategories' => $topCategories,
'filters' => [
'period' => $period,
'start_date' => $startDate,
'end_date' => $endDate,
],
]);
}
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,
];
}
}