dstpabuaran.com/app/Services/AnalysisService.php

1095 lines
43 KiB
PHP

<?php
namespace App\Services;
use App\Enums\CashTransactionType;
use App\Enums\EmployeeAdvanceStatus;
use App\Enums\OrderChannel;
use App\Enums\OrderStatus;
use App\Enums\PaymentType;
use App\Enums\PayrollStatus;
use App\Enums\PriceType;
use App\Enums\RawMaterialUnit;
use App\Enums\Role;
use App\Models\Attendance;
use App\Models\CashAccount;
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\Payroll;
use App\Models\ProductVariant;
use App\Models\Purchase;
use App\Models\RawMaterialPrice;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class AnalysisService
{
public function getAttendanceStats(?string $startDate, ?string $endDate, ?User $user = null): array
{
$start = $startDate ? Carbon::parse($startDate) : Carbon::now()->startOfMonth();
$end = $endDate ? Carbon::parse($endDate) : Carbon::now();
if ($user && $this->isMarketingUser($user)) {
$employee = $user->employee;
if (! $employee) {
return [
'total_employees' => 0,
'present' => 0,
'absent' => 0,
'on_leave' => 0,
'percentage' => 0,
];
}
$isWithinRange = $employee->join_date <= $end
&& (is_null($employee->resign_date) || $employee->resign_date >= $start);
if (! $isWithinRange) {
return [
'total_employees' => 0,
'present' => 0,
'absent' => 0,
'on_leave' => 0,
'percentage' => 0,
];
}
$present = Attendance::where('employee_id', $employee->id)
->whereBetween('attendance_date', [$start->toDateString(), $end->toDateString()])
->distinct('employee_id')
->count('employee_id');
$onLeave = LeaveRequest::approved()
->where('start_date', '<=', $end)
->where('end_date', '>=', $start)
->where('employee_id', $employee->id)
->count();
$workingDays = $this->countWorkingDays($start, $end);
$absent = max(0, $workingDays - $present - $onLeave);
return [
'total_employees' => 1,
'present' => $present,
'absent' => $absent,
'on_leave' => $onLeave,
'percentage' => $workingDays > 0 ? round(($present / $workingDays) * 100) : 0,
];
}
$employees = Employee::whereHas(
'user',
fn ($q) => $q
->where('is_active', true)
->whereHas(
'roles',
fn ($r) => $r
->whereHas(
'permissions',
fn ($p) => $p
->where('name', 'attendances.create')
)
)
)
->where('join_date', '<=', $end)
->where(function ($q) use ($start) {
$q->whereNull('resign_date')->orWhere('resign_date', '>=', $start);
})
->get();
$employeeIds = $employees->pluck('id');
$employeeCount = $employeeIds->count();
$present = Attendance::whereIn('employee_id', $employeeIds)
->whereBetween('attendance_date', [$start->toDateString(), $end->toDateString()])
->distinct('employee_id')
->count('employee_id');
$onLeave = LeaveRequest::approved()
->where('start_date', '<=', $end)
->where('end_date', '>=', $start)
->whereIn('employee_id', $employeeIds)
->count();
$absent = max(0, $employeeCount - $present - $onLeave);
return [
'total_employees' => $employeeCount,
'present' => $present,
'absent' => $absent,
'on_leave' => $onLeave,
'percentage' => $employeeCount > 0 ? round(($present / $employeeCount) * 100) : 0,
];
}
public function getMyAttendance(User $user, ?string $startDate, ?string $endDate): ?array
{
$employee = $user->employee;
if (! $employee) {
return null;
}
$start = $startDate ? Carbon::parse($startDate) : Carbon::now()->startOfMonth();
$end = $endDate ? Carbon::parse($endDate) : Carbon::now();
$workingDays = 0;
$current = $start->copy();
while ($current->lte($end)) {
if ($current->dayOfWeek !== Carbon::SUNDAY) {
$workingDays++;
}
$current->addDay();
}
$present = Attendance::where('employee_id', $employee->id)
->whereBetween('attendance_date', [$start, $end])
->count();
$leaveDays = LeaveRequest::approved()
->where('employee_id', $employee->id)
->where('start_date', '<=', $end)
->where('end_date', '>=', $start)
->get()
->reduce(function ($carry, $leave) use ($start, $end) {
$leaveStart = max($leave->start_date->timestamp, $start->timestamp);
$leaveEnd = min($leave->end_date->timestamp, $end->timestamp);
$days = Carbon::createFromTimestamp($leaveStart)->diffInDays(Carbon::createFromTimestamp($leaveEnd)) + 1;
return $carry + max(0, $days);
}, 0);
$absent = max(0, $workingDays - $present - $leaveDays);
return [
'total_days' => $workingDays,
'present_days' => $present,
'absent_days' => $absent,
'leave_days' => $leaveDays,
'percentage' => $workingDays > 0 ? round(($present / $workingDays) * 100) : 0,
];
}
public function getCashOverview(?string $startDate, ?string $endDate): array
{
$cashAccount = CashAccount::first();
if (! $cashAccount) {
return [
'total_balance' => 0,
'total_transactions' => 0,
'total_deposit' => 0,
'total_withdrawal' => 0,
];
}
$transactions = $cashAccount->cashTransactions();
$this->applyDateFilter($transactions, $startDate, $endDate, 'cash_transactions.created_at');
$totalDeposit = (clone $transactions)->where('type', CashTransactionType::DEPOSIT)->sum('amount');
$totalWithdrawal = (clone $transactions)->where('type', CashTransactionType::WITHDRAWAL)->sum('amount');
return [
'total_balance' => (int) $cashAccount->balance,
'total_transactions' => (clone $transactions)->count(),
'total_deposit' => (int) $totalDeposit,
'total_withdrawal' => (int) $totalWithdrawal,
];
}
public function getRawMaterialStock(): array
{
$items = RawMaterialPrice::query()
->join('raw_materials', 'raw_material_prices.raw_material_id', '=', 'raw_materials.id')
->select('raw_material_prices.stock', 'raw_material_prices.price', 'raw_materials.unit')
->get();
$totalQty = $items->sum('stock');
$totalPrice = $items->sum(fn ($i) => $i->stock * $i->price);
$byUnit = [
'yard' => $items->filter(fn ($i) => $i->unit === RawMaterialUnit::YARD->value)->sum('stock'),
'meter' => $items->filter(fn ($i) => $i->unit === RawMaterialUnit::METER->value)->sum('stock'),
'kilogram' => $items->filter(fn ($i) => $i->unit === RawMaterialUnit::KG->value)->sum('stock'),
];
return [
'total_stock' => $totalQty,
'total_price' => $totalPrice,
'by_unit' => $byUnit,
];
}
public function getProductStock(): array
{
$variants = ProductVariant::query()
->leftJoin('product_prices', function ($join) {
$join->on('product_variants.id', '=', 'product_prices.variant_id')
->where('product_prices.type', '=', PriceType::RETAIL->value);
})
->select('product_variants.stock', 'product_variants.reject_stock', 'product_variants.retail_stock', 'product_prices.price')
->get();
$totalStock = $variants->sum('stock');
$totalRejectStock = $variants->sum('reject_stock');
$totalRetailStock = $variants->sum('retail_stock');
$totalPrice = $variants->sum(fn ($v) => ($v->stock + $v->reject_stock + $v->retail_stock) * ($v->price ?? 0));
return [
'total_stock' => $totalStock + $totalRejectStock + $totalRetailStock,
'total_price' => $totalPrice,
'by_type' => [
'stock' => $totalStock,
'reject_stock' => $totalRejectStock,
'retail_stock' => $totalRetailStock,
],
];
}
public function getRevenueByStockType(?string $startDate, ?string $endDate, ?User $user = null): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED);
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
if ($user && $this->isMarketingUser($user)) {
$query->where('orders.marketing_id', $user->id);
}
if ($user && $this->isCashierUser($user)) {
$query->whereIn('orders.created_by_id', $this->getCashierUserIds());
}
$stockQualitySubquery = OrderItem::select('order_id')
->selectRaw('MIN(stock_quality) as stock_quality')
->groupBy('order_id');
$monthly = (clone $query)
->toBase()
->joinSub($stockQualitySubquery, 'oi', fn ($join) => $join->on('orders.id', '=', 'oi.order_id'))
->selectRaw("DATE_FORMAT(orders.created_at, '%b %Y') as month")
->selectRaw('oi.stock_quality')
->selectRaw('COALESCE(SUM(orders.total_amount), 0) as total_revenue')
->groupBy(DB::raw("DATE_FORMAT(orders.created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(orders.created_at, '%b %Y')"), 'oi.stock_quality')
->orderBy(DB::raw("DATE_FORMAT(orders.created_at, '%Y-%m')"))
->get();
$allMonths = [];
$monthlyData = [];
foreach ($monthly as $row) {
$month = $row->month;
if (! array_key_exists($month, $allMonths)) {
$allMonths[$month] = $month;
$monthlyData[$month] = ['month' => $month, 'good' => 0, 'reject' => 0, 'retail' => 0];
}
$monthlyData[$month][$row->stock_quality] = (int) $row->total_revenue;
}
$result = [];
foreach ($allMonths as $month => $_) {
$result[] = $monthlyData[$month];
}
$totals = (clone $query)
->toBase()
->joinSub($stockQualitySubquery, 'oi', fn ($join) => $join->on('orders.id', '=', 'oi.order_id'))
->selectRaw('oi.stock_quality')
->selectRaw('COALESCE(SUM(orders.total_amount), 0) as total_revenue')
->groupBy('oi.stock_quality')
->get()
->keyBy('stock_quality');
return [
'monthly' => $result,
'totals' => [
'good' => (int) ($totals['good']->total_revenue ?? 0),
'reject' => (int) ($totals['reject']->total_revenue ?? 0),
'retail' => (int) ($totals['retail']->total_revenue ?? 0),
],
];
}
public function getRevenueSummary(?string $startDate, ?string $endDate, ?User $user = null): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED);
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
if ($user && $this->isMarketingUser($user)) {
$query->where('orders.marketing_id', $user->id);
}
if ($user && $this->isCashierUser($user)) {
$query->whereIn('orders.created_by_id', $this->getCashierUserIds());
}
$stats = (clone $query)
->selectRaw('COUNT(*) as total_orders')
->selectRaw('COALESCE(SUM(total_amount), 0) as total_revenue')
->selectRaw('COALESCE(SUM(discount), 0) as total_discount')
->selectRaw('COALESCE(SUM(nego_price), 0) as total_deduction')
->selectRaw('COALESCE(SUM(cogs), 0) as total_cogs')
->first();
$payrollQuery = Payroll::where('status', PayrollStatus::PAID);
$this->applyDateFilter($payrollQuery, $startDate, $endDate, 'paid_at');
$payrollTotal = (int) $payrollQuery->sum('total_amount');
$expenseQuery = Expense::query();
$this->applyDateFilter($expenseQuery, $startDate, $endDate, 'expenses.created_at');
$expenseTotal = (int) $expenseQuery->sum('amount');
$totalRevenue = (int) $stats->total_revenue;
$totalCogs = (int) $stats->total_cogs;
$gross = $totalRevenue - $totalCogs;
$net = $gross - $payrollTotal - $expenseTotal;
return [
'total_revenue' => $totalRevenue,
'total_discount' => (int) $stats->total_discount,
'total_deduction' => (int) $stats->total_deduction,
'cogs' => $totalCogs,
'gross' => $gross,
'net' => $net,
'payroll_total' => $payrollTotal,
'expense_total' => $expenseTotal,
'total_orders' => (int) $stats->total_orders,
'avg_order' => $stats->total_orders > 0 ? (int) ($stats->total_revenue / $stats->total_orders) : 0,
];
}
public function getMonthlyRevenue(?string $startDate, ?string $endDate, ?User $user = null): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED);
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
if ($user && $this->isMarketingUser($user)) {
$query->where('orders.marketing_id', $user->id);
}
if ($user && $this->isCashierUser($user)) {
$query->whereIn('orders.created_by_id', $this->getCashierUserIds());
}
$monthly = (clone $query)
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
->selectRaw('COALESCE(SUM(total_amount), 0) as total')
->selectRaw('COALESCE(SUM(discount), 0) as discount')
->selectRaw('COALESCE(SUM(nego_price), 0) as deduction')
->selectRaw('COALESCE(SUM(cogs), 0) as cogs')
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
->orderBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"))
->get()
->keyBy('month');
$payrollMonthly = Payroll::where('status', PayrollStatus::PAID);
$this->applyDateFilter($payrollMonthly, $startDate, $endDate, 'paid_at');
$payrollByMonth = (clone $payrollMonthly)->toBase()
->selectRaw("DATE_FORMAT(paid_at, '%b %Y') as month")
->selectRaw('COALESCE(SUM(total_amount), 0) as payroll')
->groupBy(DB::raw("DATE_FORMAT(paid_at, '%Y-%m')"), DB::raw("DATE_FORMAT(paid_at, '%b %Y')"))
->get()
->keyBy('month');
$expenseMonthly = Expense::query();
$this->applyDateFilter($expenseMonthly, $startDate, $endDate, 'expenses.created_at');
$expenseByMonth = (clone $expenseMonthly)->toBase()
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
->selectRaw('COALESCE(SUM(amount), 0) as expense')
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
->get()
->keyBy('month');
$allMonths = [];
foreach ([$monthly, $payrollByMonth, $expenseByMonth] as $data) {
foreach ($data as $month => $row) {
if (! array_key_exists($month, $allMonths)) {
$allMonths[$month] = $month;
}
}
}
$result = [];
foreach ($allMonths as $month => $_) {
$total = (int) ($monthly[$month]->total ?? 0);
$discount = (int) ($monthly[$month]->discount ?? 0);
$deduction = (int) ($monthly[$month]->deduction ?? 0);
$cogs = (int) ($monthly[$month]->cogs ?? 0);
$payroll = (int) ($payrollByMonth[$month]->payroll ?? 0);
$expense = (int) ($expenseByMonth[$month]->expense ?? 0);
$gross = $total - $cogs;
$net = $gross - $payroll - $expense;
$result[] = [
'month' => $month,
'total' => $total,
'gross' => $gross,
'net' => $net,
'discount' => $discount,
'deduction' => $deduction,
'cogs' => $cogs,
'payroll' => $payroll,
'expense' => $expense,
];
}
return $result;
}
public function getMonthlyRetailRevenue(?string $startDate, ?string $endDate, ?User $user = null): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED);
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
if ($user && $this->isMarketingUser($user)) {
$query->where('orders.marketing_id', $user->id);
}
if ($user && $this->isCashierUser($user)) {
$query->whereIn('orders.created_by_id', $this->getCashierUserIds());
}
$retailOrderIds = OrderItem::where('stock_quality', 'retail')
->pluck('order_id')
->unique();
$query->whereIn('orders.id', $retailOrderIds);
$monthly = (clone $query)
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
->selectRaw('COALESCE(SUM(total_amount), 0) as total')
->selectRaw('COALESCE(SUM(discount), 0) as discount')
->selectRaw('COALESCE(SUM(nego_price), 0) as deduction')
->selectRaw('COALESCE(SUM(cogs), 0) as cogs')
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
->orderBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"))
->get()
->keyBy('month');
$summary = (clone $query)
->selectRaw('COALESCE(SUM(total_amount), 0) as total')
->selectRaw('COALESCE(SUM(discount), 0) as discount')
->selectRaw('COALESCE(SUM(nego_price), 0) as deduction')
->selectRaw('COALESCE(SUM(cogs), 0) as cogs')
->first();
$gross = (int) $summary->total - (int) $summary->cogs;
$result = [];
foreach ($monthly as $month => $row) {
$total = (int) $row->total;
$discount = (int) $row->discount;
$deduction = (int) $row->deduction;
$cogs = (int) $row->cogs;
$itemGross = $total - $cogs;
$result[] = [
'month' => $month,
'total' => $total,
'gross' => $itemGross,
'discount' => $discount,
'deduction' => $deduction,
'cogs' => $cogs,
];
}
return [
'monthly' => $result,
'summary' => [
'total' => (int) $summary->total,
'discount' => (int) $summary->discount,
'deduction' => (int) $summary->deduction,
'cogs' => (int) $summary->cogs,
'gross' => $gross,
],
];
}
public function getMonthlyRevenueByChannel(?string $startDate, ?string $endDate, ?User $user = null): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED);
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
if ($user && $this->isMarketingUser($user)) {
$query->where('orders.marketing_id', $user->id);
}
if ($user && $this->isCashierUser($user)) {
$query->whereIn('orders.created_by_id', $this->getCashierUserIds());
}
$monthly = (clone $query)
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
->selectRaw("COALESCE(SUM(CASE WHEN channel = 'store' THEN total_amount ELSE 0 END), 0) as store")
->selectRaw("COALESCE(SUM(CASE WHEN channel = 'shopee' THEN total_amount ELSE 0 END), 0) as shopee")
->selectRaw("COALESCE(SUM(CASE WHEN channel = 'tiktok' THEN total_amount ELSE 0 END), 0) as tiktok")
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
->orderBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"))
->get()
->map(fn ($item) => [
'month' => $item->month,
'store' => (int) $item->store,
'shopee' => (int) $item->shopee,
'tiktok' => (int) $item->tiktok,
]);
return $monthly->values()->toArray();
}
public function getRevenueByPaymentType(?string $startDate, ?string $endDate, ?User $user = null): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED);
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
if ($user && $this->isMarketingUser($user)) {
$query->where('orders.marketing_id', $user->id);
}
if ($user && $this->isCashierUser($user)) {
$query->whereIn('orders.created_by_id', $this->getCashierUserIds());
}
$data = (clone $query)
->select('payment_type')
->selectRaw('COALESCE(SUM(total_amount), 0) as total')
->groupBy('payment_type')
->get()
->map(fn ($item) => [
'payment_type' => $item->payment_type,
'label' => $item->payment_type->label(),
'total' => (int) $item->total,
]);
return $data->toArray();
}
public function getExpenseSummary(?string $startDate, ?string $endDate, ?User $user = null): array
{
if ($user && $this->isMarketingUser($user)) {
$employee = $user->employee;
$expenseQuery = Expense::where('created_by_id', $user->id);
$this->applyDateFilter($expenseQuery, $startDate, $endDate, 'expenses.created_at');
$advanceQuery = EmployeeAdvance::where('status', EmployeeAdvanceStatus::REPAID);
if ($employee) {
$advanceQuery->where('employee_id', $employee->id);
} else {
$advanceQuery->whereRaw('0 = 1');
}
$this->applyDateFilter($advanceQuery, $startDate, $endDate, 'employee_advances.created_at');
$expenseTotal = (clone $expenseQuery)->sum('amount');
$advanceTotal = (clone $advanceQuery)->sum('amount');
return [
'total' => (int) ($expenseTotal + $advanceTotal),
'purchase_total' => 0,
'expense_total' => (int) $expenseTotal,
'advance_total' => (int) $advanceTotal,
];
}
$expenseQuery = Expense::query();
$this->applyDateFilter($expenseQuery, $startDate, $endDate, 'expenses.created_at');
if ($user && $this->isCashierUser($user)) {
$expenseQuery->whereIn('created_by_id', $this->getCashierUserIds());
}
$advanceQuery = EmployeeAdvance::where('status', EmployeeAdvanceStatus::REPAID);
$this->applyDateFilter($advanceQuery, $startDate, $endDate, 'employee_advances.created_at');
$expenseTotal = (clone $expenseQuery)->sum('amount');
$advanceTotal = (clone $advanceQuery)->sum('amount');
$includePurchase = $user && $this->isPurchaseVisible($user);
if ($includePurchase) {
$purchaseQuery = Purchase::query();
$this->applyDateFilter($purchaseQuery, $startDate, $endDate, 'purchases.created_at');
$purchaseTotal = (clone $purchaseQuery)->sum('total');
} else {
$purchaseTotal = 0;
}
return [
'total' => (int) ($expenseTotal + $advanceTotal + $purchaseTotal),
'purchase_total' => (int) $purchaseTotal,
'expense_total' => (int) $expenseTotal,
'advance_total' => (int) $advanceTotal,
];
}
public function getMonthlyExpense(?string $startDate, ?string $endDate, ?User $user = null): array
{
if ($user && $this->isMarketingUser($user)) {
$employee = $user->employee;
$expenseMonthly = Expense::where('created_by_id', $user->id);
$this->applyDateFilter($expenseMonthly, $startDate, $endDate, 'expenses.created_at');
$expenseByMonth = (clone $expenseMonthly)->toBase()
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
->selectRaw('COALESCE(SUM(amount), 0) as expense')
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
->get()
->keyBy('month');
$advanceMonthly = EmployeeAdvance::where('status', EmployeeAdvanceStatus::REPAID);
if ($employee) {
$advanceMonthly->where('employee_id', $employee->id);
} else {
$advanceMonthly->whereRaw('0 = 1');
}
$this->applyDateFilter($advanceMonthly, $startDate, $endDate, 'employee_advances.created_at');
$advanceByMonth = (clone $advanceMonthly)->toBase()
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
->selectRaw('COALESCE(SUM(amount), 0) as advance')
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
->get()
->keyBy('month');
$allMonths = [];
foreach ([$expenseByMonth, $advanceByMonth] as $data) {
foreach ($data as $month => $row) {
if (! array_key_exists($month, $allMonths)) {
$allMonths[$month] = ['month' => $month, 'total' => 0, 'purchase' => 0, 'expense' => 0, 'advance' => 0];
}
}
}
foreach ($allMonths as $month => &$row) {
$row['expense'] = (int) ($expenseByMonth[$month]->expense ?? 0);
$row['advance'] = (int) ($advanceByMonth[$month]->advance ?? 0);
$row['total'] = $row['expense'] + $row['advance'];
}
return array_values($allMonths);
}
$expenseMonthly = Expense::query();
$this->applyDateFilter($expenseMonthly, $startDate, $endDate, 'expenses.created_at');
if ($user && $this->isCashierUser($user)) {
$expenseMonthly->whereIn('created_by_id', $this->getCashierUserIds());
}
$expenseByMonth = (clone $expenseMonthly)->toBase()
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
->selectRaw('COALESCE(SUM(amount), 0) as expense')
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
->get()
->keyBy('month');
$advanceMonthly = EmployeeAdvance::where('status', EmployeeAdvanceStatus::REPAID);
$this->applyDateFilter($advanceMonthly, $startDate, $endDate, 'employee_advances.created_at');
$advanceByMonth = (clone $advanceMonthly)->toBase()
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
->selectRaw('COALESCE(SUM(amount), 0) as advance')
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
->get()
->keyBy('month');
$includePurchase = $user && $this->isPurchaseVisible($user);
if ($includePurchase) {
$purchaseMonthly = Purchase::query();
$this->applyDateFilter($purchaseMonthly, $startDate, $endDate, 'purchases.created_at');
$purchaseByMonth = (clone $purchaseMonthly)->toBase()
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
->selectRaw('COALESCE(SUM(total), 0) as purchase')
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
->get()
->keyBy('month');
} else {
$purchaseByMonth = collect();
}
$allMonths = [];
foreach ([$expenseByMonth, $purchaseByMonth, $advanceByMonth] as $data) {
foreach ($data as $month => $row) {
if (! array_key_exists($month, $allMonths)) {
$allMonths[$month] = ['month' => $month, 'total' => 0, 'purchase' => 0, 'expense' => 0, 'advance' => 0];
}
}
}
foreach ($allMonths as $month => &$row) {
$row['purchase'] = (int) ($purchaseByMonth[$month]->purchase ?? 0);
$row['expense'] = (int) ($expenseByMonth[$month]->expense ?? 0);
$row['advance'] = (int) ($advanceByMonth[$month]->advance ?? 0);
$row['total'] = $row['expense'] + $row['advance'] + $row['purchase'];
}
return array_values($allMonths);
}
public function getBusyHours(?string $startDate, ?string $endDate, ?User $user = null): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED);
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
if ($user && $this->isMarketingUser($user)) {
$query->where('orders.marketing_id', $user->id);
}
if ($user && $this->isCashierUser($user)) {
$query->whereIn('orders.created_by_id', $this->getCashierUserIds());
}
$hours = range(0, 23);
$hourCounts = (clone $query)
->selectRaw('HOUR(created_at) as hour')
->selectRaw('COUNT(*) as orders')
->groupBy(DB::raw('HOUR(created_at)'))
->pluck('orders', 'hour')
->toArray();
return array_map(function ($h) use ($hourCounts) {
return [
'hour' => sprintf('%02d:00', $h),
'orders' => (int) ($hourCounts[$h] ?? 0),
];
}, $hours);
}
public function getProfitMetrics(?string $startDate, ?string $endDate, ?User $user = null): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED);
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
if ($user && $this->isMarketingUser($user)) {
$query->where('orders.marketing_id', $user->id);
}
if ($user && $this->isCashierUser($user)) {
$query->whereIn('orders.created_by_id', $this->getCashierUserIds());
}
$stats = (clone $query)
->selectRaw('COUNT(*) as total_orders')
->selectRaw('COALESCE(SUM(total_amount), 0) as total_revenue')
->selectRaw('COALESCE(SUM(cogs), 0) as hpp')
->first();
$totalProductsSold = (clone $query)
->join('order_items', 'orders.id', '=', 'order_items.order_id')
->sum('order_items.quantity');
$payrollQuery = Payroll::where('status', PayrollStatus::PAID);
$this->applyDateFilter($payrollQuery, $startDate, $endDate, 'paid_at');
$payrollTotal = (int) $payrollQuery->sum('total_amount');
$expenseQuery = Expense::query();
$this->applyDateFilter($expenseQuery, $startDate, $endDate, 'expenses.created_at');
$expenseTotal = (int) $expenseQuery->sum('amount');
$grossProfit = $stats->total_revenue - $stats->hpp;
$netProfit = $grossProfit - $payrollTotal - $expenseTotal;
$profitMargin = $stats->total_revenue > 0 ? round(($netProfit / $stats->total_revenue) * 100, 1) : 0;
$aov = $stats->total_orders > 0 ? (int) ($stats->total_revenue / $stats->total_orders) : 0;
$itemsPerTransaction = $stats->total_orders > 0 ? round($totalProductsSold / $stats->total_orders, 1) : 0;
return [
'total_orders' => (int) $stats->total_orders,
'total_products_sold' => (int) $totalProductsSold,
'hpp' => (int) $stats->hpp,
'gross_profit' => (int) $grossProfit,
'net_profit' => (int) $netProfit,
'profit_margin' => $profitMargin,
'aov' => $aov,
'items_per_transaction' => $itemsPerTransaction,
'payroll_total' => $payrollTotal,
'expense_total' => $expenseTotal,
];
}
public function getTopSuppliers(?string $startDate, ?string $endDate): array
{
$query = Purchase::query();
$this->applyDateFilter($query, $startDate, $endDate, 'purchases.created_at');
return (clone $query)->toBase()
->join('suppliers', 'purchases.supplier_id', '=', 'suppliers.id')
->select('suppliers.name')
->selectRaw('COALESCE(SUM(purchases.total), 0) as total_amount')
->selectRaw('COUNT(*) as purchase_count')
->groupBy('suppliers.name')
->orderByDesc('total_amount')
->limit(5)
->get()
->toArray();
}
public function getTopCustomers(?string $startDate, ?string $endDate, ?User $user = null): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED)->whereNotNull('orders.customer_id');
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
if ($user && $this->isMarketingUser($user)) {
$query->where('orders.marketing_id', $user->id);
}
if ($user && $this->isCashierUser($user)) {
$query->whereIn('orders.created_by_id', $this->getCashierUserIds());
}
return (clone $query)->toBase()
->join('customers', 'orders.customer_id', '=', 'customers.id')
->select('customers.name')
->selectRaw('COALESCE(SUM(orders.total_amount), 0) as total_amount')
->selectRaw('COUNT(*) as order_count')
->groupBy('customers.name')
->orderByDesc('total_amount')
->limit(5)
->get()
->toArray();
}
public function getTopProducts(?string $startDate, ?string $endDate, ?User $user = null): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED);
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
if ($user && $this->isMarketingUser($user)) {
$query->where('orders.marketing_id', $user->id);
}
if ($user && $this->isCashierUser($user)) {
$query->whereIn('orders.created_by_id', $this->getCashierUserIds());
}
return (clone $query)->toBase()
->join('order_items', 'orders.id', '=', 'order_items.order_id')
->join('product_variants', 'order_items.product_variant_id', '=', 'product_variants.id')
->join('products', 'product_variants.product_id', '=', 'products.id')
->select('products.name')
->selectRaw('SUM(order_items.quantity) as total_qty')
->selectRaw('COALESCE(SUM(order_items.subtotal), 0) as total_revenue')
->groupBy('products.name')
->orderByDesc('total_qty')
->limit(5)
->get()
->toArray();
}
public function getRevenueTrend(?string $startDate, ?string $endDate, ?User $user = null): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED);
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
if ($user && $this->isMarketingUser($user)) {
$query->where('orders.marketing_id', $user->id);
}
if ($user && $this->isCashierUser($user)) {
$query->whereIn('orders.created_by_id', $this->getCashierUserIds());
}
return (clone $query)
->join('order_items', 'orders.id', '=', 'order_items.order_id')
->selectRaw('DATE(orders.created_at) as date')
->selectRaw('SUM(order_items.quantity) as qty')
->groupBy(DB::raw('DATE(orders.created_at)'))
->orderBy(DB::raw('DATE(orders.created_at)'))
->get()
->map(fn ($item) => [
'date' => $item->date,
'qty' => (int) $item->qty,
])
->toArray();
}
public function getMarketingSales(?string $startDate, ?string $endDate, ?User $user = null): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED)
->whereNotNull('orders.marketing_id');
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
if ($user && $this->isMarketingUser($user)) {
$query->where('orders.marketing_id', $user->id);
}
if ($user && $this->isCashierUser($user)) {
$query->whereIn('orders.created_by_id', $this->getCashierUserIds());
}
$orders = (clone $query)
->join('users', 'orders.marketing_id', '=', 'users.id')
->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id')
->select('orders.marketing_id', 'user_profiles.full_name as marketing_name')
->selectRaw('COUNT(DISTINCT orders.id) as total_orders')
->selectRaw('COALESCE(SUM(orders.total_amount), 0) as total_revenue')
->selectRaw('COALESCE(SUM(orders.subtotal), 0) as total_subtotal')
->selectRaw('COALESCE(SUM(orders.discount), 0) as total_discount')
->selectRaw('COALESCE(SUM(orders.nego_price), 0) as total_nego_price')
->groupBy('orders.marketing_id', 'user_profiles.full_name')
->get();
$productCounts = (clone $query)
->join('order_items', 'orders.id', '=', 'order_items.order_id')
->selectRaw('orders.marketing_id, SUM(order_items.quantity) as total_qty')
->groupBy('orders.marketing_id')
->pluck('total_qty', 'marketing_id');
return $orders->map(function ($item) use ($productCounts) {
$totalOrders = (int) $item->total_orders;
$totalRevenue = (int) $item->total_revenue;
return [
'marketing_name' => $item->marketing_name,
'total_orders' => $totalOrders,
'total_products_sold' => (int) ($productCounts[$item->marketing_id] ?? 0),
'total_revenue' => $totalRevenue,
'total_subtotal' => (int) $item->total_subtotal,
'total_discount' => (int) $item->total_discount + (int) $item->total_nego_price,
'avg_order' => $totalOrders > 0 ? (int) ($totalRevenue / $totalOrders) : 0,
];
})->toArray();
}
public function getOrderStats(?string $startDate, ?string $endDate, ?User $user = null): array
{
$baseQuery = Order::query();
$this->applyDateFilter($baseQuery, $startDate, $endDate, 'orders.created_at');
if ($user && $this->isMarketingUser($user)) {
$baseQuery->where('orders.marketing_id', $user->id);
}
if ($user && $this->isCashierUser($user)) {
$baseQuery->whereIn('orders.created_by_id', $this->getCashierUserIds());
}
$byChannel = collect(OrderChannel::values())->map(function ($channel) use ($baseQuery) {
$count = (clone $baseQuery)->where('channel', $channel)->count();
$label = OrderChannel::from($channel)->label();
return [
'channel' => $channel,
'label' => $label,
'count' => $count,
'total' => (int) (clone $baseQuery)->where('channel', $channel)->sum('total_amount'),
];
});
$byPaymentType = collect(PaymentType::values())->map(function ($paymentType) use ($baseQuery) {
$count = (clone $baseQuery)->where('payment_type', $paymentType)->count();
$label = PaymentType::from($paymentType)->label();
return [
'payment_type' => $paymentType,
'label' => $label,
'count' => $count,
'total' => (int) (clone $baseQuery)->where('payment_type', $paymentType)->sum('total_amount'),
];
});
$byMarketing = (clone $baseQuery)
->whereNotNull('marketing_id')
->select('marketing_id')
->selectRaw('COUNT(*) as count')
->selectRaw('COALESCE(SUM(total_amount), 0) as total')
->groupBy('marketing_id')
->with('marketing:id')
->get()
->map(fn ($item) => [
'name' => $item->marketing?->userProfile->full_name ?? '-',
'count' => $item->count,
'total' => (int) $item->total,
]);
$byStatus = collect(OrderStatus::values())->map(function ($status) use ($baseQuery) {
$count = (clone $baseQuery)->where('status', $status)->count();
$label = OrderStatus::from($status)->label();
return [
'status' => $status,
'label' => $label,
'count' => $count,
];
});
return [
'by_channel' => $byChannel,
'by_payment_type' => $byPaymentType,
'by_marketing' => $byMarketing,
'by_status' => $byStatus,
];
}
private function applyDateFilter($query, ?string $startDate, ?string $endDate, string $dateColumn = 'created_at'): void
{
if ($startDate) {
$query->whereDate($dateColumn, '>=', $startDate);
}
if ($endDate) {
$query->whereDate($dateColumn, '<=', $endDate);
}
}
private function isMarketingUser(User $user): bool
{
return $user->hasAnyRole([
Role::MARKETING_OFFLINE->value,
Role::MARKETING_ONLINE->value,
]);
}
private function isCashierUser(User $user): bool
{
return $user->hasRole(Role::CASHIER->value);
}
private function getCashierUserIds(): Collection
{
return User::whereHas('roles', fn ($q) => $q->where('name', Role::CASHIER->value))->pluck('id');
}
private function isPurchaseVisible(User $user): bool
{
return $user->hasAnyRole([
Role::DEVELOPER->value,
Role::OWNER->value,
Role::ADMIN_BAHAN_BAKU->value,
]);
}
private function applyMarketingFilter(Builder $query, User $user, string $column = 'marketing_id'): Builder
{
if ($this->isMarketingUser($user)) {
$query->where($column, $user->id);
}
return $query;
}
private function countWorkingDays(Carbon $start, Carbon $end): int
{
$workingDays = 0;
$current = $start->copy();
while ($current->lte($end)) {
if ($current->dayOfWeek !== Carbon::SUNDAY) {
$workingDays++;
}
$current->addDay();
}
return $workingDays;
}
}