320 lines
11 KiB
Markdown
Executable File
320 lines
11 KiB
Markdown
Executable File
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.
|