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 Exception;
use Illuminate\Http\Client\Factory;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Throwable;
@@ -14,14 +15,14 @@ class ZonneplanPriceClient implements PriceClient
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>> */
@@ -2,9 +2,11 @@
namespace App\Features\Dashboard\Contracts;
use Illuminate\Support\Collection;
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;
use App\Features\Dashboard\Data\DashboardData;
use App\Features\Dashboard\Services\DashboardService;
use App\Http\Controllers\Controller;
@@ -10,6 +9,6 @@ class DashboardController extends Controller
{
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;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Data;
class DashboardData extends Data
{
/**
* @property array<ElectricityData> $electricity
* @property array<GasData> $gas
* @property Collection<ElectricityData> $electricityHistory
* @property Collection<GasData> $gasHistory
*/
public function __construct(
public array $electricity,
public array $gas
/** @var array<ElectricityData> */
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\GasData;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Override;
class ZonneplanPriceRepository implements ContractsPriceRepository
@@ -18,7 +19,7 @@ class ZonneplanPriceRepository implements ContractsPriceRepository
#[Override]
public function retrieveElectricity(): Collection
{
$data = $this->client->getElectricityPrices();
$data = Cache::remember('electricity', 3600, fn () => $this->client->getElectricityPrices()->toArray());
return collect(ElectricityData::collect($data));
}
@@ -26,7 +27,7 @@ class ZonneplanPriceRepository implements ContractsPriceRepository
#[Override]
public function retrieveGas(): Collection
{
$data = $this->client->getGasPrices();
$data = Cache::remember('gas', 3600, fn () => $this->client->getGasPrices()->toArray());
return collect(GasData::collect($data));
}
@@ -3,6 +3,7 @@
namespace App\Features\Dashboard\Services;
use App\Features\Dashboard\Contracts\PriceRepository;
use App\Features\Dashboard\Data\DashboardData;
class DashboardService
{
@@ -10,11 +11,25 @@ class DashboardService
public PriceRepository $repository
) {}
public function getDashboardData(): array
public function getDashboardData(): DashboardData
{
return [
'electricity' => $this->repository->retrieveElectricity(),
'gas' => $this->repository->retrieveGas(),
];
$electricity = $this->repository->retrieveElectricity();
$eCheapest = $electricity->min('total_price_tax_included');
$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,
"requires": true,
"packages": {
@@ -12,6 +12,7 @@
"axios": "^1.19.0",
"clsx": "^2.1.1",
"concurrently": "^9.0.1",
"i18next": "^26.3.6",
"laravel-vite-plugin": "^3.1",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.1",
@@ -3958,6 +3959,34 @@
"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": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+1
View File
@@ -36,6 +36,7 @@
"axios": "^1.19.0",
"clsx": "^2.1.1",
"concurrently": "^9.0.1",
"i18next": "^26.3.6",
"laravel-vite-plugin": "^3.1",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.1",
+8
View File
@@ -1,5 +1,7 @@
import { createInertiaApp } from '@inertiajs/vue3';
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';
@@ -8,6 +10,9 @@ createInertiaApp({
progress: {
color: '#4B5563',
},
layout(name, page) {
return Layout;
},
resolve(name) {
const pages = import.meta.glob<DefineComponent>(
['./features/*/pages/*.vue', './pages/*.vue'],
@@ -26,4 +31,7 @@ createInertiaApp({
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() {
return {};
import { create } from 'axios';
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 {
namespace Features {
namespace Dashboard {
namespace DTO {
namespace Data {
export type DashboardData = {
electricity: App.Features.Dashboard.DTO.ElectricityData;
gas: App.Features.Dashboard.DTO.GasData;
electricityHistory: App.Features.Dashboard.Data.ElectricityData[];
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 = {
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 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;
Route::inertia('/', 'dashboard::index');
Route::inertia('/', 'dashboard::index')->name('dashboard.index');
Route::inertia('/history', 'history::index')->name('history.index');