refactor(dashboard,history): replace ViewModels with Pinia stores and improve backend
This commit is contained in:
@@ -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">
|
||||
€{{ price?.toPrecision(2).toString().replace('.', ',') }}
|
||||
€{{ price?.toFixed(2).replace('.', ',') }}
|
||||
</div>
|
||||
<div class="text-sm text-black/50">
|
||||
{{ moment.isMoment(time) ? time.format('DD-MM-YYYY HH:mm') : time }}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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 +0,0 @@
|
||||
export * from './auth';
|
||||
Reference in New Issue
Block a user