feat: implement DashboardService and refactor active scope usage in services
This commit is contained in:
parent
1ffb913fe8
commit
3e5ffef2d8
@ -2,37 +2,18 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Enums\CuttingStatus;
|
||||
use App\Enums\EmployeeAdvanceStatus;
|
||||
use App\Enums\EmploymentStatus;
|
||||
use App\Enums\OrderChannel;
|
||||
use App\Enums\OrderStatus;
|
||||
use App\Enums\PaymentType;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Attendance;
|
||||
use App\Models\CashAccount;
|
||||
use App\Models\CashTransaction;
|
||||
use App\Models\Category;
|
||||
use App\Models\Cutting;
|
||||
use App\Models\CuttingResult;
|
||||
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\PayrollPeriod;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Models\Purchase;
|
||||
use App\Models\RawMaterialPrice;
|
||||
use App\Services\System\DashboardService;
|
||||
use Carbon\Carbon;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DashboardService $dashboardService,
|
||||
) {}
|
||||
|
||||
public function index(): Response
|
||||
{
|
||||
$now = Carbon::now();
|
||||
@ -40,576 +21,29 @@ public function index(): Response
|
||||
$endOfMonth = $now->copy()->endOfMonth();
|
||||
|
||||
return Inertia::render('admin/Dashboard', [
|
||||
'rawMaterialStock' => $this->getRawMaterialStock(),
|
||||
'productStock' => $this->getProductStock(),
|
||||
'lowStockProducts' => $this->getLowStockProducts(),
|
||||
'lowStockMaterials' => $this->getLowStockMaterials(),
|
||||
'topSuppliers' => $this->getTopSuppliers(),
|
||||
'topCustomers' => $this->getTopCustomers(),
|
||||
'topProducts' => $this->getTopProducts(),
|
||||
'purchaseSummary' => $this->getPurchaseSummary(),
|
||||
'cuttingSummary' => $this->getCuttingSummary(),
|
||||
'cuttingByStatus' => $this->getCuttingByStatus(),
|
||||
'orderStats' => $this->getOrderStats(),
|
||||
'revenueSummary' => $this->getRevenueSummary(),
|
||||
'marketplaceSummary' => $this->getMarketplaceSummary(),
|
||||
'cashAccounts' => $this->getCashAccounts(),
|
||||
'monthlyCashFlow' => $this->getMonthlyCashFlow(),
|
||||
'monthlyExpenses' => $this->getMonthlyExpenses($startOfMonth, $endOfMonth),
|
||||
'kasbonSummary' => $this->getKasbonSummary(),
|
||||
'payrollSummary' => $this->getPayrollSummary($startOfMonth, $endOfMonth),
|
||||
'leaveRequestSummary' => $this->getLeaveRequestSummary($startOfMonth, $endOfMonth),
|
||||
'employeeSummary' => $this->getEmployeeSummary(),
|
||||
'attendanceToday' => $this->getAttendanceToday(),
|
||||
'monthlyRevenueTrend' => $this->getMonthlyRevenueTrend(),
|
||||
'monthlyPurchaseTrend' => $this->getMonthlyPurchaseTrend(),
|
||||
'rawMaterialStock' => $this->dashboardService->getRawMaterialStock(),
|
||||
'productStock' => $this->dashboardService->getProductStock(),
|
||||
'lowStockProducts' => $this->dashboardService->getLowStockProducts(),
|
||||
'lowStockMaterials' => $this->dashboardService->getLowStockMaterials(),
|
||||
'topSuppliers' => $this->dashboardService->getTopSuppliers(),
|
||||
'topCustomers' => $this->dashboardService->getTopCustomers(),
|
||||
'topProducts' => $this->dashboardService->getTopProducts(),
|
||||
'purchaseSummary' => $this->dashboardService->getPurchaseSummary(),
|
||||
'cuttingSummary' => $this->dashboardService->getCuttingSummary(),
|
||||
'cuttingByStatus' => $this->dashboardService->getCuttingByStatus(),
|
||||
'orderStats' => $this->dashboardService->getOrderStats(),
|
||||
'revenueSummary' => $this->dashboardService->getRevenueSummary(),
|
||||
'marketplaceSummary' => $this->dashboardService->getMarketplaceSummary(),
|
||||
'cashAccounts' => $this->dashboardService->getCashAccounts(),
|
||||
'monthlyCashFlow' => $this->dashboardService->getMonthlyCashFlow(),
|
||||
'monthlyExpenses' => $this->dashboardService->getMonthlyExpenses($startOfMonth, $endOfMonth),
|
||||
'kasbonSummary' => $this->dashboardService->getKasbonSummary(),
|
||||
'payrollSummary' => $this->dashboardService->getPayrollSummary($startOfMonth, $endOfMonth),
|
||||
'leaveRequestSummary' => $this->dashboardService->getLeaveRequestSummary($startOfMonth, $endOfMonth),
|
||||
'employeeSummary' => $this->dashboardService->getEmployeeSummary(),
|
||||
'attendanceToday' => $this->dashboardService->getAttendanceToday(),
|
||||
'monthlyRevenueTrend' => $this->dashboardService->getMonthlyRevenueTrend(),
|
||||
'monthlyPurchaseTrend' => $this->dashboardService->getMonthlyPurchaseTrend(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function getRawMaterialStock(): array
|
||||
{
|
||||
$data = RawMaterialPrice::query()
|
||||
->selectRaw('SUM(stock) as total_stock, SUM(stock * price) as total_value')
|
||||
->first();
|
||||
|
||||
$byUnit = 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')
|
||||
->groupBy('raw_materials.unit')
|
||||
->get()
|
||||
->mapWithKeys(fn ($item) => [
|
||||
$item->unit => (float) $item->total_stock,
|
||||
])
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'total_stock' => (float) ($data->total_stock ?? 0),
|
||||
'total_value' => (int) ($data->total_value ?? 0),
|
||||
'by_unit' => $byUnit,
|
||||
];
|
||||
}
|
||||
|
||||
private function getProductStock(): array
|
||||
{
|
||||
$data = ProductVariant::query()
|
||||
->selectRaw('SUM(stock) as total_stock, SUM(reject_stock) as total_reject, COUNT(*) as total_variants')
|
||||
->first();
|
||||
|
||||
$totalStock = (int) ($data->total_stock ?? 0);
|
||||
|
||||
$totalValue = Cutting::query()
|
||||
->whereNotNull('cost_per_unit')
|
||||
->join('cutting_results', 'cuttings.id', '=', 'cutting_results.cutting_id')
|
||||
->selectRaw('SUM(cutting_results.warehouse_stock * cuttings.cost_per_unit) as total_value')
|
||||
->value('total_value');
|
||||
|
||||
$totalProducts = Product::query()->count();
|
||||
$totalCategories = Category::query()->count();
|
||||
|
||||
return [
|
||||
'total_stock' => $totalStock,
|
||||
'total_reject' => (int) ($data->total_reject ?? 0),
|
||||
'total_value' => (int) ($totalValue ?? 0),
|
||||
'total_variants' => (int) ($data->total_variants ?? 0),
|
||||
'total_products' => $totalProducts,
|
||||
'total_categories' => $totalCategories,
|
||||
];
|
||||
}
|
||||
|
||||
private function getLowStockProducts(): array
|
||||
{
|
||||
return ProductVariant::query()
|
||||
->where('stock', '<=', ProductVariant::minStock())
|
||||
->where('stock', '>', 0)
|
||||
->join('products', 'product_variants.product_id', '=', 'products.id')
|
||||
->selectRaw("CONCAT(products.name, ' - ', product_variants.name) as full_name, product_variants.stock")
|
||||
->orderBy('stock')
|
||||
->limit(5)
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'name' => $item->full_name,
|
||||
'stock' => (int) $item->stock,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
private function getLowStockMaterials(): array
|
||||
{
|
||||
return RawMaterialPrice::query()
|
||||
->where('stock', '<=', 5)
|
||||
->where('stock', '>', 0)
|
||||
->join('raw_materials', 'raw_material_prices.raw_material_id', '=', 'raw_materials.id')
|
||||
->selectRaw("CONCAT(raw_materials.name, ' - ', raw_material_prices.variant) as full_name, raw_material_prices.stock, raw_materials.unit")
|
||||
->orderBy('stock')
|
||||
->limit(5)
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'name' => $item->full_name,
|
||||
'stock' => (float) $item->stock,
|
||||
'unit' => $item->unit,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
private function getTopSuppliers(): array
|
||||
{
|
||||
return Purchase::query()
|
||||
->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();
|
||||
}
|
||||
|
||||
private function getTopCustomers(): array
|
||||
{
|
||||
return Order::query()
|
||||
->where('status', OrderStatus::COMPLETED)
|
||||
->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();
|
||||
}
|
||||
|
||||
private function getTopProducts(): array
|
||||
{
|
||||
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)
|
||||
->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();
|
||||
}
|
||||
|
||||
private function getPurchaseSummary(): array
|
||||
{
|
||||
$data = Purchase::query()
|
||||
->selectRaw('COUNT(*) as total_purchases, SUM(total) as total_spent, SUM(discount) as total_discount')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'total_purchases' => (int) ($data->total_purchases ?? 0),
|
||||
'total_spent' => (int) ($data->total_spent ?? 0),
|
||||
'total_discount' => (int) ($data->total_discount ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
private function getCuttingSummary(): array
|
||||
{
|
||||
$totalCost = Cutting::query()
|
||||
->selectRaw('COALESCE(SUM(total_material_cost), 0) + COALESCE(SUM(sewing_cost), 0) + COALESCE(SUM(other_cost), 0) as total_cost')
|
||||
->value('total_cost');
|
||||
|
||||
$totalResults = CuttingResult::query()
|
||||
->selectRaw('SUM(cutting_result) as total_cutting, SUM(warehouse_stock) as total_warehouse, SUM(cutting_reject) as total_reject')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'total_cost' => (int) ($totalCost ?? 0),
|
||||
'total_cutting' => (int) ($totalResults->total_cutting ?? 0),
|
||||
'total_warehouse' => (int) ($totalResults->total_warehouse ?? 0),
|
||||
'total_reject' => (int) ($totalResults->total_reject ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
private function getCuttingByStatus(): array
|
||||
{
|
||||
return Cutting::query()
|
||||
->selectRaw('status, COUNT(*) as count')
|
||||
->groupBy('status')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'status' => $item->status instanceof CuttingStatus ? $item->status->value : $item->status,
|
||||
'label' => $item->status instanceof CuttingStatus ? $item->status->label() : CuttingStatus::from($item->status)->label(),
|
||||
'count' => (int) $item->count,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
private function getOrderStats(): array
|
||||
{
|
||||
$byChannel = Order::query()
|
||||
->selectRaw('channel, COUNT(*) as count, SUM(total_amount) as total')
|
||||
->groupBy('channel')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'channel' => $item->channel instanceof OrderChannel ? $item->channel->value : $item->channel,
|
||||
'label' => $item->channel instanceof OrderChannel ? $item->channel->label() : OrderChannel::from($item->channel)->label(),
|
||||
'count' => (int) $item->count,
|
||||
'total' => (int) $item->total,
|
||||
]);
|
||||
|
||||
$byPaymentType = Order::query()
|
||||
->selectRaw('payment_type, COUNT(*) as count, SUM(total_amount) as total')
|
||||
->groupBy('payment_type')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'payment_type' => $item->payment_type instanceof PaymentType ? $item->payment_type->value : $item->payment_type,
|
||||
'label' => $item->payment_type instanceof PaymentType ? $item->payment_type->label() : PaymentType::from($item->payment_type)->label(),
|
||||
'count' => (int) $item->count,
|
||||
'total' => (int) $item->total,
|
||||
]);
|
||||
|
||||
$byMarketing = Order::query()
|
||||
->whereNotNull('marketing_id')
|
||||
->join('users', 'orders.marketing_id', '=', 'users.id')
|
||||
->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id')
|
||||
->selectRaw('COALESCE(user_profiles.full_name, users.username) as name, COUNT(*) as count, SUM(orders.total_amount) as total')
|
||||
->groupBy('orders.marketing_id', 'users.username', 'user_profiles.full_name')
|
||||
->orderByDesc('total')
|
||||
->limit(5)
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'name' => $item->name,
|
||||
'count' => (int) $item->count,
|
||||
'total' => (int) $item->total,
|
||||
]);
|
||||
|
||||
$byStatus = Order::query()
|
||||
->selectRaw('status, COUNT(*) as count')
|
||||
->groupBy('status')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'status' => $item->status instanceof OrderStatus ? $item->status->value : $item->status,
|
||||
'label' => $item->status instanceof OrderStatus ? $item->status->label() : OrderStatus::from($item->status)->label(),
|
||||
'count' => (int) $item->count,
|
||||
]);
|
||||
|
||||
return [
|
||||
'by_channel' => $byChannel->toArray(),
|
||||
'by_payment_type' => $byPaymentType->toArray(),
|
||||
'by_marketing' => $byMarketing->toArray(),
|
||||
'by_status' => $byStatus->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
private function getRevenueSummary(): array
|
||||
{
|
||||
$data = Order::query()
|
||||
->where('status', OrderStatus::COMPLETED)
|
||||
->selectRaw('SUM(total_amount) as total_revenue, SUM(discount) as total_discount, SUM(shipping_cost) as total_shipping, COUNT(*) as total_orders, AVG(total_amount) as avg_order')
|
||||
->first();
|
||||
|
||||
$totalMarketplaceFees = Order::query()
|
||||
->where('status', OrderStatus::COMPLETED)
|
||||
->whereNotNull('marketplace_settings_snapshot')
|
||||
->get()
|
||||
->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0));
|
||||
|
||||
return [
|
||||
'total_revenue' => (int) ($data->total_revenue ?? 0),
|
||||
'total_discount' => (int) ($data->total_discount ?? 0),
|
||||
'total_marketplace_fees' => $totalMarketplaceFees,
|
||||
'total_potongan' => (int) ($data->total_discount ?? 0) + $totalMarketplaceFees,
|
||||
'total_shipping' => (int) ($data->total_shipping ?? 0),
|
||||
'total_orders' => (int) ($data->total_orders ?? 0),
|
||||
'avg_order' => (int) ($data->avg_order ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
private function getMarketplaceSummary(): array
|
||||
{
|
||||
$marketplaceOrders = Order::query()
|
||||
->where('status', OrderStatus::COMPLETED)
|
||||
->whereIn('channel', [OrderChannel::SHOPEE, OrderChannel::TIKTOK])
|
||||
->whereNotNull('marketplace_settings_snapshot')
|
||||
->get();
|
||||
|
||||
$totalRevenue = $marketplaceOrders->sum('total_amount');
|
||||
$totalFees = $marketplaceOrders->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0));
|
||||
$totalNet = $marketplaceOrders->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['net_amount'] ?? 0));
|
||||
$totalOrders = $marketplaceOrders->count();
|
||||
|
||||
return [
|
||||
'total_revenue' => (int) $totalRevenue,
|
||||
'total_fees' => (int) $totalFees,
|
||||
'total_net' => (int) $totalNet,
|
||||
'total_orders' => (int) $totalOrders,
|
||||
];
|
||||
}
|
||||
|
||||
private function getCashAccounts(): array
|
||||
{
|
||||
return CashAccount::query()
|
||||
->selectRaw('name, balance')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'name' => $item->name,
|
||||
'balance' => (int) $item->balance,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
private function getMonthlyCashFlow(): array
|
||||
{
|
||||
$months = collect();
|
||||
for ($i = 5; $i >= 0; $i--) {
|
||||
$date = Carbon::now()->subMonths($i);
|
||||
$months->push([
|
||||
'year' => $date->year,
|
||||
'month' => $date->month,
|
||||
'label' => $date->translatedFormat('M Y'),
|
||||
]);
|
||||
}
|
||||
|
||||
$result = $months->map(function ($month) {
|
||||
$start = Carbon::create($month['year'], $month['month'], 1)->startOfMonth();
|
||||
$end = $start->copy()->endOfMonth();
|
||||
|
||||
$data = CashTransaction::query()
|
||||
->whereBetween('created_at', [$start, $end])
|
||||
->selectRaw("
|
||||
COALESCE(SUM(CASE WHEN type = 'deposit' THEN amount ELSE 0 END), 0) as deposits,
|
||||
COALESCE(SUM(CASE WHEN type = 'withdrawal' THEN amount ELSE 0 END), 0) as withdrawals
|
||||
")
|
||||
->first();
|
||||
|
||||
return [
|
||||
'label' => $month['label'],
|
||||
'deposits' => (int) $data->deposits,
|
||||
'withdrawals' => (int) $data->withdrawals,
|
||||
];
|
||||
});
|
||||
|
||||
return $result->toArray();
|
||||
}
|
||||
|
||||
private function getMonthlyExpenses(Carbon $startOfMonth, Carbon $endOfMonth): array
|
||||
{
|
||||
$data = Expense::query()
|
||||
->whereBetween('created_at', [$startOfMonth, $endOfMonth])
|
||||
->selectRaw('COALESCE(SUM(amount), 0) as total, COUNT(*) as count')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'total' => (int) ($data->total ?? 0),
|
||||
'count' => (int) ($data->count ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
private function getKasbonSummary(): array
|
||||
{
|
||||
$pending = EmployeeAdvance::query()
|
||||
->where('status', EmployeeAdvanceStatus::PENDING)
|
||||
->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')
|
||||
->first();
|
||||
|
||||
$approved = EmployeeAdvance::query()
|
||||
->where('status', EmployeeAdvanceStatus::APPROVED)
|
||||
->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')
|
||||
->first();
|
||||
|
||||
$paid = EmployeeAdvance::query()
|
||||
->where('status', EmployeeAdvanceStatus::PAID)
|
||||
->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'pending' => [
|
||||
'count' => (int) ($pending->count ?? 0),
|
||||
'total' => (int) ($pending->total ?? 0),
|
||||
],
|
||||
'approved' => [
|
||||
'count' => (int) ($approved->count ?? 0),
|
||||
'total' => (int) ($approved->total ?? 0),
|
||||
],
|
||||
'paid' => [
|
||||
'count' => (int) ($paid->count ?? 0),
|
||||
'total' => (int) ($paid->total ?? 0),
|
||||
],
|
||||
'total' => (int) ($pending->total ?? 0) + (int) ($approved->total ?? 0) + (int) ($paid->total ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
private function getPayrollSummary(Carbon $startOfMonth, Carbon $endOfMonth): array
|
||||
{
|
||||
$period = PayrollPeriod::query()
|
||||
->where('year', $startOfMonth->year)
|
||||
->where('month', $startOfMonth->month)
|
||||
->first();
|
||||
|
||||
if (! $period) {
|
||||
return [
|
||||
'has_period' => false,
|
||||
'total_employees' => 0,
|
||||
'total_amount' => 0,
|
||||
'paid_amount' => 0,
|
||||
'unpaid_amount' => 0,
|
||||
'paid_count' => 0,
|
||||
'unpaid_count' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$payrolls = Payroll::query()
|
||||
->where('payroll_period_id', $period->id)
|
||||
->selectRaw("
|
||||
COUNT(*) as total_employees,
|
||||
COALESCE(SUM(total_amount), 0) as total_amount,
|
||||
COALESCE(SUM(CASE WHEN status = 'paid' THEN total_amount ELSE 0 END), 0) as paid_amount,
|
||||
COALESCE(SUM(CASE WHEN status = 'unpaid' THEN total_amount ELSE 0 END), 0) as unpaid_amount,
|
||||
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) as paid_count,
|
||||
SUM(CASE WHEN status = 'unpaid' THEN 1 ELSE 0 END) as unpaid_count
|
||||
")
|
||||
->first();
|
||||
|
||||
return [
|
||||
'has_period' => true,
|
||||
'period_status' => $period->status->value,
|
||||
'total_employees' => (int) ($payrolls->total_employees ?? 0),
|
||||
'total_amount' => (int) ($payrolls->total_amount ?? 0),
|
||||
'paid_amount' => (int) ($payrolls->paid_amount ?? 0),
|
||||
'unpaid_amount' => (int) ($payrolls->unpaid_amount ?? 0),
|
||||
'paid_count' => (int) ($payrolls->paid_count ?? 0),
|
||||
'unpaid_count' => (int) ($payrolls->unpaid_count ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
private function getLeaveRequestSummary(Carbon $startOfMonth, Carbon $endOfMonth): array
|
||||
{
|
||||
$data = LeaveRequest::query()
|
||||
->where(function ($q) use ($startOfMonth, $endOfMonth) {
|
||||
$q->whereBetween('start_date', [$startOfMonth, $endOfMonth])
|
||||
->orWhereBetween('end_date', [$startOfMonth, $endOfMonth]);
|
||||
})
|
||||
->selectRaw("
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count,
|
||||
SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END) as approved_count,
|
||||
SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected_count
|
||||
")
|
||||
->first();
|
||||
|
||||
return [
|
||||
'total' => (int) ($data->total ?? 0),
|
||||
'pending' => (int) ($data->pending_count ?? 0),
|
||||
'approved' => (int) ($data->approved_count ?? 0),
|
||||
'rejected' => (int) ($data->rejected_count ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
private function getEmployeeSummary(): array
|
||||
{
|
||||
$total = Employee::query()->count();
|
||||
|
||||
$byStatus = Employee::query()
|
||||
->selectRaw('employment_status, COUNT(*) as count')
|
||||
->groupBy('employment_status')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'status' => $item->employment_status instanceof EmploymentStatus ? $item->employment_status->value : $item->employment_status,
|
||||
'label' => $item->employment_status instanceof EmploymentStatus ? $item->employment_status->label() : EmploymentStatus::from($item->employment_status)->label(),
|
||||
'count' => (int) $item->count,
|
||||
]);
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'by_status' => $byStatus->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
private function getAttendanceToday(): array
|
||||
{
|
||||
$today = Carbon::today();
|
||||
|
||||
$totalEmployees = Employee::query()->count();
|
||||
|
||||
$present = Attendance::query()
|
||||
->where('attendance_date', $today)
|
||||
->distinct('employee_id')
|
||||
->count('employee_id');
|
||||
|
||||
return [
|
||||
'total_employees' => $totalEmployees,
|
||||
'present' => $present,
|
||||
'absent' => max(0, $totalEmployees - $present),
|
||||
];
|
||||
}
|
||||
|
||||
private function getMonthlyRevenueTrend(): array
|
||||
{
|
||||
$months = collect();
|
||||
for ($i = 5; $i >= 0; $i--) {
|
||||
$date = Carbon::now()->subMonths($i);
|
||||
$months->push([
|
||||
'year' => $date->year,
|
||||
'month' => $date->month,
|
||||
'label' => $date->translatedFormat('M Y'),
|
||||
]);
|
||||
}
|
||||
|
||||
$result = $months->map(function ($month) {
|
||||
$start = Carbon::create($month['year'], $month['month'], 1)->startOfMonth();
|
||||
$end = $start->copy()->endOfMonth();
|
||||
|
||||
$data = Order::query()
|
||||
->where('status', OrderStatus::COMPLETED)
|
||||
->whereBetween('created_at', [$start, $end])
|
||||
->selectRaw('COALESCE(SUM(total_amount), 0) as total, COUNT(*) as count')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'label' => $month['label'],
|
||||
'total' => (int) $data->total,
|
||||
'count' => (int) $data->count,
|
||||
];
|
||||
});
|
||||
|
||||
return $result->toArray();
|
||||
}
|
||||
|
||||
private function getMonthlyPurchaseTrend(): array
|
||||
{
|
||||
$months = collect();
|
||||
for ($i = 5; $i >= 0; $i--) {
|
||||
$date = Carbon::now()->subMonths($i);
|
||||
$months->push([
|
||||
'year' => $date->year,
|
||||
'month' => $date->month,
|
||||
'label' => $date->translatedFormat('M Y'),
|
||||
]);
|
||||
}
|
||||
|
||||
$result = $months->map(function ($month) {
|
||||
$start = Carbon::create($month['year'], $month['month'], 1)->startOfMonth();
|
||||
$end = $start->copy()->endOfMonth();
|
||||
|
||||
$data = Purchase::query()
|
||||
->whereBetween('created_at', [$start, $end])
|
||||
->selectRaw('COALESCE(SUM(total), 0) as total, COUNT(*) as count')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'label' => $month['label'],
|
||||
'total' => (int) $data->total,
|
||||
'count' => (int) $data->count,
|
||||
];
|
||||
});
|
||||
|
||||
return $result->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,16 +22,9 @@ public function __construct(
|
||||
|
||||
public function index(): Response
|
||||
{
|
||||
$categories = Category::whereHas('products')->get(['id', 'name', 'slug']);
|
||||
$categories = Category::getActiveWithProducts();
|
||||
|
||||
$products = Product::where('is_active', true)
|
||||
->with([
|
||||
'categories',
|
||||
'variants' => fn ($query) => $query
|
||||
->with('media')
|
||||
->orderBy('created_at'),
|
||||
])
|
||||
->get();
|
||||
$products = Product::getActiveWithVariantsAndCategories();
|
||||
|
||||
$products = $products->map(function ($product) {
|
||||
$product->variants->each(function ($variant) {
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
@ -20,4 +21,9 @@ public function products(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Product::class, 'product_categories');
|
||||
}
|
||||
|
||||
public static function getActiveWithProducts(): Collection
|
||||
{
|
||||
return self::whereHas('products')->get(['id', 'name', 'slug']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,9 @@
|
||||
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Attributes\Scope;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
@ -11,6 +14,10 @@
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\Sluggable\Attributes\Sluggable;
|
||||
|
||||
/**
|
||||
* @method static \Illuminate\Database\Eloquent\Builder active()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder inactive()
|
||||
*/
|
||||
#[Guarded(['id'])]
|
||||
#[Sluggable(from: 'name', to: 'slug')]
|
||||
class Product extends Model
|
||||
@ -33,4 +40,28 @@ public function variants(): HasMany
|
||||
{
|
||||
return $this->hasMany(ProductVariant::class);
|
||||
}
|
||||
|
||||
#[Scope]
|
||||
public function active(Builder $query): void
|
||||
{
|
||||
$query->where('is_active', true);
|
||||
}
|
||||
|
||||
#[Scope]
|
||||
public function inactive(Builder $query): void
|
||||
{
|
||||
$query->where('is_active', false);
|
||||
}
|
||||
|
||||
public static function getActiveWithVariantsAndCategories(): Collection
|
||||
{
|
||||
return self::active()
|
||||
->with([
|
||||
'categories',
|
||||
'variants' => fn ($query) => $query
|
||||
->with('media')
|
||||
->orderBy('created_at'),
|
||||
])
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
||||
@ -340,7 +340,7 @@ private function payrollEligibleEmployees(PayrollPeriod $period): Collection
|
||||
return Employee::query()
|
||||
->with('user.roles')
|
||||
->whereHas('user', function (Builder $query): void {
|
||||
$query->where('is_active', true)
|
||||
$query->active()
|
||||
->whereDoesntHave('roles', fn (Builder $query) => $query->whereIn('name', [
|
||||
Role::DEVELOPER->value,
|
||||
Role::OWNER->value,
|
||||
|
||||
@ -183,7 +183,7 @@ public function productCatalog(?Cutting $cutting = null, ?User $user = null): Co
|
||||
->orderBy('created_at'),
|
||||
])
|
||||
->where(function (Builder $query) use ($selectedVariantIds): void {
|
||||
$query->where('is_active', true);
|
||||
$query->active();
|
||||
|
||||
if ($selectedVariantIds !== []) {
|
||||
$query->orWhereHas(
|
||||
|
||||
@ -143,7 +143,7 @@ public function catalogItems(?Order $order = null, ?User $user = null): Collecti
|
||||
->orderBy('created_at'),
|
||||
])
|
||||
->where(function (Builder $query) use ($orderVariantIds): void {
|
||||
$query->where('is_active', true);
|
||||
$query->active();
|
||||
|
||||
if ($orderVariantIds !== []) {
|
||||
$query->orWhereHas(
|
||||
|
||||
579
app/Services/System/DashboardService.php
Normal file
579
app/Services/System/DashboardService.php
Normal file
@ -0,0 +1,579 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\System;
|
||||
|
||||
use App\Enums\CuttingStatus;
|
||||
use App\Enums\EmployeeAdvanceStatus;
|
||||
use App\Enums\EmploymentStatus;
|
||||
use App\Enums\OrderChannel;
|
||||
use App\Enums\OrderStatus;
|
||||
use App\Enums\PaymentType;
|
||||
use App\Models\Attendance;
|
||||
use App\Models\CashAccount;
|
||||
use App\Models\CashTransaction;
|
||||
use App\Models\Category;
|
||||
use App\Models\Cutting;
|
||||
use App\Models\CuttingResult;
|
||||
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\PayrollPeriod;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Models\Purchase;
|
||||
use App\Models\RawMaterialPrice;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class DashboardService
|
||||
{
|
||||
public function getRawMaterialStock(): array
|
||||
{
|
||||
$data = RawMaterialPrice::query()
|
||||
->selectRaw('SUM(stock) as total_stock, SUM(stock * price) as total_value')
|
||||
->first();
|
||||
|
||||
$byUnit = 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')
|
||||
->groupBy('raw_materials.unit')
|
||||
->get()
|
||||
->mapWithKeys(fn ($item) => [
|
||||
$item->unit => (float) $item->total_stock,
|
||||
])
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'total_stock' => (float) ($data->total_stock ?? 0),
|
||||
'total_value' => (int) ($data->total_value ?? 0),
|
||||
'by_unit' => $byUnit,
|
||||
];
|
||||
}
|
||||
|
||||
public function getProductStock(): array
|
||||
{
|
||||
$data = ProductVariant::query()
|
||||
->selectRaw('SUM(stock) as total_stock, SUM(reject_stock) as total_reject, COUNT(*) as total_variants')
|
||||
->first();
|
||||
|
||||
$totalStock = (int) ($data->total_stock ?? 0);
|
||||
|
||||
$totalValue = Cutting::query()
|
||||
->whereNotNull('cost_per_unit')
|
||||
->join('cutting_results', 'cuttings.id', '=', 'cutting_results.cutting_id')
|
||||
->selectRaw('SUM(cutting_results.warehouse_stock * cuttings.cost_per_unit) as total_value')
|
||||
->value('total_value');
|
||||
|
||||
$totalProducts = Product::query()->count();
|
||||
$totalCategories = Category::query()->count();
|
||||
|
||||
return [
|
||||
'total_stock' => $totalStock,
|
||||
'total_reject' => (int) ($data->total_reject ?? 0),
|
||||
'total_value' => (int) ($totalValue ?? 0),
|
||||
'total_variants' => (int) ($data->total_variants ?? 0),
|
||||
'total_products' => $totalProducts,
|
||||
'total_categories' => $totalCategories,
|
||||
];
|
||||
}
|
||||
|
||||
public function getLowStockProducts(): array
|
||||
{
|
||||
return ProductVariant::query()
|
||||
->where('stock', '<=', ProductVariant::minStock())
|
||||
->where('stock', '>', 0)
|
||||
->join('products', 'product_variants.product_id', '=', 'products.id')
|
||||
->selectRaw("CONCAT(products.name, ' - ', product_variants.name) as full_name, product_variants.stock")
|
||||
->orderBy('stock')
|
||||
->limit(5)
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'name' => $item->full_name,
|
||||
'stock' => (int) $item->stock,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function getLowStockMaterials(): array
|
||||
{
|
||||
return RawMaterialPrice::query()
|
||||
->where('stock', '<=', 5)
|
||||
->where('stock', '>', 0)
|
||||
->join('raw_materials', 'raw_material_prices.raw_material_id', '=', 'raw_materials.id')
|
||||
->selectRaw("CONCAT(raw_materials.name, ' - ', raw_material_prices.variant) as full_name, raw_material_prices.stock, raw_materials.unit")
|
||||
->orderBy('stock')
|
||||
->limit(5)
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'name' => $item->full_name,
|
||||
'stock' => (float) $item->stock,
|
||||
'unit' => $item->unit,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function getTopSuppliers(): array
|
||||
{
|
||||
return Purchase::query()
|
||||
->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(): array
|
||||
{
|
||||
return Order::query()
|
||||
->where('status', OrderStatus::COMPLETED)
|
||||
->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 getTopProducts(): array
|
||||
{
|
||||
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)
|
||||
->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 getPurchaseSummary(): array
|
||||
{
|
||||
$data = Purchase::query()
|
||||
->selectRaw('COUNT(*) as total_purchases, SUM(total) as total_spent, SUM(discount) as total_discount')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'total_purchases' => (int) ($data->total_purchases ?? 0),
|
||||
'total_spent' => (int) ($data->total_spent ?? 0),
|
||||
'total_discount' => (int) ($data->total_discount ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
public function getCuttingSummary(): array
|
||||
{
|
||||
$totalCost = Cutting::query()
|
||||
->selectRaw('COALESCE(SUM(total_material_cost), 0) + COALESCE(SUM(sewing_cost), 0) + COALESCE(SUM(other_cost), 0) as total_cost')
|
||||
->value('total_cost');
|
||||
|
||||
$totalResults = CuttingResult::query()
|
||||
->selectRaw('SUM(cutting_result) as total_cutting, SUM(warehouse_stock) as total_warehouse, SUM(cutting_reject) as total_reject')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'total_cost' => (int) ($totalCost ?? 0),
|
||||
'total_cutting' => (int) ($totalResults->total_cutting ?? 0),
|
||||
'total_warehouse' => (int) ($totalResults->total_warehouse ?? 0),
|
||||
'total_reject' => (int) ($totalResults->total_reject ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
public function getCuttingByStatus(): array
|
||||
{
|
||||
return Cutting::query()
|
||||
->selectRaw('status, COUNT(*) as count')
|
||||
->groupBy('status')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'status' => $item->status instanceof CuttingStatus ? $item->status->value : $item->status,
|
||||
'label' => $item->status instanceof CuttingStatus ? $item->status->label() : CuttingStatus::from($item->status)->label(),
|
||||
'count' => (int) $item->count,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function getOrderStats(): array
|
||||
{
|
||||
$byChannel = Order::query()
|
||||
->selectRaw('channel, COUNT(*) as count, SUM(total_amount) as total')
|
||||
->groupBy('channel')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'channel' => $item->channel instanceof OrderChannel ? $item->channel->value : $item->channel,
|
||||
'label' => $item->channel instanceof OrderChannel ? $item->channel->label() : OrderChannel::from($item->channel)->label(),
|
||||
'count' => (int) $item->count,
|
||||
'total' => (int) $item->total,
|
||||
]);
|
||||
|
||||
$byPaymentType = Order::query()
|
||||
->selectRaw('payment_type, COUNT(*) as count, SUM(total_amount) as total')
|
||||
->groupBy('payment_type')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'payment_type' => $item->payment_type instanceof PaymentType ? $item->payment_type->value : $item->payment_type,
|
||||
'label' => $item->payment_type instanceof PaymentType ? $item->payment_type->label() : PaymentType::from($item->payment_type)->label(),
|
||||
'count' => (int) $item->count,
|
||||
'total' => (int) $item->total,
|
||||
]);
|
||||
|
||||
$byMarketing = Order::query()
|
||||
->whereNotNull('marketing_id')
|
||||
->join('users', 'orders.marketing_id', '=', 'users.id')
|
||||
->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id')
|
||||
->selectRaw('COALESCE(user_profiles.full_name, users.username) as name, COUNT(*) as count, SUM(orders.total_amount) as total')
|
||||
->groupBy('orders.marketing_id', 'users.username', 'user_profiles.full_name')
|
||||
->orderByDesc('total')
|
||||
->limit(5)
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'name' => $item->name,
|
||||
'count' => (int) $item->count,
|
||||
'total' => (int) $item->total,
|
||||
]);
|
||||
|
||||
$byStatus = Order::query()
|
||||
->selectRaw('status, COUNT(*) as count')
|
||||
->groupBy('status')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'status' => $item->status instanceof OrderStatus ? $item->status->value : $item->status,
|
||||
'label' => $item->status instanceof OrderStatus ? $item->status->label() : OrderStatus::from($item->status)->label(),
|
||||
'count' => (int) $item->count,
|
||||
]);
|
||||
|
||||
return [
|
||||
'by_channel' => $byChannel->toArray(),
|
||||
'by_payment_type' => $byPaymentType->toArray(),
|
||||
'by_marketing' => $byMarketing->toArray(),
|
||||
'by_status' => $byStatus->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getRevenueSummary(): array
|
||||
{
|
||||
$data = Order::query()
|
||||
->where('status', OrderStatus::COMPLETED)
|
||||
->selectRaw('SUM(total_amount) as total_revenue, SUM(discount) as total_discount, SUM(shipping_cost) as total_shipping, COUNT(*) as total_orders, AVG(total_amount) as avg_order')
|
||||
->first();
|
||||
|
||||
$totalMarketplaceFees = Order::query()
|
||||
->where('status', OrderStatus::COMPLETED)
|
||||
->whereNotNull('marketplace_settings_snapshot')
|
||||
->get()
|
||||
->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0));
|
||||
|
||||
return [
|
||||
'total_revenue' => (int) ($data->total_revenue ?? 0),
|
||||
'total_discount' => (int) ($data->total_discount ?? 0),
|
||||
'total_marketplace_fees' => $totalMarketplaceFees,
|
||||
'total_potongan' => (int) ($data->total_discount ?? 0) + $totalMarketplaceFees,
|
||||
'total_shipping' => (int) ($data->total_shipping ?? 0),
|
||||
'total_orders' => (int) ($data->total_orders ?? 0),
|
||||
'avg_order' => (int) ($data->avg_order ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
public function getMarketplaceSummary(): array
|
||||
{
|
||||
$marketplaceOrders = Order::query()
|
||||
->where('status', OrderStatus::COMPLETED)
|
||||
->whereIn('channel', [OrderChannel::SHOPEE, OrderChannel::TIKTOK])
|
||||
->whereNotNull('marketplace_settings_snapshot')
|
||||
->get();
|
||||
|
||||
$totalRevenue = $marketplaceOrders->sum('total_amount');
|
||||
$totalFees = $marketplaceOrders->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0));
|
||||
$totalNet = $marketplaceOrders->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['net_amount'] ?? 0));
|
||||
$totalOrders = $marketplaceOrders->count();
|
||||
|
||||
return [
|
||||
'total_revenue' => (int) $totalRevenue,
|
||||
'total_fees' => (int) $totalFees,
|
||||
'total_net' => (int) $totalNet,
|
||||
'total_orders' => (int) $totalOrders,
|
||||
];
|
||||
}
|
||||
|
||||
public function getCashAccounts(): array
|
||||
{
|
||||
return CashAccount::query()
|
||||
->selectRaw('name, balance')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'name' => $item->name,
|
||||
'balance' => (int) $item->balance,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function getMonthlyCashFlow(): array
|
||||
{
|
||||
$months = collect();
|
||||
for ($i = 5; $i >= 0; $i--) {
|
||||
$date = Carbon::now()->subMonths($i);
|
||||
$months->push([
|
||||
'year' => $date->year,
|
||||
'month' => $date->month,
|
||||
'label' => $date->translatedFormat('M Y'),
|
||||
]);
|
||||
}
|
||||
|
||||
$result = $months->map(function ($month) {
|
||||
$start = Carbon::create($month['year'], $month['month'], 1)->startOfMonth();
|
||||
$end = $start->copy()->endOfMonth();
|
||||
|
||||
$data = CashTransaction::query()
|
||||
->whereBetween('created_at', [$start, $end])
|
||||
->selectRaw("
|
||||
COALESCE(SUM(CASE WHEN type = 'deposit' THEN amount ELSE 0 END), 0) as deposits,
|
||||
COALESCE(SUM(CASE WHEN type = 'withdrawal' THEN amount ELSE 0 END), 0) as withdrawals
|
||||
")
|
||||
->first();
|
||||
|
||||
return [
|
||||
'label' => $month['label'],
|
||||
'deposits' => (int) $data->deposits,
|
||||
'withdrawals' => (int) $data->withdrawals,
|
||||
];
|
||||
});
|
||||
|
||||
return $result->toArray();
|
||||
}
|
||||
|
||||
public function getMonthlyExpenses(Carbon $startOfMonth, Carbon $endOfMonth): array
|
||||
{
|
||||
$data = Expense::query()
|
||||
->whereBetween('created_at', [$startOfMonth, $endOfMonth])
|
||||
->selectRaw('COALESCE(SUM(amount), 0) as total, COUNT(*) as count')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'total' => (int) ($data->total ?? 0),
|
||||
'count' => (int) ($data->count ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
public function getKasbonSummary(): array
|
||||
{
|
||||
$pending = EmployeeAdvance::query()
|
||||
->where('status', EmployeeAdvanceStatus::PENDING)
|
||||
->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')
|
||||
->first();
|
||||
|
||||
$approved = EmployeeAdvance::query()
|
||||
->where('status', EmployeeAdvanceStatus::APPROVED)
|
||||
->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')
|
||||
->first();
|
||||
|
||||
$paid = EmployeeAdvance::query()
|
||||
->where('status', EmployeeAdvanceStatus::PAID)
|
||||
->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'pending' => [
|
||||
'count' => (int) ($pending->count ?? 0),
|
||||
'total' => (int) ($pending->total ?? 0),
|
||||
],
|
||||
'approved' => [
|
||||
'count' => (int) ($approved->count ?? 0),
|
||||
'total' => (int) ($approved->total ?? 0),
|
||||
],
|
||||
'paid' => [
|
||||
'count' => (int) ($paid->count ?? 0),
|
||||
'total' => (int) ($paid->total ?? 0),
|
||||
],
|
||||
'total' => (int) ($pending->total ?? 0) + (int) ($approved->total ?? 0) + (int) ($paid->total ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
public function getPayrollSummary(Carbon $startOfMonth, Carbon $endOfMonth): array
|
||||
{
|
||||
$period = PayrollPeriod::query()
|
||||
->where('year', $startOfMonth->year)
|
||||
->where('month', $startOfMonth->month)
|
||||
->first();
|
||||
|
||||
if (! $period) {
|
||||
return [
|
||||
'has_period' => false,
|
||||
'total_employees' => 0,
|
||||
'total_amount' => 0,
|
||||
'paid_amount' => 0,
|
||||
'unpaid_amount' => 0,
|
||||
'paid_count' => 0,
|
||||
'unpaid_count' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$payrolls = Payroll::query()
|
||||
->where('payroll_period_id', $period->id)
|
||||
->selectRaw("
|
||||
COUNT(*) as total_employees,
|
||||
COALESCE(SUM(total_amount), 0) as total_amount,
|
||||
COALESCE(SUM(CASE WHEN status = 'paid' THEN total_amount ELSE 0 END), 0) as paid_amount,
|
||||
COALESCE(SUM(CASE WHEN status = 'unpaid' THEN total_amount ELSE 0 END), 0) as unpaid_amount,
|
||||
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) as paid_count,
|
||||
SUM(CASE WHEN status = 'unpaid' THEN 1 ELSE 0 END) as unpaid_count
|
||||
")
|
||||
->first();
|
||||
|
||||
return [
|
||||
'has_period' => true,
|
||||
'period_status' => $period->status->value,
|
||||
'total_employees' => (int) ($payrolls->total_employees ?? 0),
|
||||
'total_amount' => (int) ($payrolls->total_amount ?? 0),
|
||||
'paid_amount' => (int) ($payrolls->paid_amount ?? 0),
|
||||
'unpaid_amount' => (int) ($payrolls->unpaid_amount ?? 0),
|
||||
'paid_count' => (int) ($payrolls->paid_count ?? 0),
|
||||
'unpaid_count' => (int) ($payrolls->unpaid_count ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
public function getLeaveRequestSummary(Carbon $startOfMonth, Carbon $endOfMonth): array
|
||||
{
|
||||
$data = LeaveRequest::query()
|
||||
->where(function ($q) use ($startOfMonth, $endOfMonth) {
|
||||
$q->whereBetween('start_date', [$startOfMonth, $endOfMonth])
|
||||
->orWhereBetween('end_date', [$startOfMonth, $endOfMonth]);
|
||||
})
|
||||
->selectRaw("
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count,
|
||||
SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END) as approved_count,
|
||||
SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected_count
|
||||
")
|
||||
->first();
|
||||
|
||||
return [
|
||||
'total' => (int) ($data->total ?? 0),
|
||||
'pending' => (int) ($data->pending_count ?? 0),
|
||||
'approved' => (int) ($data->approved_count ?? 0),
|
||||
'rejected' => (int) ($data->rejected_count ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
public function getEmployeeSummary(): array
|
||||
{
|
||||
$total = Employee::query()->count();
|
||||
|
||||
$byStatus = Employee::query()
|
||||
->selectRaw('employment_status, COUNT(*) as count')
|
||||
->groupBy('employment_status')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'status' => $item->employment_status instanceof EmploymentStatus ? $item->employment_status->value : $item->employment_status,
|
||||
'label' => $item->employment_status instanceof EmploymentStatus ? $item->employment_status->label() : EmploymentStatus::from($item->employment_status)->label(),
|
||||
'count' => (int) $item->count,
|
||||
]);
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'by_status' => $byStatus->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getAttendanceToday(): array
|
||||
{
|
||||
$today = Carbon::today();
|
||||
|
||||
$totalEmployees = Employee::query()->count();
|
||||
|
||||
$present = Attendance::query()
|
||||
->where('attendance_date', $today)
|
||||
->distinct('employee_id')
|
||||
->count('employee_id');
|
||||
|
||||
return [
|
||||
'total_employees' => $totalEmployees,
|
||||
'present' => $present,
|
||||
'absent' => max(0, $totalEmployees - $present),
|
||||
];
|
||||
}
|
||||
|
||||
public function getMonthlyRevenueTrend(): array
|
||||
{
|
||||
$months = collect();
|
||||
for ($i = 5; $i >= 0; $i--) {
|
||||
$date = Carbon::now()->subMonths($i);
|
||||
$months->push([
|
||||
'year' => $date->year,
|
||||
'month' => $date->month,
|
||||
'label' => $date->translatedFormat('M Y'),
|
||||
]);
|
||||
}
|
||||
|
||||
$result = $months->map(function ($month) {
|
||||
$start = Carbon::create($month['year'], $month['month'], 1)->startOfMonth();
|
||||
$end = $start->copy()->endOfMonth();
|
||||
|
||||
$data = Order::query()
|
||||
->where('status', OrderStatus::COMPLETED)
|
||||
->whereBetween('created_at', [$start, $end])
|
||||
->selectRaw('COALESCE(SUM(total_amount), 0) as total, COUNT(*) as count')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'label' => $month['label'],
|
||||
'total' => (int) $data->total,
|
||||
'count' => (int) $data->count,
|
||||
];
|
||||
});
|
||||
|
||||
return $result->toArray();
|
||||
}
|
||||
|
||||
public function getMonthlyPurchaseTrend(): array
|
||||
{
|
||||
$months = collect();
|
||||
for ($i = 5; $i >= 0; $i--) {
|
||||
$date = Carbon::now()->subMonths($i);
|
||||
$months->push([
|
||||
'year' => $date->year,
|
||||
'month' => $date->month,
|
||||
'label' => $date->translatedFormat('M Y'),
|
||||
]);
|
||||
}
|
||||
|
||||
$result = $months->map(function ($month) {
|
||||
$start = Carbon::create($month['year'], $month['month'], 1)->startOfMonth();
|
||||
$end = $start->copy()->endOfMonth();
|
||||
|
||||
$data = Purchase::query()
|
||||
->whereBetween('created_at', [$start, $end])
|
||||
->selectRaw('COALESCE(SUM(total), 0) as total, COUNT(*) as count')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'label' => $month['label'],
|
||||
'total' => (int) $data->total,
|
||||
'count' => (int) $data->count,
|
||||
];
|
||||
});
|
||||
|
||||
return $result->toArray();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user