dstpabuaran.com/app/Services/AnalysisService.php
Yoga Pangestu 0023309a8f Refactor services to improve role checks and streamline data retrieval
- Updated CustomerService to simplify getAll method.
- Refactored ProductService to utilize HasRoleChecks trait and improved role verification logic.
- Enhanced ProductVariantService with new methods for fetching data for restocking and transactions.
- Cleaned up RawMaterialService by removing unused methods and improving data retrieval.
- Adjusted SupplierService to streamline getAll method.
- Refactored RoleService to use Spatie's Role model and improved role filtering logic.
- Updated NotificationService to handle role labels more effectively.
- Improved StockMutationService by removing redundant paginated method.
- Cleaned up various frontend components to directly accept necessary props instead of nested data objects.
- Updated tests to reflect changes in service method names and ensure proper notification handling.
2026-08-09 11:33:25 +07:00

485 lines
20 KiB
PHP

<?php
namespace App\Services;
use App\Enums\OrderStatus;
use App\Enums\PriceType;
use App\Enums\RawMaterialUnit;
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\ProductVariant;
use App\Models\Purchase;
use App\Models\RawMaterialPrice;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class AnalysisService
{
public function getAttendanceStats(?string $startDate, ?string $endDate): array
{
$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 (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) {
$workingDays++;
}
$current->addDay();
}
$totalEmployees = Employee::whereHas('user', fn ($q) => $q->where('is_active', true))->count();
$present = Attendance::whereBetween('attendance_date', [$start, $end])->count();
$leaveDays = LeaveRequest::approved()
->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_employees' => $totalEmployees,
'present' => $present,
'absent' => $absent,
'on_leave' => $leaveDays,
'percentage' => $workingDays > 0 ? round(($present / $workingDays) * 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 (! in_array($current->dayOfWeek, [Carbon::SATURDAY, 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(): array
{
$cashAccount = CashAccount::first();
if (! $cashAccount) {
return [
'total_balance' => 0,
'total_transactions' => 0,
'total_deposit' => 0,
'total_withdrawal' => 0,
];
}
$transactions = $cashAccount->cashTransactions();
return [
'total_balance' => $cashAccount->balance,
'total_transactions' => (clone $transactions)->count(),
'total_deposit' => (int) (clone $transactions)->where('type', 'deposit')->sum('amount'),
'total_withdrawal' => (int) (clone $transactions)->where('type', 'withdrawal')->sum('amount'),
];
}
public function getRawMaterialStock(): array
{
$prices = RawMaterialPrice::select('stock', 'price', 'raw_material_id')
->with('rawMaterial:id,unit')
->get();
$totalStock = $prices->sum('stock');
$totalValue = $prices->sum(fn ($p) => $p->stock * $p->price);
$byUnit = [
'yard' => $prices->filter(fn ($p) => $p->rawMaterial?->unit === RawMaterialUnit::YARD)->sum('stock'),
'meter' => $prices->filter(fn ($p) => $p->rawMaterial?->unit === RawMaterialUnit::METER)->sum('stock'),
'kilogram' => $prices->filter(fn ($p) => $p->rawMaterial?->unit === RawMaterialUnit::KG)->sum('stock'),
];
return [
'total_stock' => $totalStock,
'total_value' => $totalValue,
'by_unit' => $byUnit,
];
}
public function getProductStock(): array
{
$variants = ProductVariant::select('id', 'product_id', 'stock', 'reject_stock', 'retail_stock')
->with(['product:id,name', 'productPrices' => fn ($q) => $q->where('type', PriceType::CAPITAL)])
->get();
$totalStock = $variants->sum('stock');
$totalReject = $variants->sum('reject_stock');
$totalRetail = $variants->sum('retail_stock');
$totalValue = $variants->sum(function ($v) {
$capitalPrice = $v->productPrices->first()?->price ?? 0;
return $v->stock * $capitalPrice;
});
return [
'total_stock' => $totalStock,
'total_reject' => $totalReject,
'total_retail' => $totalRetail,
'total_value' => $totalValue,
'total_products' => ProductVariant::distinct('product_id')->count('product_id'),
'total_variants' => ProductVariant::count(),
'total_categories' => DB::table('product_categories')->distinct('category_id')->count('category_id'),
];
}
public function getRevenueSummary(?string $startDate, ?string $endDate): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED);
$this->applyDateFilter($query, $startDate, $endDate);
$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(discount), 0) as total_deduction')
->selectRaw('COALESCE(SUM(total_amount) - SUM(discount), 0) as net')
->selectRaw("COALESCE(SUM(CASE WHEN price_type != 'retail' THEN total_amount ELSE 0 END), 0) as net_warehouse")
->selectRaw("COALESCE(SUM(CASE WHEN price_type = 'retail' THEN total_amount ELSE 0 END), 0) as net_retail")
->first();
return [
'total_revenue' => (int) $stats->total_revenue,
'total_discount' => (int) $stats->total_discount,
'total_deduction' => (int) $stats->total_deduction,
'net' => (int) $stats->net,
'net_warehouse' => (int) $stats->net_warehouse,
'net_retail' => (int) $stats->net_retail,
'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): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED);
$this->applyDateFilter($query, $startDate, $endDate);
$monthly = (clone $query)
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
->selectRaw('COALESCE(SUM(total_amount), 0) as total')
->selectRaw('COALESCE(SUM(total_amount) - SUM(discount), 0) as net')
->selectRaw("COALESCE(SUM(CASE WHEN price_type != 'retail' THEN total_amount ELSE 0 END), 0) as net_warehouse")
->selectRaw("COALESCE(SUM(CASE WHEN price_type = 'retail' THEN total_amount ELSE 0 END), 0) as net_retail")
->selectRaw('COALESCE(SUM(discount), 0) as deduction')
->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) => $item->only(['month', 'total', 'net', 'net_warehouse', 'net_retail', 'deduction']));
return $monthly->values()->toArray();
}
public function getMonthlyRevenueByChannel(?string $startDate, ?string $endDate): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED);
$this->applyDateFilter($query, $startDate, $endDate);
$monthly = (clone $query)
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
->selectRaw("SUM(CASE WHEN channel = 'store' THEN total_amount ELSE 0 END) as store")
->selectRaw("SUM(CASE WHEN channel = 'shopee' THEN total_amount ELSE 0 END) as shopee")
->selectRaw("SUM(CASE WHEN channel = 'tiktok' THEN total_amount ELSE 0 END) 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) => $item->only(['month', 'store', 'shopee', 'tiktok']));
return $monthly->values()->toArray();
}
public function getRevenueByPaymentType(?string $startDate, ?string $endDate): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED);
$this->applyDateFilter($query, $startDate, $endDate);
$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): array
{
$expenseQuery = Expense::query();
$this->applyDateFilter($expenseQuery, $startDate, $endDate);
$advanceQuery = EmployeeAdvance::where('status', 'paid');
$this->applyDateFilter($advanceQuery, $startDate, $endDate);
$purchaseQuery = Purchase::query();
$this->applyDateFilter($purchaseQuery, $startDate, $endDate);
$expenseTotal = (clone $expenseQuery)->sum('amount');
$advanceTotal = (clone $advanceQuery)->sum('amount');
$purchaseTotal = (clone $purchaseQuery)->sum('total');
return [
'total' => $expenseTotal + $advanceTotal + $purchaseTotal,
'purchase_total' => $purchaseTotal,
'expense_total' => $expenseTotal,
'advance_total' => $advanceTotal,
];
}
public function getMonthlyExpense(?string $startDate, ?string $endDate): array
{
$expenseMonthly = Expense::query();
$this->applyDateFilter($expenseMonthly, $startDate, $endDate);
$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');
$purchaseMonthly = Purchase::query();
$this->applyDateFilter($purchaseMonthly, $startDate, $endDate);
$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');
$advanceMonthly = EmployeeAdvance::where('status', 'paid');
$this->applyDateFilter($advanceMonthly, $startDate, $endDate);
$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, $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['purchase'] + $row['expense'] + $row['advance'];
}
return array_values($allMonths);
}
public function getBusyHours(?string $startDate, ?string $endDate): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED);
$this->applyDateFilter($query, $startDate, $endDate);
$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): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED);
$this->applyDateFilter($query, $startDate, $endDate);
$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');
$grossProfit = $stats->total_revenue - $stats->hpp;
$netProfit = $grossProfit;
$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,
];
}
public function getTopSuppliers(?string $startDate, ?string $endDate): array
{
$query = Purchase::query();
$this->applyDateFilter($query, $startDate, $endDate);
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): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED)->whereNotNull('orders.customer_id');
$this->applyDateFilter($query, $startDate, $endDate);
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): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED);
$this->applyDateFilter($query, $startDate, $endDate);
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 getMarketingSales(?string $startDate, ?string $endDate): array
{
$query = Order::where('orders.status', OrderStatus::COMPLETED)
->whereNotNull('orders.marketing_id');
$this->applyDateFilter($query, $startDate, $endDate);
return (clone $query)->toBase()
->join('users', 'orders.marketing_id', '=', 'users.id')
->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id')
->leftJoin('order_items', 'orders.id', '=', 'order_items.order_id')
->select('user_profiles.full_name as marketing_name')
->selectRaw('COUNT(DISTINCT orders.id) as total_orders')
->selectRaw('COALESCE(SUM(order_items.quantity), 0) as total_products_sold')
->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('ROUND(COALESCE(SUM(orders.total_amount), 0) / COUNT(DISTINCT orders.id)) as avg_order')
->groupBy('user_profiles.full_name')
->orderByDesc('total_revenue')
->get()
->toArray();
}
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);
}
}
}