refactor(dashboard,history): replace ViewModels with Pinia stores and improve backend

This commit is contained in:
2026-08-05 09:23:18 +02:00
parent a0387126de
commit 90930cd223
29 changed files with 1372 additions and 237 deletions
+3
View File
@@ -1,10 +1,12 @@
import { createInertiaApp } from '@inertiajs/vue3';
import I18NextVue from 'i18next-vue';
import { createPinia } from 'pinia';
import type { DefineComponent } from 'vue';
import i18next from './lang/init';
import Layout from './layouts/Layout.vue';
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
const pinia = createPinia();
createInertiaApp({
title: (title) => (title ? `${title} - ${appName}` : appName),
@@ -33,6 +35,7 @@ createInertiaApp({
return pages[`./pages/${name}.vue`];
},
withApp(app, options) {
app.use(pinia);
app.use(I18NextVue, {
i18next,
...options,
@@ -13,7 +13,7 @@ defineProps<{
<div class="font-bold text-primary uppercase">{{ label }}</div>
<div class="flex flex-col gap-2">
<div class="text-4xl font-black">
&euro;{{ price?.toPrecision(2).toString().replace('.', ',') }}
&euro;{{ price?.toFixed(2).replace('.', ',') }}
</div>
<div class="text-sm text-black/50">
{{ moment.isMoment(time) ? time.format('DD-MM-YYYY HH:mm') : time }}
+90 -120
View File
@@ -3,14 +3,14 @@ import { t } from 'i18next';
import moment from 'moment';
import { onMounted } from 'vue';
import Price from '../components/Price.vue';
import useDashboardViewModel from '../viewmodels/DashboardViewModel';
import { useDashboardStore } from '@/stores/dashboard';
import Error from '@/components/Error.vue';
import Loading from '@/components/Loading.vue';
const model = useDashboardViewModel();
const store = useDashboardStore();
onMounted(() => {
model.getDashboardData();
store.fetchDashboardData();
});
</script>
@@ -18,124 +18,94 @@ onMounted(() => {
<h1 class="mb-4 text-3xl font-black text-primary">
{{ t('dashboard.title') }}
</h1>
<div v-if="model.isLoading"></div>
<div class="mb-4 flex flex-col gap-2" v-if="model.dashboardData.value">
<h2 class="text-2xl font-black text-primary">
{{ t('dashboard.electricity') }}
</h2>
<div class="flex flex-wrap gap-4">
<Price
v-if="
model.dashboardData.value?.electricity.totalPriceTaxIncluded
"
:label="t('dashboard.current_electricity_price')"
:price="
model.dashboardData.value?.electricity.totalPriceTaxIncluded
"
:time="moment(model.dashboardData.value?.electricity.startDate)"
/>
<Price
v-if="
model.dashboardData.value?.electricityCheapest
?.totalPriceTaxIncluded
"
:label="t('dashboard.cheapest_electricity_price')"
:price="
model.dashboardData.value?.electricityCheapest
?.totalPriceTaxIncluded
"
:time="
moment(
model.dashboardData.value?.electricityCheapest
?.startDate,
)
"
/>
<Price
v-if="
model.dashboardData.value?.electricityDurable
?.totalPriceTaxIncluded
"
:label="t('dashboard.durable_electricity_price')"
:price="
model.dashboardData.value?.electricityDurable
?.totalPriceTaxIncluded
"
:time="
moment(
model.dashboardData.value?.electricityDurable
?.startDate,
)
"
/>
<Price
v-if="
model.dashboardData.value?.electricityPriciest
?.totalPriceTaxIncluded
"
:label="t('dashboard.priciest_electricity_price')"
:price="
model.dashboardData.value?.electricityPriciest
?.totalPriceTaxIncluded
"
:time="
moment(
model.dashboardData.value?.electricityPriciest
?.startDate,
)
"
/>
<Loading v-if="store.isLoading" />
<template v-else-if="store.dashboardData">
<div class="mb-4 flex flex-col gap-2">
<h2 class="text-2xl font-black text-primary">
{{ t('dashboard.electricity') }}
</h2>
<div class="flex flex-wrap gap-4">
<Price
v-if="
store.dashboardData.electricity?.totalPriceTaxIncluded
"
:label="t('dashboard.current_electricity_price')"
:price="
store.dashboardData.electricity.totalPriceTaxIncluded
"
:time="moment(store.dashboardData.electricity.startDate)"
/>
<Price
v-if="
store.dashboardData.electricityCheapest
?.totalPriceTaxIncluded
"
:label="t('dashboard.cheapest_electricity_price')"
:price="
store.dashboardData.electricityCheapest
.totalPriceTaxIncluded
"
:time="
moment(
store.dashboardData.electricityCheapest.startDate,
)
"
/>
<Price
v-if="
store.dashboardData.electricityDurable
?.totalPriceTaxIncluded
"
:label="t('dashboard.durable_electricity_price')"
:price="
store.dashboardData.electricityDurable
.totalPriceTaxIncluded
"
:time="
moment(store.dashboardData.electricityDurable.startDate)
"
/>
<Price
v-if="
store.dashboardData.electricityPriciest
?.totalPriceTaxIncluded
"
:label="t('dashboard.priciest_electricity_price')"
:price="
store.dashboardData.electricityPriciest
.totalPriceTaxIncluded
"
:time="
moment(
store.dashboardData.electricityPriciest.startDate,
)
"
/>
</div>
</div>
</div>
<div class="mb-4 flex flex-col gap-2" v-if="model.dashboardData.value">
<h2 class="text-2xl font-black text-primary">
{{ t('dashboard.gas') }}
</h2>
<div class="flex flex-wrap gap-4">
<Price
v-if="model.dashboardData.value?.gas.totalPriceTaxIncluded"
:label="t('dashboard.current_gas_price')"
:price="model.dashboardData.value?.gas.totalPriceTaxIncluded"
:time="
moment(model.dashboardData.value?.gas.startDate).format(
'DD-MM-YYYY',
)
"
/>
<Price
v-if="
model.dashboardData.value?.gasCheapest
?.totalPriceTaxIncluded
"
:label="t('dashboard.cheapest_gas_price')"
:price="
model.dashboardData.value?.gasCheapest
?.totalPriceTaxIncluded
"
:time="
moment(
model.dashboardData.value?.gasCheapest?.startDate,
).format('DD-MM-YYYY')
"
/>
<Price
v-if="
model.dashboardData.value?.gasPriciest
?.totalPriceTaxIncluded
"
:label="t('dashboard.priciest_gas_price')"
:price="
model.dashboardData.value?.gasPriciest
?.totalPriceTaxIncluded
"
:time="
moment(
model.dashboardData.value?.gasPriciest?.startDate,
).format('DD-MM-YYYY')
"
/>
<div class="mb-4 flex flex-col gap-2">
<h2 class="text-2xl font-black text-primary">
{{ t('dashboard.gas') }}
</h2>
<div class="flex flex-wrap gap-4">
<Price
v-if="store.dashboardData.gas?.totalPriceTaxIncluded"
:label="t('dashboard.current_gas_price')"
:price="store.dashboardData.gas.totalPriceTaxIncluded"
:time="
moment(store.dashboardData.gas.startDate).format(
'DD-MM-YYYY',
)
"
/>
</div>
</div>
</template>
<div
v-else-if="!store.isLoading && !store.dashboardData"
class="rounded-xl bg-gray-100 p-8 text-center text-gray-500"
>
{{ t('dashboard.no_data') }}
</div>
<Error :message="model.error.value" v-if="model.error.value" />
<Loading v-if="model.isLoading.value" />
<Error :message="store.error" v-if="store.error" />
</template>
@@ -1,26 +0,0 @@
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 error = ref<string>();
const dashboardData = ref<DashboardData>();
function getDashboardData() {
isLoading.value = true;
const client = create();
client
.get<DashboardData>('/api/energy-prices')
.then((res) => {
dashboardData.value = res.data;
})
.catch((err) => {
error.value = err.response.data.message;
})
.finally(() => (isLoading.value = false));
}
return { isLoading, dashboardData, error, getDashboardData };
}
@@ -29,6 +29,7 @@ export interface PricePoint {
const props = defineProps<{
data: PricePoint[];
color?: string;
}>();
const chartData = computed(() => ({
@@ -36,8 +37,10 @@ const chartData = computed(() => ({
datasets: [
{
data: props.data.map((d) => d.value),
borderColor: '#4ba66a',
backgroundColor: 'rgba(75, 166, 106, 0.1)',
borderColor: props.color || '#4ba66a',
backgroundColor: props.color
? `${props.color}1a`
: 'rgba(75, 166, 106, 0.1)',
fill: true,
tension: 0.4,
pointRadius: 2,
+86 -15
View File
@@ -1,37 +1,108 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import useHistoryViewModel from '../viewmodels/HistoryViewModel';
import { computed, onMounted, watch } from 'vue';
import moment from 'moment';
import { useHistoryStore } from '@/stores/history';
import Chart from '../components/Chart.vue';
import Loading from '@/components/Loading.vue';
import Error from '@/components/Error.vue';
import { t } from 'i18next';
const model = useHistoryViewModel();
const date = ref<Date>();
const store = useHistoryStore();
onMounted(() => {
model.getHistoryData(moment().format('YYYY-MM-DD'));
store.fetchHistoryData(moment().format('YYYY-MM-DD'));
});
watch(date, (value) => {
model.getHistoryData(moment(value).format('YYYY-MM-DD'));
});
watch(
() => store.selectedDate,
(value) => {
if (value) {
store.fetchHistoryData(value);
}
},
);
const chartData = computed(
const electricityChartData = computed(
() =>
model.historyData.value?.electricity
store.historyData?.electricity
.filter((e) => e.totalPriceTaxIncluded != null)
.map((e) => ({
label: moment(e.startDate).format('HH:mm'),
value: e.totalPriceTaxIncluded,
})) ?? [],
);
const gasChartData = computed(
() =>
store.historyData?.gas
.filter((g) => g.totalPriceTaxIncluded != null)
.map((g) => ({
label: moment(g.startDate).format('DD-MM-YYYY'),
value: g.totalPriceTaxIncluded,
})) ?? [],
);
</script>
<template>
<input type="date" v-model="date" />
<Loading v-if="model.isLoading.value" />
<Chart :data="chartData" v-if="chartData" />
<Error v-if="model.error.value" :message="model.error.value" />
<h1 class="mb-4 text-3xl font-black text-primary">
{{ t('history.title') }}
</h1>
<div class="mb-6">
<label
for="date-select"
class="mb-2 block text-sm font-medium text-gray-700"
>
{{ t('history.select_date') }}
</label>
<input
id="date-select"
type="date"
:value="store.selectedDate"
@input="
store.selectedDate = ($event.target as HTMLInputElement).value
"
class="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm shadow-sm focus:border-primary focus:ring-1 focus:ring-primary focus:outline-none"
/>
</div>
<Loading v-if="store.isLoading" />
<template v-else-if="store.historyData">
<div class="mb-6">
<h2 class="mb-2 text-xl font-bold text-primary">
{{ t('history.electricity_prices') }}
</h2>
<Chart
v-if="electricityChartData.length > 0"
:data="electricityChartData"
/>
<div
v-else
class="rounded-xl bg-gray-100 p-8 text-center text-gray-500"
>
{{ t('history.no_electricity_data') }}
</div>
</div>
<div>
<h2 class="mb-2 text-xl font-bold text-primary">
{{ t('history.gas_prices') }}
</h2>
<Chart
v-if="gasChartData.length > 0"
:data="gasChartData"
color="#3b82f6"
/>
<div
v-else
class="rounded-xl bg-gray-100 p-8 text-center text-gray-500"
>
{{ t('history.no_gas_data') }}
</div>
</div>
</template>
<div
v-else-if="!store.isLoading && !store.historyData"
class="rounded-xl bg-gray-100 p-8 text-center text-gray-500"
>
{{ t('history.no_data') }}
</div>
<Error v-if="store.error" :message="store.error" />
</template>
@@ -1,30 +0,0 @@
import { create } from 'axios';
import { ref } from 'vue';
import HistoryData = App.Features.History.Data.HistoryData;
export default function useHistoryViewModel() {
const isLoading = ref<boolean>(false);
const error = ref<string>();
const historyData = ref<HistoryData>();
function getHistoryData(date: string) {
isLoading.value = true;
const client = create();
client
.get<HistoryData>('/api/history', {
params: {
date,
},
})
.then((res) => {
historyData.value = res.data;
})
.catch((err) => {
error.value = err.response.data.message;
})
.finally(() => (isLoading.value = false));
}
return { isLoading, historyData, error, getHistoryData };
}
+2 -4
View File
@@ -28,13 +28,11 @@ declare namespace App {
namespace Dashboard {
namespace Data {
export type DashboardData = {
electricity: App.Data.ElectricityData;
electricity: App.Data.ElectricityData | null;
electricityCheapest: App.Data.ElectricityData | null;
electricityDurable: App.Data.ElectricityData | null;
electricityPriciest: App.Data.ElectricityData | null;
gas: App.Data.GasData;
gasCheapest: App.Data.GasData | null;
gasPriciest: App.Data.GasData | null;
gas: App.Data.GasData | null;
};
}
}
@@ -1,3 +1,3 @@
{
"generated.d.ts": "596cfad66b8ff3f8b6b98271630919ae"
"generated.d.ts": "50b70ce1a75284fa50634037e0073fb4"
}
@@ -8,5 +8,6 @@
"priciest_electricity_price": "Duurste elektriciteitsprijs",
"current_gas_price": "Actuele gasprijs",
"cheapest_gas_price": "Goedkoopste gasprijs",
"priciest_gas_price": "Duurste gasprijs"
"priciest_gas_price": "Duurste gasprijs",
"no_data": "Geen prijsgegevens beschikbaar"
}
@@ -1,3 +1,9 @@
{
"title": "Prijshistorie"
"title": "Prijshistorie",
"select_date": "Selecteer een datum",
"electricity_prices": "Elektriciteitsprijzen",
"gas_prices": "Gasprijzen",
"no_data": "Geen prijsgegevens beschikbaar voor deze datum",
"no_electricity_data": "Geen elektriciteitsprijsgegevens beschikbaar",
"no_gas_data": "Geen gasprijsgegevens beschikbaar"
}
+24
View File
@@ -0,0 +1,24 @@
import axios from 'axios';
const http = axios.create({
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
http.interceptors.response.use(
(response) => response,
(error) => {
const message =
error.response?.data?.message ||
error.message ||
'Er is een onverwachte fout opgetreden';
console.error('[HTTP Error]', message);
return Promise.reject(error);
},
);
export default http;
-7
View File
@@ -1,7 +0,0 @@
import { clsx } from 'clsx';
import type { ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+34
View File
@@ -0,0 +1,34 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import http from '@/lib/http';
import DashboardData = App.Features.Dashboard.Data.DashboardData;
export const useDashboardStore = defineStore('dashboard', () => {
const isLoading = ref(false);
const error = ref<string>();
const dashboardData = ref<DashboardData>();
async function fetchDashboardData() {
isLoading.value = true;
error.value = undefined;
try {
const { data } = await http.get<DashboardData>('/api/energy-prices');
dashboardData.value = data;
} catch (err: unknown) {
const axiosError = err as { response?: { data?: { message?: string } } };
error.value =
axiosError.response?.data?.message ||
'Kon dashboard data niet ophalen';
} finally {
isLoading.value = false;
}
}
return {
isLoading,
error,
dashboardData,
fetchDashboardData,
};
});
+41
View File
@@ -0,0 +1,41 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import http from '@/lib/http';
import HistoryData = App.Features.History.Data.HistoryData;
export const useHistoryStore = defineStore('history', () => {
const isLoading = ref(false);
const error = ref<string>();
const historyData = ref<HistoryData>();
const selectedDate = ref<string>(new Date().toISOString().split('T')[0]);
async function fetchHistoryData(date: string) {
isLoading.value = true;
error.value = undefined;
selectedDate.value = date;
try {
const { data } = await http.get<HistoryData>('/api/history', {
params: { date },
});
historyData.value = data;
} catch (err: unknown) {
const axiosError = err as {
response?: { data?: { message?: string } };
};
error.value =
axiosError.response?.data?.message ||
'Kon prijshistorie niet ophalen';
} finally {
isLoading.value = false;
}
}
return {
isLoading,
error,
historyData,
selectedDate,
fetchHistoryData,
};
});
-1
View File
@@ -1 +0,0 @@
export * from './auth';