store/app/Services/System/AnalysisService.php

786 lines
38 KiB
PHP

<?php
namespace App\Services\System;
use App\Enums\EmployeeAdvanceStatus;
use App\Enums\OrderStatus;
use App\Enums\PriceType;
use App\Enums\ProductStockQuality;
use App\Enums\Role;
use App\Models\Attendance;
use App\Models\CashAccount;
use App\Models\CashTransaction;
use App\Models\Employee;
use App\Models\EmployeeAdvance;
use App\Models\Expense;
use App\Models\LeaveRequest;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\ProductVariant;
use App\Models\Purchase;
use App\Models\RawMaterialPrice;
use App\Models\User;
use App\Services\Concerns\CachesQuery;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class AnalysisService
{
use CachesQuery;
public function getAttendance(): array
{
return $this->cacheRemember('analysis:get_attendance', 900, function () {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
$employeeQuery = Employee::query();
$attendanceQuery = Attendance::query()->where('attendance_date', Carbon::today());
$leaveRequestQuery = LeaveRequest::query()
->approved()
->where('start_date', '<=', Carbon::today())
->where('end_date', '>=', Carbon::today());
if (! $isSuper) {
$employeeId = $user?->employee?->id;
$employeeQuery->where('id', $employeeId);
$attendanceQuery->where('employee_id', $employeeId);
$leaveRequestQuery->where('employee_id', $employeeId);
}
$totalEmployees = $employeeQuery->count();
$present = $attendanceQuery->distinct('employee_id')->count('employee_id');
$onLeave = $leaveRequestQuery->distinct('employee_id')->count('employee_id');
$absent = max(0, $totalEmployees - $present - $onLeave);
return [
'total_employees' => $totalEmployees,
'percentage' => $totalEmployees > 0 ? round($present / $totalEmployees * 100) : 0,
'present' => $present,
'absent' => $absent,
'on_leave' => $onLeave,
];
});
}
public function getMyAttendance(User $user, ?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_my_attendance', 900, function () use ($user, $startDate, $endDate) {
$employee = $user->employee;
if ($employee === null) {
return [
'total_days' => 0,
'present_days' => 0,
'absent_days' => 0,
'leave_days' => 0,
'percentage' => 0,
];
}
$start = $startDate ?? ($employee->join_date ? Carbon::parse($employee->join_date) : Carbon::today()->startOfMonth());
$end = $endDate ?? Carbon::today();
$totalDays = 0;
$presentDays = 0;
$current = $start->copy()->startOfDay();
while ($current->lte($end)) {
if ($current->isWeekday()) {
$totalDays++;
$hasAttendance = Attendance::query()
->where('employee_id', $employee->id)
->whereDate('attendance_date', $current)
->exists();
if ($hasAttendance) {
$presentDays++;
}
}
$current->addDay();
}
$leaveDays = LeaveRequest::query()
->approved()
->where('employee_id', $employee->id)
->where('start_date', '<=', $end->toDateString())
->where('end_date', '>=', $start->toDateString())
->get()
->sum(fn ($leave) => max(
0,
min($leave->end_date, $end->toDateString())
- max($leave->start_date, $start->toDateString())
) / 86400 + 1);
$leaveDays = (int) $leaveDays;
$absentDays = max(0, $totalDays - $presentDays - $leaveDays);
$percentage = $totalDays > 0 ? round(($presentDays / $totalDays) * 100) : 0;
return [
'total_days' => $totalDays,
'present_days' => $presentDays,
'absent_days' => $absentDays,
'leave_days' => $leaveDays,
'percentage' => $percentage,
];
});
}
public function getCashOverview(): array
{
return $this->cacheRemember('analysis:get_cash_overview', 900, function () {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
$totalBalanceQuery = CashAccount::query();
$transactionQuery = CashTransaction::query()->whereDate('created_at', Carbon::today());
if (! $isSuper) {
$transactionQuery->where('created_by_id', $user->id);
$totalBalanceQuery->whereHas('transactions', fn ($q) => $q->where('created_by_id', $user->id));
}
$totalBalance = $totalBalanceQuery->sum('balance');
$summary = $transactionQuery
->selectRaw("
COUNT(*) as total_transactions,
COALESCE(SUM(CASE WHEN type = 'deposit' THEN amount ELSE 0 END), 0) as total_deposit,
COALESCE(SUM(CASE WHEN type = 'withdrawal' THEN amount ELSE 0 END), 0) as total_withdrawal
")
->first();
return [
'total_balance' => (int) $totalBalance,
'total_transactions' => (int) ($summary->total_transactions ?? 0),
'total_deposit' => (int) ($summary->total_deposit ?? 0),
'total_withdrawal' => (int) ($summary->total_withdrawal ?? 0),
];
});
}
public function getTopSuppliers(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_top_suppliers', 900, function () use ($startDate, $endDate) {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
return Purchase::query()
->when($startDate && $endDate, fn ($q) => $q->whereBetween('purchases.created_at', [$startDate, $endDate]))
->when(! $isSuper, fn ($q) => $q->where('purchases.created_by_id', $user->id))
->join('suppliers', 'purchases.supplier_id', '=', 'suppliers.id')
->selectRaw('suppliers.name, SUM(purchases.total) as total_amount, COUNT(purchases.id) as purchase_count')
->groupBy('suppliers.id', 'suppliers.name')
->orderByDesc('total_amount')
->limit(5)
->get()
->map(fn ($item) => [
'name' => $item->name,
'total_amount' => (int) $item->total_amount,
'purchase_count' => (int) $item->purchase_count,
])
->toArray();
});
}
public function getTopCustomers(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_top_customers', 900, function () use ($startDate, $endDate) {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
$isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false;
return Order::query()
->completed()
->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id))
->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id))
->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate]))
->join('customers', 'orders.customer_id', '=', 'customers.id')
->selectRaw('customers.name, SUM(orders.total_amount) as total_amount, COUNT(orders.id) as order_count')
->groupBy('customers.id', 'customers.name')
->orderByDesc('total_amount')
->limit(5)
->get()
->map(fn ($item) => [
'name' => $item->name,
'total_amount' => (int) $item->total_amount,
'order_count' => (int) $item->order_count,
])
->toArray();
});
}
public function getRevenueSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_revenue_summary', 900, function () use ($startDate, $endDate) {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
$isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false;
$revenueSummary = Order::query()
->completed()
->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id))
->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id))
->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate]))
->selectRaw('SUM(total_amount) as total_revenue, SUM(discount) as total_discount, COUNT(*) as total_orders, AVG(total_amount) as avg_order')
->first();
$totalMarketplaceFees = Order::query()
->completed()
->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id))
->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id))
->whereNotNull('marketplace_settings_snapshot')
->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate]))
->get()
->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0));
$totalCostPrice = OrderItem::query()
->join('orders', 'order_items.order_id', '=', 'orders.id')
->leftJoin('product_prices', function ($join) {
$join->on('order_items.product_variant_id', '=', 'product_prices.variant_id')
->where('product_prices.type', PriceType::HARGA_MODAL->value);
})
->where('orders.status', OrderStatus::COMPLETED)
->when(! $isSuper && $isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id))
->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('orders.created_by_id', $user->id))
->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate]))
->selectRaw('COALESCE(SUM(order_items.quantity * product_prices.price), 0) as total_hpp')
->value('total_hpp');
return [
'total_revenue' => (int) ($revenueSummary->total_revenue ?? 0),
'total_discount' => (int) ($revenueSummary->total_discount ?? 0),
'total_marketplace_fees' => $totalMarketplaceFees,
'total_cost_price' => (int) ($totalCostPrice ?? 0),
'total_deduction' => (int) ($revenueSummary->total_discount ?? 0) + $totalMarketplaceFees,
'total_orders' => (int) ($revenueSummary->total_orders ?? 0),
'avg_order' => (int) ($revenueSummary->avg_order ?? 0),
];
});
}
public function getMonthlyRevenue(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_monthly_revenue', 900, function () use ($startDate, $endDate) {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
$isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false;
$query = Order::query()->completed()
->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id))
->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id));
$feeQuery = Order::query()->completed()
->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id))
->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id))
->whereNotNull('marketplace_settings_snapshot');
if ($startDate && $endDate) {
$query->whereBetween('orders.created_at', [$startDate, $endDate]);
$feeQuery->whereBetween('orders.created_at', [$startDate, $endDate]);
}
$monthlyData = $query
->selectRaw("
DATE_FORMAT(orders.created_at, '%Y-%m') as month_key,
SUM(total_amount) as total_revenue,
SUM(discount) as total_discount
")
->groupBy('month_key')
->orderBy('month_key')
->get();
$monthlyFees = $feeQuery
->selectRaw("DATE_FORMAT(orders.created_at, '%Y-%m') as month_key")
->get()
->groupBy('month_key')
->map(fn ($orders) => $orders->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0)));
$monthlyItemsData = OrderItem::query()
->join('orders', 'order_items.order_id', '=', 'orders.id')
->leftJoin('product_prices', function ($join) {
$join->on('order_items.product_variant_id', '=', 'product_prices.variant_id')
->where('product_prices.type', PriceType::HARGA_MODAL->value);
})
->where('orders.status', OrderStatus::COMPLETED)
->when(! $isSuper && $isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id))
->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('orders.created_by_id', $user->id))
->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate]))
->selectRaw("
DATE_FORMAT(orders.created_at, '%Y-%m') as month_key,
order_items.stock_quality,
SUM(order_items.subtotal) as subtotal,
COALESCE(SUM(order_items.quantity * product_prices.price), 0) as hpp
")
->groupBy('month_key', 'order_items.stock_quality')
->get()
->groupBy('month_key');
if ($monthlyData->isEmpty()) {
return [];
}
$start = $startDate ?? Carbon::parse($monthlyData->first()->month_key.'-01');
$end = $endDate ?? Carbon::parse($monthlyData->last()->month_key.'-01')->endOfMonth();
$result = [];
$current = $start->copy()->startOfMonth();
while ($current->lte($end)) {
$key = $current->format('Y-m');
$monthLabel = $current->locale('id')->translatedFormat('M Y');
$revenue = $monthlyData->firstWhere('month_key', $key);
$fees = $monthlyFees->get($key, 0);
$discount = (int) ($revenue->total_discount ?? 0);
$deduction = $discount + $fees;
$monthItems = $monthlyItemsData->get($key, collect());
$warehouseItem = $monthItems->first(fn ($item) => ($item->stock_quality instanceof ProductStockQuality ? $item->stock_quality->value : $item->stock_quality) === 'good'
);
$retailItem = $monthItems->first(fn ($item) => ($item->stock_quality instanceof ProductStockQuality ? $item->stock_quality->value : $item->stock_quality) === 'retail'
);
$rejectItem = $monthItems->first(fn ($item) => ($item->stock_quality instanceof ProductStockQuality ? $item->stock_quality->value : $item->stock_quality) === 'reject'
);
$warehouseSubtotal = (int) ($warehouseItem?->subtotal ?? 0);
$warehouseHpp = (int) ($warehouseItem?->hpp ?? 0);
$retailSubtotal = (int) ($retailItem?->subtotal ?? 0);
$retailHpp = (int) ($retailItem?->hpp ?? 0);
$rejectSubtotal = (int) ($rejectItem?->subtotal ?? 0);
$rejectHpp = (int) ($rejectItem?->hpp ?? 0);
$totalItemsSubtotal = $warehouseSubtotal + $retailSubtotal + $rejectSubtotal;
$hpp = $warehouseHpp + $retailHpp + $rejectHpp;
$warehouseDeduction = 0;
$retailDeduction = 0;
if ($totalItemsSubtotal > 0) {
$warehouseDeduction = ($warehouseSubtotal / $totalItemsSubtotal) * $deduction;
$retailDeduction = ($retailSubtotal / $totalItemsSubtotal) * $deduction;
}
$netWarehouse = $warehouseSubtotal - $warehouseDeduction - $warehouseHpp;
$netRetail = $retailSubtotal - $retailDeduction - $retailHpp;
$result[] = [
'month' => $monthLabel,
'total' => (int) ($revenue->total_revenue ?? 0),
'net' => (int) ($revenue->total_revenue ?? 0) - $deduction - $hpp,
'net_warehouse' => (int) round($netWarehouse),
'net_retail' => (int) round($netRetail),
'deduction' => $deduction,
];
$current->addMonth();
}
return $result;
});
}
public function getExpenseSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_expense_summary', 900, function () use ($startDate, $endDate) {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
$purchase = Purchase::query()
->when($startDate && $endDate, fn ($q) => $q->whereBetween('purchases.created_at', [$startDate, $endDate]))
->when(! $isSuper, fn ($q) => $q->where('purchases.created_by_id', $user->id))
->selectRaw('COALESCE(SUM(total), 0) as total')
->first();
$expenses = Expense::query()
->when($startDate && $endDate, fn ($q) => $q->whereBetween('expenses.created_at', [$startDate, $endDate]))
->when(! $isSuper, fn ($q) => $q->where('expenses.created_by_id', $user->id))
->selectRaw('COALESCE(SUM(amount), 0) as total')
->first();
$employeeAdvance = EmployeeAdvance::query()
->whereIn('status', [EmployeeAdvanceStatus::APPROVED, EmployeeAdvanceStatus::PARTIALLY_PAID])
->when($startDate && $endDate, fn ($q) => $q->whereBetween('employee_advances.created_at', [$startDate, $endDate]))
->when(! $isSuper, fn ($q) => $q->where('employee_id', $user->employee?->id))
->selectRaw('COALESCE(SUM(amount - paid_amount), 0) as total')
->first();
$purchaseTotal = (int) ($purchase->total ?? 0);
if (! $user?->hasAnyRole([Role::DEVELOPER->value, Role::OWNER->value])) {
$purchaseTotal = 0;
}
$expenseTotal = (int) ($expenses->total ?? 0);
$advanceTotal = (int) ($employeeAdvance->total ?? 0);
return [
'total' => $purchaseTotal + $expenseTotal + $advanceTotal,
'purchase_total' => $purchaseTotal,
'expense_total' => $expenseTotal,
'advance_total' => $advanceTotal,
];
});
}
public function getMonthlyExpense(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_monthly_expense', 900, function () use ($startDate, $endDate) {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
$purchases = Purchase::query()
->when($startDate && $endDate, fn ($q) => $q->whereBetween('purchases.created_at', [$startDate, $endDate]))
->when(! $isSuper, fn ($q) => $q->where('purchases.created_by_id', $user->id))
->selectRaw("DATE_FORMAT(purchases.created_at, '%Y-%m') as month_key, COALESCE(SUM(total), 0) as total")
->groupBy('month_key')
->orderBy('month_key')
->get();
$expenses = Expense::query()
->when($startDate && $endDate, fn ($q) => $q->whereBetween('expenses.created_at', [$startDate, $endDate]))
->when(! $isSuper, fn ($q) => $q->where('expenses.created_by_id', $user->id))
->selectRaw("DATE_FORMAT(expenses.created_at, '%Y-%m') as month_key, COALESCE(SUM(amount), 0) as total")
->groupBy('month_key')
->orderBy('month_key')
->get();
$advances = EmployeeAdvance::query()
->whereIn('status', [EmployeeAdvanceStatus::APPROVED, EmployeeAdvanceStatus::PARTIALLY_PAID])
->when($startDate && $endDate, fn ($q) => $q->whereBetween('employee_advances.created_at', [$startDate, $endDate]))
->when(! $isSuper, fn ($q) => $q->where('employee_id', $user->employee?->id))
->selectRaw("DATE_FORMAT(employee_advances.created_at, '%Y-%m') as month_key, COALESCE(SUM(amount - paid_amount), 0) as total")
->groupBy('month_key')
->orderBy('month_key')
->get();
$allMonths = collect()
->merge($purchases->pluck('month_key'))
->merge($expenses->pluck('month_key'))
->merge($advances->pluck('month_key'))
->unique()
->sort()
->values();
if ($allMonths->isEmpty()) {
return [];
}
$start = $startDate ?? Carbon::parse($allMonths->first().'-01');
$end = $endDate ?? Carbon::parse($allMonths->last().'-01')->endOfMonth();
$result = [];
$current = $start->copy()->startOfMonth();
while ($current->lte($end)) {
$key = $current->format('Y-m');
$monthLabel = $current->locale('id')->translatedFormat('M Y');
$purchaseAmount = (int) ($purchases->firstWhere('month_key', $key)->total ?? 0);
if (! $user?->hasAnyRole([Role::DEVELOPER->value, Role::OWNER->value])) {
$purchaseAmount = 0;
}
$expenseAmount = (int) ($expenses->firstWhere('month_key', $key)->total ?? 0);
$advanceAmount = (int) ($advances->firstWhere('month_key', $key)->total ?? 0);
$result[] = [
'month' => $monthLabel,
'total' => $purchaseAmount + $expenseAmount + $advanceAmount,
'purchase' => $purchaseAmount,
'expense' => $expenseAmount,
'advance' => $advanceAmount,
];
$current->addMonth();
}
return $result;
});
}
public function getProfitMetrics(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_profit_metrics', 900, function () use ($startDate, $endDate) {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
$isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false;
$orderQuery = Order::query()->completed()
->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id))
->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id));
if ($startDate && $endDate) {
$orderQuery->whereBetween('orders.created_at', [$startDate, $endDate]);
}
$revenueData = (clone $orderQuery)
->selectRaw('
COUNT(*) as total_orders,
SUM(total_amount) as total_revenue,
SUM(discount) as total_discount,
SUM(subtotal) as total_subtotal
')
->first();
$totalRevenue = (int) ($revenueData->total_revenue ?? 0);
$totalDiscount = (int) ($revenueData->total_discount ?? 0);
$totalSubtotal = (int) ($revenueData->total_subtotal ?? 0);
$totalOrders = (int) ($revenueData->total_orders ?? 0);
$marketplaceFees = (clone $orderQuery)
->whereNotNull('marketplace_settings_snapshot')
->get()
->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0));
$itemsData = OrderItem::query()
->join('orders', 'order_items.order_id', '=', 'orders.id')
->where('orders.status', OrderStatus::COMPLETED)
->when(! $isSuper && $isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id))
->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('orders.created_by_id', $user->id))
->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate]))
->selectRaw('
SUM(order_items.quantity) as total_qty,
COUNT(order_items.id) as total_items
')
->first();
$totalQty = (int) ($itemsData->total_qty ?? 0);
$totalItems = (int) ($itemsData->total_items ?? 0);
// HPP from product_prices with type = 'harga_modal'
$hpp = OrderItem::query()
->join('orders', 'order_items.order_id', '=', 'orders.id')
->leftJoin('product_prices', function ($join) {
$join->on('order_items.product_variant_id', '=', 'product_prices.variant_id')
->where('product_prices.type', PriceType::HARGA_MODAL->value);
})
->where('orders.status', OrderStatus::COMPLETED)
->when(! $isSuper && $isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id))
->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('orders.created_by_id', $user->id))
->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate]))
->selectRaw('COALESCE(SUM(order_items.quantity * product_prices.price), 0) as total_hpp')
->value('total_hpp');
$totalHpp = (int) ($hpp ?? 0);
// Expenses
$purchaseTotal = Purchase::query()
->when($startDate && $endDate, fn ($q) => $q->whereBetween('purchases.created_at', [$startDate, $endDate]))
->when(! $isSuper, fn ($q) => $q->where('purchases.created_by_id', $user->id))
->sum('total');
if (! $user?->hasAnyRole([Role::DEVELOPER->value, Role::OWNER->value])) {
$purchaseTotal = 0;
}
$expenseTotal = Expense::query()
->when($startDate && $endDate, fn ($q) => $q->whereBetween('expenses.created_at', [$startDate, $endDate]))
->when(! $isSuper, fn ($q) => $q->where('expenses.created_by_id', $user->id))
->sum('amount');
$advanceTotal = EmployeeAdvance::query()
->whereIn('status', [EmployeeAdvanceStatus::APPROVED, EmployeeAdvanceStatus::PARTIALLY_PAID])
->when($startDate && $endDate, fn ($q) => $q->whereBetween('employee_advances.created_at', [$startDate, $endDate]))
->when(! $isSuper, fn ($q) => $q->where('employee_id', $user->employee?->id))
->sum(DB::raw('amount - paid_amount'));
$totalExpenses = (int) $purchaseTotal + (int) $expenseTotal + (int) $advanceTotal;
$grossProfit = $totalRevenue - $totalHpp - $totalDiscount - $marketplaceFees;
$netProfit = $grossProfit - (int) $expenseTotal - (int) $advanceTotal;
$profitMargin = $totalRevenue > 0 ? round(($netProfit / $totalRevenue) * 100, 1) : 0;
$aov = $totalOrders > 0 ? (int) round($totalRevenue / $totalOrders) : 0;
$itemsPerTransaction = $totalOrders > 0 ? round($totalItems / $totalOrders, 1) : 0;
return [
'total_orders' => $totalOrders,
'total_products_sold' => $totalQty,
'hpp' => $totalHpp,
'gross_profit' => $grossProfit,
'net_profit' => $netProfit,
'profit_margin' => $profitMargin,
'aov' => $aov,
'items_per_transaction' => $itemsPerTransaction,
];
});
}
public function getRawMaterialStock(): array
{
return $this->cacheRemember('analysis:get_raw_material_stock', 900, function () {
$prices = RawMaterialPrice::query()
->join('raw_materials', 'raw_material_prices.raw_material_id', '=', 'raw_materials.id')
->selectRaw('
raw_materials.unit,
SUM(raw_material_prices.stock) as total_stock,
SUM(raw_material_prices.stock * raw_material_prices.price) as total_value
')
->groupBy('raw_materials.unit')
->get()
->keyBy('unit');
$totalStock = (float) $prices->sum('total_stock');
$totalValue = (int) $prices->sum('total_value');
return [
'total_stock' => round($totalStock, 2),
'total_value' => $totalValue,
'by_unit' => [
'yard' => round((float) ($prices->get('yard')->total_stock ?? 0), 2),
'meter' => round((float) ($prices->get('meter')->total_stock ?? 0), 2),
'kilogram' => round((float) ($prices->get('kilogram')->total_stock ?? 0), 2),
],
];
});
}
public function getProductStock(): array
{
return $this->cacheRemember('analysis:get_product_stock', 900, function () {
$variants = ProductVariant::query()
->join('products', 'product_variants.product_id', '=', 'products.id')
->selectRaw('
SUM(product_variants.stock) as total_stock,
SUM(product_variants.reject_stock) as total_reject,
SUM(product_variants.retail_stock) as total_retail,
COUNT(product_variants.id) as total_variants,
COUNT(DISTINCT products.id) as total_products
')
->first();
$totalValue = \DB::table('product_prices')
->join('product_variants', 'product_prices.variant_id', '=', 'product_variants.id')
->where('product_prices.type', PriceType::HARGA_MODAL->value)
->whereNull('product_variants.deleted_at')
->whereNull('product_prices.deleted_at')
->selectRaw('SUM(product_variants.stock * product_prices.price) as total')
->value('total');
$totalCategories = \DB::table('product_categories')
->distinct('category_id')
->count('category_id');
return [
'total_stock' => (int) ($variants->total_stock ?? 0),
'total_reject' => (int) ($variants->total_reject ?? 0),
'total_retail' => (int) ($variants->total_retail ?? 0),
'total_value' => (int) ($totalValue ?? 0),
'total_products' => (int) ($variants->total_products ?? 0),
'total_variants' => (int) ($variants->total_variants ?? 0),
'total_categories' => (int) $totalCategories,
];
});
}
public function getBusyHours(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_busy_hours', 900, function () use ($startDate, $endDate) {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
$isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false;
$hourlyData = Order::query()
->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id))
->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id))
->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate]))
->selectRaw('HOUR(orders.created_at) as hour, COUNT(*) as order_count')
->groupBy('hour')
->orderBy('hour')
->get()
->keyBy('hour');
$result = [];
for ($h = 0; $h < 24; $h++) {
$result[] = [
'hour' => str_pad($h, 2, '0', STR_PAD_LEFT).':00',
'orders' => (int) ($hourlyData->get($h)->order_count ?? 0),
];
}
return $result;
});
}
public function getTopProducts(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_top_products', 900, function () use ($startDate, $endDate) {
/** @var User|null $user */
$user = auth()->user();
$isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false;
$isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false;
return OrderItem::query()
->join('orders', 'order_items.order_id', '=', 'orders.id')
->join('product_variants', 'order_items.product_variant_id', '=', 'product_variants.id')
->join('products', 'product_variants.product_id', '=', 'products.id')
->where('orders.status', OrderStatus::COMPLETED)
->when(! $isSuper && $isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id))
->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('orders.created_by_id', $user->id))
->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate]))
->selectRaw("CONCAT(products.name, ' - ', product_variants.name) as full_name, SUM(order_items.quantity) as total_qty, SUM(order_items.subtotal) as total_revenue")
->groupBy('order_items.product_variant_id', 'products.name', 'product_variants.name')
->orderByDesc('total_qty')
->limit(5)
->get()
->map(fn ($item) => [
'name' => $item->full_name,
'total_qty' => (int) $item->total_qty,
'total_revenue' => (int) $item->total_revenue,
])
->toArray();
});
}
public function getMarketingSales(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
return $this->cacheRemember('analysis:get_marketing_sales', 900, function () use ($startDate, $endDate) {
$qtySubquery = DB::table('order_items')
->select('order_id', DB::raw('SUM(quantity) as total_qty'))
->whereNull('deleted_at')
->groupBy('order_id');
return Order::query()
->completed()
->whereNotNull('marketing_id')
->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate]))
->join('users', 'orders.marketing_id', '=', 'users.id')
->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id')
->leftJoinSub($qtySubquery, 'order_qtys', function ($join) {
$join->on('orders.id', '=', 'order_qtys.order_id');
})
->selectRaw('
users.id as marketing_id,
COALESCE(user_profiles.full_name, users.username) as marketing_name,
COUNT(orders.id) as total_orders,
SUM(orders.total_amount) as total_revenue,
SUM(orders.subtotal) as total_subtotal,
SUM(orders.discount) as total_discount,
AVG(orders.total_amount) as avg_order,
SUM(COALESCE(order_qtys.total_qty, 0)) as total_products_sold
')
->groupBy('users.id', 'user_profiles.full_name', 'users.username')
->orderByDesc('total_revenue')
->get()
->map(fn ($item) => [
'marketing_name' => $item->marketing_name,
'total_orders' => (int) $item->total_orders,
'total_products_sold' => (int) $item->total_products_sold,
'total_revenue' => (int) $item->total_revenue,
'total_subtotal' => (int) $item->total_subtotal,
'total_discount' => (int) $item->total_discount,
'avg_order' => (int) $item->avg_order,
])
->toArray();
});
}
public function isManager(?User $user): bool
{
return $user?->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]) ?? false;
}
}