From 254810b90a3db17474a036df21b354110f196116 Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Thu, 11 Jun 2026 17:52:58 +0100 Subject: [PATCH] nre layouts --- .../PlatformDashboardController.php | 111 +++++++++- .../resources/views/dashboard/index.blade.php | 206 +++++++++++------- composer.json | 9 + resources/js/app.js | 94 ++++++++ 4 files changed, 334 insertions(+), 86 deletions(-) diff --git a/Modules/Platform/app/Http/Controllers/PlatformDashboardController.php b/Modules/Platform/app/Http/Controllers/PlatformDashboardController.php index b9f13451..420ad985 100644 --- a/Modules/Platform/app/Http/Controllers/PlatformDashboardController.php +++ b/Modules/Platform/app/Http/Controllers/PlatformDashboardController.php @@ -3,6 +3,8 @@ namespace Modules\Platform\app\Http\Controllers; use App\Http\Controllers\Controller; +use Illuminate\Contracts\View\View; +use Illuminate\Support\Collection; use Modules\Platform\app\Models\Company; use Modules\Platform\app\Models\LicenseKey; use Modules\Platform\app\Models\Payment; @@ -14,17 +16,112 @@ class PlatformDashboardController extends Controller /** * Show the platform administration dashboard. */ - public function __invoke() + public function __invoke(): View { + $companies = Company::withCount(['users', 'warehouses', 'licenseKeys']) + ->latest() + ->limit(8) + ->get(); + + $companiesCount = Company::count(); + $activeCompaniesCount = Company::where('status', 'active')->count(); + $licensesCount = LicenseKey::count(); + $warehousesCount = Warehouse::count(); + $pricesCount = PlatformPrice::count(); + return view('platform::dashboard.index', [ - 'companiesCount' => Company::count(), - 'activeCompaniesCount' => Company::where('status', 'active')->count(), - 'licensesCount' => LicenseKey::count(), + 'companiesCount' => $companiesCount, + 'activeCompaniesCount' => $activeCompaniesCount, + 'licensesCount' => $licensesCount, 'paymentsTotal' => Payment::where('status', 'paid')->sum('amount'), - 'warehousesCount' => Warehouse::count(), - 'pricesCount' => PlatformPrice::count(), + 'warehousesCount' => $warehousesCount, + 'pricesCount' => $pricesCount, 'prices' => PlatformPrice::query()->ordered()->limit(3)->get(), - 'companies' => Company::withCount(['users', 'warehouses', 'licenseKeys'])->latest()->limit(8)->get(), + 'companies' => $companies, + + 'companyUsageChartLabels' => $companies->isNotEmpty() + ? $companies->pluck('name')->values()->all() + : ['MPBA', 'Abbott Demo', 'North Hub', 'South Hub'], + + 'companyUsageChartSeries' => $companies->isNotEmpty() + ? $this->companyUsageChartSeries($companies) + : $this->exampleCompanyUsageChartSeries(), + + 'platformMixChartLabels' => ['Companies', 'Warehouses', 'Prices', 'Licences'], + + 'platformMixChartValues' => $this->hasPlatformMixData( + $companiesCount, + $warehousesCount, + $pricesCount, + $licensesCount + ) + ? [$companiesCount, $warehousesCount, $pricesCount, $licensesCount] + : [4, 11, 4, 9], + + 'companyStatusChartLabels' => ['Active', 'Inactive'], + + 'companyStatusChartValues' => $companiesCount > 0 + ? [$activeCompaniesCount, max($companiesCount - $activeCompaniesCount, 0)] + : [3, 1], ]); } + + /** + * Build the company usage chart series. + * + * @param Collection $companies + * @return array}> + */ + private function companyUsageChartSeries(Collection $companies): array + { + return [ + [ + 'label' => 'Users', + 'data' => $companies->pluck('users_count')->values()->all(), + ], + [ + 'label' => 'Warehouses', + 'data' => $companies->pluck('warehouses_count')->values()->all(), + ], + [ + 'label' => 'Licences', + 'data' => $companies->pluck('license_keys_count')->values()->all(), + ], + ]; + } + + /** + * Build example company usage chart series for an empty platform. + * + * @return array}> + */ + private function exampleCompanyUsageChartSeries(): array + { + return [ + [ + 'label' => 'Users', + 'data' => [14, 9, 6, 4], + ], + [ + 'label' => 'Warehouses', + 'data' => [5, 3, 2, 1], + ], + [ + 'label' => 'Licences', + 'data' => [4, 2, 2, 1], + ], + ]; + } + + /** + * Determine whether the platform mix chart has real data. + */ + private function hasPlatformMixData( + int $companiesCount, + int $warehousesCount, + int $pricesCount, + int $licensesCount + ): bool { + return ($companiesCount + $warehousesCount + $pricesCount + $licensesCount) > 0; + } } diff --git a/Modules/Platform/resources/views/dashboard/index.blade.php b/Modules/Platform/resources/views/dashboard/index.blade.php index afa2ebfa..0cc87bc6 100644 --- a/Modules/Platform/resources/views/dashboard/index.blade.php +++ b/Modules/Platform/resources/views/dashboard/index.blade.php @@ -3,13 +3,14 @@ +
@foreach ([['Companies', $companiesCount], ['Active', $activeCompaniesCount], ['Warehouses', $warehousesCount], ['Prices', $pricesCount], ['Licences', $licensesCount]] as [$label, $value])
@@ -19,87 +20,131 @@ @endforeach
-
-
- + -
- @forelse ($prices as $price) - @php - $isBasic = $price->code === 'basic'; - $isAdvanced = $price->code === 'advanced'; - $headerClass = $isAdvanced - ? 'from-cyan-500 to-blue-600' - : ($isBasic ? 'from-slate-700 to-slate-950' : 'from-indigo-600 to-violet-700'); - $features = array_slice($price->featureRows(), 0, 3); - @endphp + {{-- Recent Companies --}} +
+
+
+

Companies

+

Recent companies

+

+ Tenant isolation is enforced by company ownership on every warehouse record. +

+
-
-
-
-
-

{{ $price->badge_label ?: 'Plan' }}

-

{{ $price->name }}

-
- - - -
-

{{ $price->strapline }}

-
- -
-
- {{ $price->formatAmount($price->monthly_amount) }} - / month -
-

{{ $price->description }}

- -
    - @foreach ($features as $feature) -
  • - - {{ $feature }} -
  • - @endforeach -
-
-
- @empty -
- No platform pricing plans have been published yet. -
- @endforelse -
-
- -
-
-
-

Recent companies

-

Tenant isolation is enforced by company ownership on every warehouse record.

+ + + Manage Companies +
- - Manage companies - -
-
- - + +
+
+ @@ -107,8 +152,9 @@ - - + + + @foreach ($companies as $company) @@ -118,9 +164,11 @@ @endforeach - -
Company UsersLicences Status
{{ $company->name }}{{ $company->is_system_company ? 'System' : ucfirst($company->status) }}
-
+ + + +
+ diff --git a/composer.json b/composer.json index 8570907e..6421e4a9 100644 --- a/composer.json +++ b/composer.json @@ -96,6 +96,15 @@ "npm install", "npm run build" ], + "rebuild": [ + + "npm run build", + + "@php artisan optimize:clear", + + "@php artisan octane:reload" + + ], "dev": [ "Composer\\Config::disableProcessTimeout", "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve --host=localhost\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others" diff --git a/resources/js/app.js b/resources/js/app.js index be6fdaea..5421815b 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1 +1,95 @@ import '@fortawesome/fontawesome-free/css/all.min.css'; +import Chart from 'chart.js/auto'; + +const chartColours = [ + 'rgb(8, 145, 178)', + 'rgb(37, 99, 235)', + 'rgb(79, 70, 229)', + 'rgb(14, 116, 144)', +]; + +function parseJsonAttribute(element, attribute, fallback = []) { + try { + return JSON.parse(element.getAttribute(attribute) || JSON.stringify(fallback)); + } catch (error) { + return fallback; + } +} + +function renderWmsCharts() { + document.querySelectorAll('[data-wms-chart]').forEach((canvas) => { + if (canvas.dataset.rendered === 'true') { + return; + } + + const type = canvas.getAttribute('data-wms-chart'); + const labels = parseJsonAttribute(canvas, 'data-labels'); + const series = parseJsonAttribute(canvas, 'data-series'); + const values = parseJsonAttribute(canvas, 'data-values'); + + const datasets = series.length + ? series.map((item, index) => ({ + label: item.label, + data: item.data, + backgroundColor: chartColours[index % chartColours.length], + borderColor: chartColours[index % chartColours.length], + borderWidth: 2, + borderRadius: 8, + })) + : [{ + label: 'Total', + data: values, + backgroundColor: chartColours, + borderColor: 'rgb(255, 255, 255)', + borderWidth: 2, + }]; + + new Chart(canvas, { + type, + data: { + labels, + datasets, + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + labels: { + color: document.documentElement.classList.contains('dark') ? '#e4e4e7' : '#3f3f46', + boxWidth: 12, + boxHeight: 12, + }, + }, + }, + scales: type === 'doughnut' + ? {} + : { + x: { + ticks: { + color: document.documentElement.classList.contains('dark') ? '#d4d4d8' : '#52525b', + }, + grid: { + color: 'rgba(113, 113, 122, 0.18)', + }, + }, + y: { + beginAtZero: true, + ticks: { + precision: 0, + color: document.documentElement.classList.contains('dark') ? '#d4d4d8' : '#52525b', + }, + grid: { + color: 'rgba(113, 113, 122, 0.18)', + }, + }, + }, + }, + }); + + canvas.dataset.rendered = 'true'; + }); +} + +document.addEventListener('DOMContentLoaded', renderWmsCharts); +document.addEventListener('livewire:navigated', renderWmsCharts);