Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
95.18% |
237 / 249 |
|
0.00% |
0 / 1 |
CRAP | |
0.00% |
0 / 1 |
| DashboardController | |
95.18% |
237 / 249 |
|
0.00% |
0 / 1 |
37 | |
0.00% |
0 / 1 |
| index | |
95.18% |
237 / 249 |
|
0.00% |
0 / 1 |
37 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Controllers\admin; |
| 4 | |
| 5 | use App\Enums\OrderStatus; |
| 6 | use App\Enums\PaymentStatus; |
| 7 | use App\Http\Controllers\Controller; |
| 8 | use App\Models\Account; |
| 9 | use App\Models\admin\Customer; |
| 10 | use App\Models\admin\Product; |
| 11 | use App\Models\admin\Purchase; |
| 12 | use App\Models\admin\PurchasePayment; |
| 13 | use App\Models\admin\Sale; |
| 14 | use App\Models\admin\SaleDetalis; |
| 15 | use App\Models\admin\SalePayment; |
| 16 | use App\Models\Order; |
| 17 | use App\Models\OrderDetails; |
| 18 | use App\Models\Payment; |
| 19 | use App\Models\ProductionOrder; |
| 20 | use App\Models\Transaction; |
| 21 | use App\Services\Accounting\AccountingService; |
| 22 | use App\Services\Location\BranchContext; |
| 23 | use App\Services\Payment\PaymentManager; |
| 24 | use Illuminate\Http\Response; |
| 25 | use Illuminate\Support\Facades\DB; |
| 26 | |
| 27 | class DashboardController extends Controller |
| 28 | { |
| 29 | /** |
| 30 | * Display the ERP-style admin dashboard (real data, mobile + desktop |
| 31 | * layouts). See resources/views/admin/dashboard/dashboard.blade.php. |
| 32 | * |
| 33 | * @return Response |
| 34 | */ |
| 35 | public function index() |
| 36 | { |
| 37 | $data = []; |
| 38 | |
| 39 | $today = today(); |
| 40 | $monthStart = now()->startOfMonth(); |
| 41 | $lastMonthStart = now()->subMonthNoOverflow()->startOfMonth(); |
| 42 | $lastMonthEnd = now()->subMonthNoOverflow()->endOfMonth(); |
| 43 | $branchId = app(BranchContext::class)->id(); |
| 44 | $orders = fn () => Order::query()->when($branchId, fn ($query) => $query->where('branch_id', $branchId)); |
| 45 | $payments = fn () => Payment::query()->when($branchId, fn ($query) => $query->whereHas('order', fn ($order) => $order->where('branch_id', $branchId))); |
| 46 | $purchases = fn () => Purchase::query()->when($branchId, fn ($query) => $query->where('branch_id', $branchId)); |
| 47 | $purchasePayments = fn () => PurchasePayment::query()->when($branchId, fn ($query) => $query->whereHas('purchase', fn ($purchase) => $purchase->where('branch_id', $branchId))); |
| 48 | $transactions = fn () => Transaction::query()->when($branchId, fn ($query) => $query->whereHas('account', fn ($account) => $account->where('branch_id', $branchId))); |
| 49 | |
| 50 | // ── Order status breakdown (kept from the previous dashboard) ────── |
| 51 | // Values updated to the new lifecycle (App\Enums\OrderStatus): |
| 52 | // Recived -> Confirmed, Success -> Delivered, Cancel -> Cancelled. |
| 53 | $data['pendingCount'] = $orders()->where('status', OrderStatus::Pending->value)->count(); |
| 54 | $data['confirmedCount'] = $orders()->where('status', OrderStatus::Confirmed->value)->count(); |
| 55 | $data['deliveredCount'] = $orders()->where('status', OrderStatus::Delivered->value)->count(); |
| 56 | $data['cancelledCount'] = $orders()->where('status', OrderStatus::Cancelled->value)->count(); |
| 57 | |
| 58 | // ── "Today at a Glance" (mobile reference card) ──────────────────── |
| 59 | // Combines both transaction systems: website checkout (Order) and |
| 60 | // POS counter sales (Sale/SalePayment) — a single source of truth |
| 61 | // for "Total Sales" instead of only one of the two. |
| 62 | $todayOrderSales = (float) $orders()->whereDate('created_at', $today)->sum('total'); |
| 63 | $todaySalePos = $branchId ? 0.0 : (float) SalePayment::whereHas('sale', fn ($q) => $q->whereDate('created_at', $today))->sum('total'); |
| 64 | $data['todaySales'] = $todayOrderSales + $todaySalePos; |
| 65 | |
| 66 | $data['todayPurchase'] = (float) $purchasePayments()->whereHas('purchase', fn ($q) => $q->whereDate('created_at', $today))->sum('total'); |
| 67 | |
| 68 | $todayOrderCollected = (float) $payments()->whereDate('created_at', $today)->sum('paid'); |
| 69 | $todaySaleCollected = $branchId ? 0.0 : (float) SalePayment::whereHas('sale', fn ($q) => $q->whereDate('created_at', $today))->sum('paid'); |
| 70 | $data['todayCollection'] = $todayOrderCollected + $todaySaleCollected; |
| 71 | |
| 72 | $data['todayPayment'] = (float) $purchasePayments()->whereHas('purchase', fn ($q) => $q->whereDate('created_at', $today))->sum('paid'); |
| 73 | |
| 74 | // ── Today's cash flow ─────────────────────────────────────────────── |
| 75 | $data['cashIn'] = $data['todayCollection']; |
| 76 | $data['cashOut'] = $data['todayPayment']; |
| 77 | $data['netFlow'] = $data['cashIn'] - $data['cashOut']; |
| 78 | |
| 79 | // ── Month totals + % change vs last month (desktop reference cards) ─ |
| 80 | $monthSales = (float) $orders()->where('created_at', '>=', $monthStart)->sum('total') |
| 81 | + ($branchId ? 0.0 : (float) SalePayment::whereHas('sale', fn ($q) => $q->where('created_at', '>=', $monthStart))->sum('total')); |
| 82 | $lastMonthSales = (float) $orders()->whereBetween('created_at', [$lastMonthStart, $lastMonthEnd])->sum('total') |
| 83 | + ($branchId ? 0.0 : (float) SalePayment::whereHas('sale', fn ($q) => $q->whereBetween('created_at', [$lastMonthStart, $lastMonthEnd]))->sum('total')); |
| 84 | |
| 85 | $monthPurchase = (float) $purchasePayments()->whereHas('purchase', fn ($q) => $q->where('created_at', '>=', $monthStart))->sum('total'); |
| 86 | $lastMonthPurchase = (float) $purchasePayments()->whereHas('purchase', fn ($q) => $q->whereBetween('created_at', [$lastMonthStart, $lastMonthEnd]))->sum('total'); |
| 87 | |
| 88 | $totalCustomersNow = Customer::count(); |
| 89 | $customersThisMonth = Customer::where('created_at', '>=', $monthStart)->count(); |
| 90 | $customersLastMonth = Customer::whereBetween('created_at', [$lastMonthStart, $lastMonthEnd])->count(); |
| 91 | |
| 92 | $totalProductsNow = Product::count(); |
| 93 | $productsThisMonth = Product::where('created_at', '>=', $monthStart)->count(); |
| 94 | $productsLastMonth = Product::whereBetween('created_at', [$lastMonthStart, $lastMonthEnd])->count(); |
| 95 | |
| 96 | $pct = function ($current, $previous) { |
| 97 | if ($previous <= 0) { |
| 98 | return $current > 0 ? 100.0 : 0.0; |
| 99 | } |
| 100 | |
| 101 | return round((($current - $previous) / $previous) * 100, 1); |
| 102 | }; |
| 103 | |
| 104 | $data['monthSales'] = $monthSales; |
| 105 | $data['monthSalesPct'] = $pct($monthSales, $lastMonthSales); |
| 106 | |
| 107 | $data['monthPurchase'] = $monthPurchase; |
| 108 | $data['monthPurchasePct'] = $pct($monthPurchase, $lastMonthPurchase); |
| 109 | |
| 110 | $data['totalCustomers'] = $totalCustomersNow; |
| 111 | $data['totalCustomersPct'] = $pct($customersThisMonth, $customersLastMonth); |
| 112 | |
| 113 | $data['totalProducts'] = $totalProductsNow; |
| 114 | $data['totalProductsPct'] = $pct($productsThisMonth, $productsLastMonth); |
| 115 | |
| 116 | // ── Legacy keys still referenced elsewhere on the page ───────────── |
| 117 | $data['totalRevenue'] = (float) $orders()->where('status', OrderStatus::Delivered->value)->sum('total'); |
| 118 | $data['totalOrders'] = $orders()->count(); |
| 119 | $data['totalDue'] = (float) $orders()->sum('due'); |
| 120 | $data['outOfStockCount'] = Product::where('stock_status', 'out_of_stock')->count(); |
| 121 | $data['totalSuppliers'] = totalSupplier(); |
| 122 | $data['totalPurchase'] = (float) totalPurchase(); |
| 123 | |
| 124 | // ── 7-day sales trend (Sales Overview line chart) ────────────────── |
| 125 | $chartDays = []; |
| 126 | $chartSales = []; |
| 127 | for ($i = 6; $i >= 0; $i--) { |
| 128 | $day = now()->subDays($i); |
| 129 | $orderSum = (float) $orders()->whereDate('created_at', $day)->sum('total'); |
| 130 | $saleSum = $branchId ? 0.0 : (float) SalePayment::whereHas('sale', fn ($q) => $q->whereDate('created_at', $day))->sum('total'); |
| 131 | $chartDays[] = $day->format('D j'); |
| 132 | $chartSales[] = round($orderSum + $saleSum, 2); |
| 133 | } |
| 134 | $data['chartDays'] = $chartDays; |
| 135 | $data['chartSales'] = $chartSales; |
| 136 | |
| 137 | // ── Stock summary donut ───────────────────────────────────────────── |
| 138 | $inStock = Product::where('stock_status', 'in_stock')->where('stock', '>', 5)->count(); |
| 139 | $lowStock = Product::whereIn('stock_status', ['in_stock', 'on_backorder'])->where('stock', '>', 0)->where('stock', '<=', 5)->count(); |
| 140 | $outStock = Product::where(function ($q) { |
| 141 | $q->where('stock_status', 'out_of_stock')->orWhereNull('stock')->orWhere('stock', '<=', 0); |
| 142 | })->count(); |
| 143 | $others = max($totalProductsNow - $inStock - $lowStock - $outStock, 0); |
| 144 | |
| 145 | $data['stockInStock'] = $inStock; |
| 146 | $data['stockLow'] = $lowStock; |
| 147 | $data['stockOut'] = $outStock; |
| 148 | $data['stockOthers'] = $others; |
| 149 | $data['stockTotal'] = max($totalProductsNow, 1); |
| 150 | |
| 151 | // ── Top selling products (POS sale_details + website order_details) ─ |
| 152 | $posTop = $branchId |
| 153 | ? collect() |
| 154 | : SaleDetalis::select('product_id', DB::raw('SUM(qty) as qty_sold'), DB::raw('SUM(qty*selling_price) as revenue')) |
| 155 | ->groupBy('product_id')->get()->keyBy('product_id'); |
| 156 | |
| 157 | $webTop = OrderDetails::join('stocks', 'order_details.stock_id', '=', 'stocks.id') |
| 158 | ->when($branchId, fn ($query) => $query->where('order_details.branch_id', $branchId)) |
| 159 | ->select('stocks.product_id', DB::raw('SUM(order_details.qty) as qty_sold'), DB::raw('SUM(order_details.total) as revenue')) |
| 160 | ->groupBy('stocks.product_id')->get()->keyBy('product_id'); |
| 161 | |
| 162 | $merged = []; |
| 163 | foreach ($posTop as $pid => $row) { |
| 164 | $merged[$pid] = ['qty' => (float) $row->qty_sold, 'revenue' => (float) $row->revenue]; |
| 165 | } |
| 166 | foreach ($webTop as $pid => $row) { |
| 167 | if (! isset($merged[$pid])) { |
| 168 | $merged[$pid] = ['qty' => 0.0, 'revenue' => 0.0]; |
| 169 | } |
| 170 | $merged[$pid]['qty'] += (float) $row->qty_sold; |
| 171 | $merged[$pid]['revenue'] += (float) $row->revenue; |
| 172 | } |
| 173 | uasort($merged, fn ($a, $b) => $b['revenue'] <=> $a['revenue']); |
| 174 | $topIds = array_slice(array_keys($merged), 0, 5); |
| 175 | $topProducts = Product::whereIn('id', $topIds)->get()->keyBy('id'); |
| 176 | |
| 177 | $data['topSellingProducts'] = collect($topIds)->map(function ($id) use ($merged, $topProducts) { |
| 178 | $p = $topProducts->get($id); |
| 179 | if (! $p) { |
| 180 | return null; |
| 181 | } |
| 182 | |
| 183 | return (object) [ |
| 184 | 'product' => $p, |
| 185 | 'qty' => $merged[$id]['qty'], |
| 186 | 'revenue' => $merged[$id]['revenue'], |
| 187 | ]; |
| 188 | })->filter()->values(); |
| 189 | |
| 190 | // ── Recent transactions (Sales + Purchases + Payments, merged) ───── |
| 191 | $recentSales = ($branchId ? collect() : Sale::with(['customer', 'sale_payment'])->latest()->take(5)->get())->map(fn ($s) => (object) [ |
| 192 | 'icon' => 'fa-cash-register', |
| 193 | 'color' => '#22c55e', |
| 194 | 'title' => 'Sale #' . $s->id, |
| 195 | 'sub' => optional($s->customer)->name ?? 'Walking Customer', |
| 196 | 'amount' => optional($s->sale_payment)->total ?? 0, |
| 197 | 'time' => $s->created_at, |
| 198 | ]); |
| 199 | $recentPurchases = $purchases()->with(['supplier', 'purchase_payment'])->latest()->take(5)->get()->map(fn ($p) => (object) [ |
| 200 | 'icon' => 'fa-truck-loading', |
| 201 | 'color' => '#3b82f6', |
| 202 | 'title' => 'Purchase #' . $p->id, |
| 203 | 'sub' => optional($p->supplier)->name ?? 'Supplier', |
| 204 | 'amount' => optional($p->purchase_payment)->total ?? 0, |
| 205 | 'time' => $p->created_at, |
| 206 | ]); |
| 207 | $recentPayments = $payments()->latest()->take(5)->get()->map(fn ($pay) => (object) [ |
| 208 | 'icon' => 'fa-hand-holding-usd', |
| 209 | 'color' => '#f59e0b', |
| 210 | 'title' => 'Payment Received', |
| 211 | 'sub' => 'Order #' . $pay->order_id, |
| 212 | 'amount' => $pay->paid, |
| 213 | 'time' => $pay->created_at, |
| 214 | ]); |
| 215 | |
| 216 | $data['recentTransactions'] = $recentSales->concat($recentPurchases)->concat($recentPayments) |
| 217 | ->sortByDesc('time')->take(6)->values(); |
| 218 | |
| 219 | // ── Recent customers with outstanding due ─────────────────────────── |
| 220 | $recentCustomerIds = $branchId |
| 221 | ? $orders()->orderByDesc('created_at')->pluck('customer_id')->unique()->take(6)->values() |
| 222 | : Sale::orderByDesc('created_at')->pluck('customer_id')->unique()->take(6)->values(); |
| 223 | $customersById = Customer::whereIn('id', $recentCustomerIds)->get()->keyBy('id'); |
| 224 | |
| 225 | $data['recentCustomers'] = $recentCustomerIds->map(function ($id) use ($branchId, $customersById, $orders) { |
| 226 | $c = $customersById->get($id); |
| 227 | if (! $c) { |
| 228 | return null; |
| 229 | } |
| 230 | $due = $branchId |
| 231 | ? (float) $orders()->where('customer_id', $id)->sum('due') |
| 232 | : (float) SalePayment::whereHas('sale', fn ($q) => $q->where('customer_id', $id))->sum('due'); |
| 233 | |
| 234 | return (object) ['customer' => $c, 'due' => $due]; |
| 235 | })->filter()->values(); |
| 236 | |
| 237 | // ── Recent orders table (kept) ────────────────────────────────────── |
| 238 | $data['recentOrders'] = $orders()->with('customer')->latest()->take(8)->get(); |
| 239 | |
| 240 | // ── Low stock products (kept) ─────────────────────────────────────── |
| 241 | $data['lowStockProducts'] = Product::where('stock_status', 'out_of_stock') |
| 242 | ->orWhere('stock', '<=', 5) |
| 243 | ->latest() |
| 244 | ->take(6) |
| 245 | ->get(); |
| 246 | |
| 247 | // ── Desktop widgets (replaces the old static "CashBook Report" mockup — |
| 248 | // every figure below is computed, not fabricated; each accounting/ |
| 249 | // manufacturing-specific piece falls back to something real from |
| 250 | // Orders/Purchases when that feature is off, per §Phase 10 spec. ─────── |
| 251 | $accountingOn = (bool) feature('accounting'); |
| 252 | $accounting = $accountingOn ? app(AccountingService::class) : null; |
| 253 | $defaultAccount = $accountingOn ? $accounting->defaultAccount(branchId: $branchId) : null; |
| 254 | |
| 255 | $bigStats = []; |
| 256 | if ($defaultAccount) { |
| 257 | $bigStats[] = ['label' => 'Opening Balance', 'value' => money($accounting->balance($defaultAccount, $monthStart->copy()->subDay())), 'caption' => 'As of ' . $monthStart->format('d M Y'), 'icon' => 'fa-wallet', 'c' => 'indigo']; |
| 258 | } |
| 259 | $bigStats[] = ['label' => 'This Month Sales', 'value' => money($monthSales), 'caption' => $monthStart->format('d M') . ' – ' . now()->format('d M Y'), 'icon' => 'fa-long-arrow-alt-down', 'c' => 'green', 'delta' => $pct($monthSales, $lastMonthSales)]; |
| 260 | $bigStats[] = ['label' => 'This Month Purchase', 'value' => money($monthPurchase), 'caption' => $monthStart->format('d M') . ' – ' . now()->format('d M Y'), 'icon' => 'fa-long-arrow-alt-up', 'c' => 'red', 'delta' => $pct($monthPurchase, $lastMonthPurchase)]; |
| 261 | if ($defaultAccount) { |
| 262 | $bigStats[] = ['label' => 'Balance Now', 'value' => money($accounting->balance($defaultAccount)), 'caption' => 'As of today', 'icon' => 'fa-piggy-bank', 'c' => 'purple']; |
| 263 | } else { |
| 264 | $bigStats[] = ['label' => 'Total Due', 'value' => money($data['totalDue']), 'caption' => 'Across all orders', 'icon' => 'fa-piggy-bank', 'c' => 'purple']; |
| 265 | } |
| 266 | $data['bigStats'] = $bigStats; |
| 267 | |
| 268 | $miniStats = [ |
| 269 | ['label' => 'Orders Today', 'value' => $orders()->whereDate('created_at', $today)->count(), 'icon' => 'fa-receipt', 'c' => 'indigo'], |
| 270 | ['label' => 'Total Products', 'value' => $totalProductsNow, 'icon' => 'fa-box-open', 'c' => 'green'], |
| 271 | ['label' => 'Low Stock Alerts', 'value' => $lowStock, 'icon' => 'fa-exclamation-triangle', 'c' => 'red'], |
| 272 | ['label' => 'Payment Methods', 'value' => app(PaymentManager::class)->enabled()->count(), 'icon' => 'fa-credit-card', 'c' => 'indigo'], |
| 273 | ]; |
| 274 | if (feature('manufacturing')) { |
| 275 | $miniStats[] = ['label' => 'Open Production Orders', 'value' => ProductionOrder::whereIn('status', ['Draft', 'Planned', 'InProgress'])->when($branchId, fn ($query) => $query->where('branch_id', $branchId))->count(), 'icon' => 'fa-industry', 'c' => 'amber']; |
| 276 | } |
| 277 | if ($accountingOn) { |
| 278 | $miniStats[] = ['label' => 'Active Accounts', 'value' => Account::where('is_active', true)->count(), 'icon' => 'fa-wallet', 'c' => 'indigo']; |
| 279 | } |
| 280 | $data['miniStats'] = $miniStats; |
| 281 | |
| 282 | // Month-to-date cash flow, day by day — real ledger data when |
| 283 | // Accounting is on (and an account is set up), otherwise the same |
| 284 | // Sales-vs-Purchase figures the rest of this dashboard already uses. |
| 285 | $flowLabels = []; |
| 286 | $flowIn = []; |
| 287 | $flowOut = []; |
| 288 | $daysSoFar = now()->day; |
| 289 | |
| 290 | if ($defaultAccount) { |
| 291 | $ledgerByDay = []; |
| 292 | foreach ($accounting->ledger($defaultAccount, $monthStart, now()) as $row) { |
| 293 | $d = (int) $row['transaction']->date->format('j'); |
| 294 | $signed = $row['transaction']->signedAmountFor($defaultAccount->id); |
| 295 | $ledgerByDay[$d]['in'] = ($ledgerByDay[$d]['in'] ?? 0) + max($signed, 0); |
| 296 | $ledgerByDay[$d]['out'] = ($ledgerByDay[$d]['out'] ?? 0) + max(-$signed, 0); |
| 297 | } |
| 298 | for ($d = 1; $d <= $daysSoFar; $d++) { |
| 299 | $flowLabels[] = (string) $d; |
| 300 | $flowIn[] = round($ledgerByDay[$d]['in'] ?? 0, 2); |
| 301 | $flowOut[] = round($ledgerByDay[$d]['out'] ?? 0, 2); |
| 302 | } |
| 303 | } else { |
| 304 | for ($d = 1; $d <= $daysSoFar; $d++) { |
| 305 | $day = $monthStart->copy()->addDays($d - 1); |
| 306 | $flowLabels[] = (string) $d; |
| 307 | $daySales = (float) $orders()->whereDate('created_at', $day)->sum('total') |
| 308 | + ($branchId ? 0.0 : (float) SalePayment::whereHas('sale', fn ($q) => $q->whereDate('created_at', $day))->sum('total')); |
| 309 | $dayPurchase = (float) $purchasePayments()->whereHas('purchase', fn ($q) => $q->whereDate('created_at', $day))->sum('total'); |
| 310 | $flowIn[] = round($daySales, 2); |
| 311 | $flowOut[] = round($dayPurchase, 2); |
| 312 | } |
| 313 | } |
| 314 | $data['flowLabels'] = $flowLabels; |
| 315 | $data['flowIn'] = $flowIn; |
| 316 | $data['flowOut'] = $flowOut; |
| 317 | |
| 318 | // Category summary — real Accounting categories this month when the |
| 319 | // feature is on, otherwise the Sales/Purchase split everyone already sees above. |
| 320 | if ($accountingOn) { |
| 321 | $catTotals = $transactions() |
| 322 | ->whereIn('type', ['income', 'expense']) |
| 323 | ->where('date', '>=', $monthStart) |
| 324 | ->whereNull('reversed_at') |
| 325 | ->with('category') |
| 326 | ->get() |
| 327 | ->groupBy(fn ($t) => $t->category_id ?: ('_' . $t->type)) |
| 328 | ->map(fn ($rows) => [ |
| 329 | 'label' => $rows->first()->category->name ?? ucfirst($rows->first()->type), |
| 330 | 'type' => $rows->first()->type, |
| 331 | 'amt' => (float) $rows->sum('amount'), |
| 332 | ]); |
| 333 | $catSum = $catTotals->sum('amt') ?: 1; |
| 334 | $data['categorySummary'] = $catTotals->map(fn ($c) => $c + ['pct' => round($c['amt'] / $catSum * 100)]) |
| 335 | ->sortByDesc('amt')->take(6)->values(); |
| 336 | } else { |
| 337 | $catSum = ($monthSales + $monthPurchase) ?: 1; |
| 338 | $data['categorySummary'] = collect([ |
| 339 | ['label' => 'Sales', 'type' => 'income', 'amt' => $monthSales, 'pct' => round($monthSales / $catSum * 100)], |
| 340 | ['label' => 'Purchase', 'type' => 'expense', 'amt' => $monthPurchase, 'pct' => round($monthPurchase / $catSum * 100)], |
| 341 | ]); |
| 342 | } |
| 343 | |
| 344 | // Payment method mix — real gateway usage this month (empty if nothing paid online yet). |
| 345 | $paletteColors = ['#22c55e', '#3b82f6', '#8b5cf6', '#ef4444', '#f59e0b', '#06b6d4']; |
| 346 | $paymentRows = $payments()->where('created_at', '>=', $monthStart)->where('status', PaymentStatus::Paid->value) |
| 347 | ->selectRaw('payment_type, SUM(paid) as total, COUNT(*) as cnt') |
| 348 | ->groupBy('payment_type')->get(); |
| 349 | $paymentTotal = (float) $paymentRows->sum('total') ?: 1; |
| 350 | $data['paymentMix'] = $paymentRows->values()->map(fn ($row, $i) => [ |
| 351 | 'label' => ucfirst(str_replace('_', ' ', $row->payment_type)), |
| 352 | 'amt' => (float) $row->total, |
| 353 | 'pct' => round(((float) $row->total / $paymentTotal) * 100), |
| 354 | 'color' => $paletteColors[$i % count($paletteColors)], |
| 355 | ]); |
| 356 | $data['paymentTxnCount'] = (int) $paymentRows->sum('cnt'); |
| 357 | |
| 358 | // Top 5 expenses this month — only meaningful once Accounting is on. |
| 359 | $data['topExpenses'] = $accountingOn |
| 360 | ? $transactions()->where('type', 'expense')->whereNull('reversed_at')->where('date', '>=', $monthStart) |
| 361 | ->with('category')->orderByDesc('amount')->take(5)->get() |
| 362 | ->map(fn ($t) => ['label' => ($t->category->name ?? 'Expense') . ($t->party_name ? ' — ' . $t->party_name : ''), 'amt' => (float) $t->amount]) |
| 363 | : collect(); |
| 364 | |
| 365 | // Yesterday's cash in/out, so "Today's Cash Flow" can show a real % change. |
| 366 | $yesterday = $today->copy()->subDay(); |
| 367 | $yesterdayCashIn = (float) $payments()->whereDate('created_at', $yesterday)->sum('paid') |
| 368 | + ($branchId ? 0.0 : (float) SalePayment::whereHas('sale', fn ($q) => $q->whereDate('created_at', $yesterday))->sum('paid')); |
| 369 | $yesterdayCashOut = (float) $purchasePayments()->whereHas('purchase', fn ($q) => $q->whereDate('created_at', $yesterday))->sum('paid'); |
| 370 | $data['cashInPct'] = $pct($data['cashIn'], $yesterdayCashIn); |
| 371 | $data['cashOutPct'] = $pct($data['cashOut'], $yesterdayCashOut); |
| 372 | $data['netFlowPct'] = $pct($data['netFlow'], $yesterdayCashIn - $yesterdayCashOut); |
| 373 | |
| 374 | $data['accountingOn'] = $accountingOn; |
| 375 | $data['defaultAccount'] = $defaultAccount; |
| 376 | |
| 377 | return view('admin.dashboard.dashboard')->with($data); |
| 378 | } |
| 379 | } |