feat(dashboard): add price history, caching, and i18n

This commit is contained in:
2026-08-01 21:21:43 +02:00
parent 1cd905ca4a
commit 7fb49131c4
22 changed files with 177 additions and 30 deletions
@@ -5,6 +5,7 @@ namespace App\Features\Dashboard\Clients;
use App\Features\Dashboard\Contracts\PriceClient; use App\Features\Dashboard\Contracts\PriceClient;
use Exception; use Exception;
use Illuminate\Http\Client\Factory; use Illuminate\Http\Client\Factory;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Throwable; use Throwable;
@@ -14,14 +15,14 @@ class ZonneplanPriceClient implements PriceClient
private readonly Factory $http, private readonly Factory $http,
) {} ) {}
public function getElectricityPrices(): array public function getElectricityPrices(): Collection
{ {
return $this->get('/energy-prices/electricity/upcoming'); return collect($this->get('/energy-prices/electricity/upcoming'));
} }
public function getGasPrices(): array public function getGasPrices(): Collection
{ {
return $this->get('/energy-prices/gas/upcoming'); return collect($this->get('/energy-prices/gas/upcoming'));
} }
/** @return array<int, array<string, mixed>> */ /** @return array<int, array<string, mixed>> */
@@ -2,9 +2,11 @@
namespace App\Features\Dashboard\Contracts; namespace App\Features\Dashboard\Contracts;
use Illuminate\Support\Collection;
interface PriceClient interface PriceClient
{ {
public function getElectricityPrices(): array; public function getElectricityPrices(): Collection;
public function getGasPrices(): array; public function getGasPrices(): Collection;
} }
@@ -2,7 +2,6 @@
namespace App\Features\Dashboard\Controllers; namespace App\Features\Dashboard\Controllers;
use App\Features\Dashboard\Data\DashboardData;
use App\Features\Dashboard\Services\DashboardService; use App\Features\Dashboard\Services\DashboardService;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
@@ -10,6 +9,6 @@ class DashboardController extends Controller
{ {
public function index(DashboardService $dashboardService) public function index(DashboardService $dashboardService)
{ {
return inertia('dashboard::index', DashboardData::from($dashboardService->getDashboardData())); return $dashboardService->getDashboardData();
} }
} }
+12 -4
View File
@@ -2,16 +2,24 @@
namespace App\Features\Dashboard\Data; namespace App\Features\Dashboard\Data;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Data; use Spatie\LaravelData\Data;
class DashboardData extends Data class DashboardData extends Data
{ {
/** /**
* @property array<ElectricityData> $electricity * @property Collection<ElectricityData> $electricityHistory
* @property array<GasData> $gas * @property Collection<GasData> $gasHistory
*/ */
public function __construct( public function __construct(
public array $electricity, /** @var array<ElectricityData> */
public array $gas public Collection $electricityHistory,
public ?ElectricityData $electricityCheapest,
public ?ElectricityData $electricityDurable,
public ?ElectricityData $electricityPriciest,
/** @var array<ElectricityData> */
public Collection $gasHistory,
public ?GasData $gasCheapest,
public ?GasData $gasPriciest,
) {} ) {}
} }
@@ -7,6 +7,7 @@ use App\Features\Dashboard\Contracts\PriceRepository as ContractsPriceRepository
use App\Features\Dashboard\Data\ElectricityData; use App\Features\Dashboard\Data\ElectricityData;
use App\Features\Dashboard\Data\GasData; use App\Features\Dashboard\Data\GasData;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Override; use Override;
class ZonneplanPriceRepository implements ContractsPriceRepository class ZonneplanPriceRepository implements ContractsPriceRepository
@@ -18,7 +19,7 @@ class ZonneplanPriceRepository implements ContractsPriceRepository
#[Override] #[Override]
public function retrieveElectricity(): Collection public function retrieveElectricity(): Collection
{ {
$data = $this->client->getElectricityPrices(); $data = Cache::remember('electricity', 3600, fn () => $this->client->getElectricityPrices()->toArray());
return collect(ElectricityData::collect($data)); return collect(ElectricityData::collect($data));
} }
@@ -26,7 +27,7 @@ class ZonneplanPriceRepository implements ContractsPriceRepository
#[Override] #[Override]
public function retrieveGas(): Collection public function retrieveGas(): Collection
{ {
$data = $this->client->getGasPrices(); $data = Cache::remember('gas', 3600, fn () => $this->client->getGasPrices()->toArray());
return collect(GasData::collect($data)); return collect(GasData::collect($data));
} }
@@ -3,6 +3,7 @@
namespace App\Features\Dashboard\Services; namespace App\Features\Dashboard\Services;
use App\Features\Dashboard\Contracts\PriceRepository; use App\Features\Dashboard\Contracts\PriceRepository;
use App\Features\Dashboard\Data\DashboardData;
class DashboardService class DashboardService
{ {
@@ -10,11 +11,25 @@ class DashboardService
public PriceRepository $repository public PriceRepository $repository
) {} ) {}
public function getDashboardData(): array public function getDashboardData(): DashboardData
{ {
return [ $electricity = $this->repository->retrieveElectricity();
'electricity' => $this->repository->retrieveElectricity(), $eCheapest = $electricity->min('total_price_tax_included');
'gas' => $this->repository->retrieveGas(), $eDurable = $electricity->min('sustainability_score');
]; $ePriciest = $electricity->max('total_price_tax_included');
$gas = $this->repository->retrieveGas();
$gCheapest = $gas->min('total_price_tax_included');
$gPriciest = $gas->min('total_price_tax_included');
return new DashboardData(
$electricity,
$eCheapest,
$eDurable,
$ePriciest,
$gas,
$gCheapest,
$gPriciest
);
} }
} }
+30 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "Zonneplan", "name": "html",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
@@ -12,6 +12,7 @@
"axios": "^1.19.0", "axios": "^1.19.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"i18next": "^26.3.6",
"laravel-vite-plugin": "^3.1", "laravel-vite-plugin": "^3.1",
"tailwind-merge": "^3.2.0", "tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.1", "tailwindcss": "^4.1.1",
@@ -3958,6 +3959,34 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/i18next": {
"version": "26.3.6",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz",
"integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==",
"funding": [
{
"type": "individual",
"url": "https://www.locize.com/i18next"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
},
{
"type": "individual",
"url": "https://www.locize.com"
}
],
"license": "MIT",
"peerDependencies": {
"typescript": "^5 || ^6 || ^7"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+1
View File
@@ -36,6 +36,7 @@
"axios": "^1.19.0", "axios": "^1.19.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"i18next": "^26.3.6",
"laravel-vite-plugin": "^3.1", "laravel-vite-plugin": "^3.1",
"tailwind-merge": "^3.2.0", "tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.1", "tailwindcss": "^4.1.1",
+8
View File
@@ -1,5 +1,7 @@
import { createInertiaApp } from '@inertiajs/vue3'; import { createInertiaApp } from '@inertiajs/vue3';
import { DefineComponent } from 'vue'; import { DefineComponent } from 'vue';
import Layout from './layouts/Layout.vue';
import { init } from './lang/init.js';
const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
@@ -8,6 +10,9 @@ createInertiaApp({
progress: { progress: {
color: '#4B5563', color: '#4B5563',
}, },
layout(name, page) {
return Layout;
},
resolve(name) { resolve(name) {
const pages = import.meta.glob<DefineComponent>( const pages = import.meta.glob<DefineComponent>(
['./features/*/pages/*.vue', './pages/*.vue'], ['./features/*/pages/*.vue', './pages/*.vue'],
@@ -26,4 +31,7 @@ createInertiaApp({
return pages[`./pages/${name}.vue`]; return pages[`./pages/${name}.vue`];
}, },
async withApp(app, options) {
await init;
},
}); });
@@ -1,3 +1,15 @@
<script setup lang="ts"></script> <script setup lang="ts">
import { onMounted } from 'vue';
import useDashboardViewModel from '../viewmodels/DashboardViewModel';
<template></template> const model = useDashboardViewModel();
onMounted(() => {
model.getDashboardData();
});
</script>
<template>
<div>{{ model.isLoading }}</div>
<div>{{ model.dashboardData }}</div>
</template>
@@ -1,3 +1,22 @@
export default function DashboardViewModel() { import { create } from 'axios';
return {}; import { ref } from 'vue';
import DashboardData = App.Features.Dashboard.Data.DashboardData;
export default function useDashboardViewModel() {
const isLoading = ref<boolean>(false);
const dashboardData = ref<DashboardData>();
function getDashboardData() {
isLoading.value = true;
const client = create();
client
.get<DashboardData>('/api/energy-prices')
.then((res) => {
dashboardData.value = res.data;
})
.finally(() => (isLoading.value = false));
}
return { isLoading, dashboardData, getDashboardData };
} }
@@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>
<div></div>
</template>
+8 -3
View File
@@ -1,10 +1,15 @@
declare namespace App { declare namespace App {
namespace Features { namespace Features {
namespace Dashboard { namespace Dashboard {
namespace DTO { namespace Data {
export type DashboardData = { export type DashboardData = {
electricity: App.Features.Dashboard.DTO.ElectricityData; electricityHistory: App.Features.Dashboard.Data.ElectricityData[];
gas: App.Features.Dashboard.DTO.GasData; electricityCheapest: App.Features.Dashboard.Data.ElectricityData | null;
electricityDurable: App.Features.Dashboard.Data.ElectricityData | null;
electricityPriciest: App.Features.Dashboard.Data.ElectricityData | null;
gasHistory: App.Features.Dashboard.Data.ElectricityData[];
gasCheapest: App.Features.Dashboard.Data.GasData | null;
gasPriciest: App.Features.Dashboard.Data.GasData | null;
}; };
export type ElectricityData = { export type ElectricityData = {
startDate: string; startDate: string;
@@ -1,3 +1,3 @@
{ {
"generated.d.ts": "237c6a8a14bec8d9c63d81a00b842cf6" "generated.d.ts": "63b1a5a3410956e9dfd29ea3c90abc71"
} }
+7
View File
@@ -0,0 +1,7 @@
import i18next from 'i18next';
import translations from './translations';
export const init = i18next.init({
lng: 'nl',
resources: translations,
});
+5
View File
@@ -0,0 +1,5 @@
import nl from "./nl";
export default {
nl
}
@@ -0,0 +1,3 @@
{
"title": "Overzicht"
}
@@ -0,0 +1,5 @@
import dashboard from './dashboard.json';
export default {
dashboard,
};
+19
View File
@@ -0,0 +1,19 @@
<script setup lang="ts">
import { t } from 'i18next';
import dashboard from '@/routes/dashboard';
import history from '@/routes/history';
</script>
<template>
<nav class="fixed top-0 left-0 w-40">
<ul>
<li>
<a :href="dashboard.index().url">{{ t('dashboard.title') }}</a>
</li>
<li>
<a :href="history.index().url">{{ t('history.title') }}</a>
</li>
</ul>
</nav>
<div class="absolute top-0 left-40"><slot /></div>
</template>
+3 -1
View File
@@ -3,4 +3,6 @@
use App\Features\Dashboard\Controllers\DashboardController; use App\Features\Dashboard\Controllers\DashboardController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get('/energy-prices', [DashboardController::class, 'index'])->name('dashboard.index'); Route::name('api.')->group(function () {
Route::get('/energy-prices', [DashboardController::class, 'index'])->name('dashboard.index');
});
+2 -1
View File
@@ -2,4 +2,5 @@
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::inertia('/', 'dashboard::index'); Route::inertia('/', 'dashboard::index')->name('dashboard.index');
Route::inertia('/history', 'history::index')->name('history.index');