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
<?php
namespace Modules\Spares\ViewModels;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Modules\Spares\Repositories\API\APIStorageRepository;
use Psr\SimpleCache\InvalidArgumentException;
use Throwable;
final class DashboardViewModel
{
public array $returns = [];
public array $returnsLabels = [];
public array $returnsValues = [];
public float $totalPOValue = 0.0;
public array $inventory = [];
public array $inventoryLabels = [];
public array $inventoryValues = [];
public float $totalInventoryValue = 0.0;
public array $issues = [];
public array $issuesLabels = [];
public array $issuesValues = [];
public float $totalIssuesValue = 0.0;
public array $stock = [];
public array $stockLabels = [];
public array $stockValues = [];
public float $totalStockValue = 0.0;
public float $totalStockValueLastMonth = 0.0;
public float $grandTotal = 0.0;
public array $topTenIssues = [];
public array $topTenInventory = [];
/**
* Logs are normalized for the Blade:
* [
* ['key'=>'inventory','label'=>'<i ...>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' => '<i class="fa-fw fas fa-sliders-h mr-2"></i>Adjustments Sync',
'ISSUES' => '<i class="fas fa-exchange-alt mr-2"></i>Item Usage Sync',
'RETURNS' => '<i class="fa-fw fas fa-thermometer-full mr-2"></i>PO Analysis Sync',
'STOCK' => '<i class="fa-fw fas fa-warehouse mr-2"></i>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
<?php
namespace Modules\Spares\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\View\View;
use Modules\Spares\ViewModels\DashboardViewModel;
class DashboardController extends Controller
{
/**
* Main View Loader
* now uses a view model
* @return View
*
*/
public function index(): View
{
$dashboardViewModel = (new DashboardViewModel())->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.