diff --git a/app/Http/Controllers/Admin/DashboardController.php b/app/Http/Controllers/Admin/DashboardController.php index f1aa753..d99f17a 100644 --- a/app/Http/Controllers/Admin/DashboardController.php +++ b/app/Http/Controllers/Admin/DashboardController.php @@ -3,28 +3,29 @@ namespace App\Http\Controllers\Admin; use App\Enums\CashTransactionType; +use App\Enums\CuttingStatus; use App\Enums\EmployeeAdvanceStatus; -use App\Enums\LeaveRequestStatus; +use App\Enums\EmploymentStatus; use App\Enums\OrderChannel; use App\Enums\OrderStatus; use App\Enums\PaymentType; -use App\Enums\PayrollStatus; use App\Http\Controllers\Controller; +use App\Models\Attendance; use App\Models\CashAccount; use App\Models\CashTransaction; use App\Models\Cutting; use App\Models\CuttingResult; -use App\Models\Customer; +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\ProductVariant; use App\Models\Purchase; use App\Models\RawMaterialPrice; -use App\Models\Supplier; -use App\Models\User; use Carbon\Carbon; use Illuminate\Support\Facades\DB; use Inertia\Inertia; @@ -41,16 +42,26 @@ public function index(): Response 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(), '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(), ]); } @@ -87,6 +98,41 @@ private function getProductStock(): array ]; } + 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() @@ -122,6 +168,26 @@ private function getTopCustomers(): array ->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() @@ -153,6 +219,20 @@ private function getCuttingSummary(): array ]; } + 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() @@ -214,7 +294,7 @@ 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') + ->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(); return [ @@ -222,6 +302,7 @@ private function getRevenueSummary(): array 'total_discount' => (int) ($data->total_discount ?? 0), 'total_shipping' => (int) ($data->total_shipping ?? 0), 'total_orders' => (int) ($data->total_orders ?? 0), + 'avg_order' => (int) ($data->avg_order ?? 0), ]; } @@ -237,6 +318,53 @@ private function getCashAccounts(): array ->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() @@ -336,4 +464,105 @@ private function getLeaveRequestSummary(Carbon $startOfMonth, Carbon $endOfMonth '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(); + } } diff --git a/resources/js/pages/admin/Dashboard.vue b/resources/js/pages/admin/Dashboard.vue index b2c6f7f..c32be29 100644 --- a/resources/js/pages/admin/Dashboard.vue +++ b/resources/js/pages/admin/Dashboard.vue @@ -1,6 +1,7 @@