View Model Implementation This is how the Laravel view model is used with the spares dashboard to remove all the logic from the blade view ### The DashboardViewModel ```` 'inventory','label'=>'Adjustments Sync','run_at'=>'14 Oct 2025, 12:30','server_name'=>'maximo01'], * ... * ] */ public array $logs = []; private APIStorageRepository $api; private bool $useCache; private int $cacheMinutes; public function __construct(APIStorageRepository $api = null, ?bool $useCache = null, int $cacheMinutes = 60) { $this->api = $api ?? new APIStorageRepository(); $this->useCache = $useCache ?? (bool)settings()->group('maximo')->get('dashboard_cache', false); $this->cacheMinutes = $cacheMinutes; } /** * Build the data ready fro the view * @return $this * @throws Throwable * @throws InvalidArgumentException */ public function build(): self { $start = microtime(true); $apiToken = (string)$this->api->getApiToken(); $params = []; // ------- Returns ------- $this->returns = $this->fetch('/api/v1/returns/index', $apiToken, $params, 'returns'); $returnsByWeek = $this->groupByIsoWeek($this->returns, 'SRRTRDT', 'SRRUCST'); $this->returnsLabels = array_keys($returnsByWeek); $this->returnsValues = array_values($returnsByWeek); $this->totalPOValue = array_sum($this->returnsValues); // ------- Inventory ------- $this->inventory = $this->fetch('/api/v1/inventory/index', $apiToken, $params, 'inventory'); $this->totalInventoryValue = (float)(collect($this->inventory)->sum('SPACOST') ?: 0.0); $inventoryByWeek = $this->groupByIsoWeek($this->inventory, 'SPATRDT', 'SPACOST'); $this->inventoryLabels = array_keys($inventoryByWeek); $this->inventoryValues = array_values($inventoryByWeek); // ------- Issues ------- $this->issues = $this->fetch('/api/v1/issues/index', $apiToken, $params, 'issues'); $issuesByWeek = $this->groupByIsoWeek($this->issues, 'SPRTRDT', 'SPRUCST'); $this->issuesLabels = array_keys($issuesByWeek); $this->issuesValues = array_values($issuesByWeek); $this->totalIssuesValue = array_sum($this->issuesValues); // ------- Stock ------- $stock = $this->fetch('/api/v1/stock/index', $apiToken, $params, 'stock'); $stockLastMonth = $this->fetch('/api/v1/stock/index/last_month', $apiToken, $params, 'stock_last_month'); $totalStockNow = (float)(collect($stock)->sum('SPSRCST') ?: 0.0); $this->totalStockValueLastMonth = (float)(collect($stockLastMonth)->sum('SPSRCST') ?: 0.0); $this->totalStockValue = $totalStockNow - $this->totalStockValueLastMonth; $combinedStock = array_merge(is_array($stock) ? $stock : [], is_array($stockLastMonth) ? $stockLastMonth : []); $this->stock = $combinedStock; $stockByMonth = $this->groupByMonth($combinedStock, 'SPSTRDT', 'SPSRCST'); $this->stockLabels = array_keys($stockByMonth); $this->stockValues = array_values($stockByMonth); array_multisort($this->stockLabels, SORT_ASC, $this->stockValues); // ------- Top Tens (defensive) ------- $this->topTenIssues = $this->fetch('/api/v1/issues/index/top_ten', $apiToken, $params, 'top_ten_issues'); $this->topTenInventory = $this->fetch('/api/v1/inventory/index/top_ten', $apiToken, $params, 'top_ten_inventory'); // ------- Logs (with label mapping & formatted timestamps) ------- $labelMap = [ 'INVENTORY' => 'Adjustments Sync', 'ISSUES' => 'Item Usage Sync', 'RETURNS' => 'PO Analysis Sync', 'STOCK' => 'Movement Sync', ]; $this->logs = collect(['RETURNS', 'STOCK', 'ISSUES', 'INVENTORY']) ->map(function (string $type) use ($labelMap) { $raw = (array)$this->api->getRunLog('/api/v1/runlog/last', $type); return [ 'key' => strtolower($type), 'label' => $labelMap[$type] ?? e($type), 'run_at' => $this->formatDate($raw['run_at'] ?? null), 'server_name' => (string)($raw['server_name'] ?? 'N/A'), ]; }) ->values() ->toArray(); $this->grandTotal = $this->totalPOValue + $this->totalIssuesValue + $this->totalInventoryValue + $this->totalStockValue; if (app()->environment(['local', 'dev', 'qat'])) { $runtime = round(microtime(true) - $start, 3); Log::channel('dashboard')->info(emoji('orange') . "Dashboard ViewModel build() runtime: {$runtime} seconds"); } return $this; } /** * Returns all the variables as an array. * @return array */ public function toArray(): array { return [ 'totalPOValue' => $this->totalPOValue, 'totalInventoryValue' => $this->totalInventoryValue, 'totalIssuesValue' => $this->totalIssuesValue, 'totalStockValueLastMonth' => $this->totalStockValueLastMonth, 'totalStockValue' => $this->totalStockValue, 'grandTotal' => $this->grandTotal, 'inventory' => $this->inventory, 'inventoryLabels' => $this->inventoryLabels, 'inventoryValues' => $this->inventoryValues, 'stock' => $this->stock, 'stockLabels' => $this->stockLabels, 'stockValues' => $this->stockValues, 'issues' => $this->issues, 'issuesLabels' => $this->issuesLabels, 'issuesValues' => $this->issuesValues, 'returns' => $this->returns, 'returnsLabels' => $this->returnsLabels, 'returnsValues' => $this->returnsValues, 'topTenIssues' => $this->topTenIssues, 'topTenInventory' => $this->topTenInventory, 'logs' => $this->logs, ]; } // ----------------- helpers ----------------- /** * A combined log formatter * @param string $url * @param string $token * @param array $params * @param string $name * @return array|mixed */ private function fetch(string $url, string $token, array $params, string $name) { // normalize everything to strings for cache key safety $url = (string)$url; $name = (string)$name; $paramsJson = json_encode($params, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); $key = $name . '_' . md5($url . '|' . ($paramsJson ?? '[]')); if (!$this->useCache) { return $this->executeApi($token, $url, $params); } return Cache::remember($key, now()->addMinutes($this->cacheMinutes), function () use ($token, $url, $params) { return $this->executeApi($token, $url, $params); }); } /** * A common API executor * @param string $token * @param string $url * @param array $params * @return array * @throws InvalidArgumentException */ private function executeApi(string $token, string $url, array $params) { try { $data = APIStorageRepository::execute($token, $url, $params); // Always return an array so downstream code is predictable return is_array($data) ? $data : []; } catch (Throwable $e) { if (app()->environment(['local', 'dev', 'qat'])) { Log::channel('dashboard')->warning("API fetch failed for {$url}: {$e->getMessage()}"); } return []; } } /** ISO year-week to avoid cross-year collisions, e.g. '2025-W01'. */ private function groupByIsoWeek(array $data, string $dateField, string $valueField): array { return collect($data) ->filter(fn($i) => !empty($i[$dateField]) && is_numeric($i[$valueField] ?? null)) ->groupBy(function ($i) use ($dateField) { $d = Carbon::parse($i[$dateField]); return $d->format('o-\WW'); }) ->map(fn($items) => collect($items)->sum($valueField)) ->toArray(); } private function groupByMonth(array $data, string $dateField, string $valueField): array { return collect($data) ->filter(fn($i) => !empty($i[$dateField]) && is_numeric($i[$valueField] ?? null)) ->groupBy(fn($i) => Carbon::parse($i[$dateField])->format('Y-m')) ->map(fn($items) => collect($items)->sum($valueField)) ->toArray(); } /** * Nice helper for format dates * @param string|null $value * @return string */ private function formatDate(?string $value): string { if (empty($value)) { return 'N/A'; } try { return Carbon::parse($value)->format('d M Y, H:i'); } catch (Throwable $e) { return 'N/A'; } } } ```` And the new controller DashBoardController.php ```` build(); if (app()->environment(['local', 'dev', 'qat'])) { $useCache = (bool)settings()->group('maximo')->get('dashboard_cache', false); logger()->channel('dashboard')->info( $useCache ? emoji('green') . 'Using Cached Data' : emoji('red') . 'Not Using Cached Data' ); } return view('spares::Components.dashboard', $dashboardViewModel->toArray()); } } ```` This abstracts the data from the view and makes it far more maintainable and extendable.