Compare commits

..
3 Commits
25 changed files with 491 additions and 36 deletions
+1 -1
View File
@@ -72,7 +72,7 @@ class ZonneplanPriceClient implements PriceClient
]); ]);
throw new Exception( throw new Exception(
"Kon energieprijzen niet ophalen voor {$path}", __('errors.client_error'),
previous: $e, previous: $e,
); );
} }
+3 -4
View File
@@ -9,12 +9,11 @@ use Spatie\LaravelData\Data;
class HistoryData extends Data class HistoryData extends Data
{ {
/**
* @property Collection<ElectricityData> $electricity
* @property Collection<GasData> $gas
*/
public function __construct( public function __construct(
/** @var Collection<ElectricityData> */
public Collection $electricity, public Collection $electricity,
/** @var Collection<GasData> */
public Collection $gas, public Collection $gas,
) {} ) {}
} }
+20
View File
@@ -0,0 +1,20 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'These credentials do not match our records.',
'password' => 'The provided password is incorrect.',
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
];
+19
View File
@@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; Previous',
'next' => 'Next &raquo;',
];
+22
View File
@@ -0,0 +1,22 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reset Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| outcome such as failure due to an invalid password / reset token.
|
*/
'reset' => 'Your password has been reset.',
'sent' => 'We have emailed your password reset link.',
'throttled' => 'Please wait before retrying.',
'token' => 'This password reset token is invalid.',
'user' => "We can't find a user with that email address.",
];
+201
View File
@@ -0,0 +1,201 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => 'The :attribute field must be accepted.',
'accepted_if' => 'The :attribute field must be accepted when :other is :value.',
'active_url' => 'The :attribute field must be a valid URL.',
'after' => 'The :attribute field must be a date after :date.',
'after_or_equal' => 'The :attribute field must be a date after or equal to :date.',
'alpha' => 'The :attribute field must only contain letters.',
'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes, and underscores.',
'alpha_num' => 'The :attribute field must only contain letters and numbers.',
'any_of' => 'The :attribute field is invalid.',
'array' => 'The :attribute field must be an array.',
'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.',
'base64' => 'The :attribute field must be a valid Base64 string.',
'before' => 'The :attribute field must be a date before :date.',
'before_or_equal' => 'The :attribute field must be a date before or equal to :date.',
'between' => [
'array' => 'The :attribute field must have between :min and :max items.',
'file' => 'The :attribute field must be between :min and :max kilobytes.',
'numeric' => 'The :attribute field must be between :min and :max.',
'string' => 'The :attribute field must be between :min and :max characters.',
],
'boolean' => 'The :attribute field must be true or false.',
'can' => 'The :attribute field contains an unauthorized value.',
'confirmed' => 'The :attribute field confirmation does not match.',
'contains' => 'The :attribute field is missing a required value.',
'current_password' => 'The password is incorrect.',
'date' => 'The :attribute field must be a valid date.',
'date_equals' => 'The :attribute field must be a date equal to :date.',
'date_format' => 'The :attribute field must match the format :format.',
'decimal' => 'The :attribute field must have :decimal decimal places.',
'declined' => 'The :attribute field must be declined.',
'declined_if' => 'The :attribute field must be declined when :other is :value.',
'different' => 'The :attribute field and :other must be different.',
'digits' => 'The :attribute field must be :digits digits.',
'digits_between' => 'The :attribute field must be between :min and :max digits.',
'dimensions' => 'The :attribute field has invalid image dimensions.',
'distinct' => 'The :attribute field has a duplicate value.',
'doesnt_contain' => 'The :attribute field must not contain any of the following: :values.',
'doesnt_end_with' => 'The :attribute field must not end with one of the following: :values.',
'doesnt_start_with' => 'The :attribute field must not start with one of the following: :values.',
'email' => 'The :attribute field must be a valid email address.',
'encoding' => 'The :attribute field must be encoded in :encoding.',
'ends_with' => 'The :attribute field must end with one of the following: :values.',
'enum' => 'The selected :attribute is invalid.',
'exists' => 'The selected :attribute is invalid.',
'extensions' => 'The :attribute field must have one of the following extensions: :values.',
'file' => 'The :attribute field must be a file.',
'filled' => 'The :attribute field must have a value.',
'gt' => [
'array' => 'The :attribute field must have more than :value items.',
'file' => 'The :attribute field must be greater than :value kilobytes.',
'numeric' => 'The :attribute field must be greater than :value.',
'string' => 'The :attribute field must be greater than :value characters.',
],
'gte' => [
'array' => 'The :attribute field must have :value items or more.',
'file' => 'The :attribute field must be greater than or equal to :value kilobytes.',
'numeric' => 'The :attribute field must be greater than or equal to :value.',
'string' => 'The :attribute field must be greater than or equal to :value characters.',
],
'hex_color' => 'The :attribute field must be a valid hexadecimal color.',
'image' => 'The :attribute field must be an image.',
'in' => 'The selected :attribute is invalid.',
'in_array' => 'The :attribute field must exist in :other.',
'in_array_keys' => 'The :attribute field must contain at least one of the following keys: :values.',
'integer' => 'The :attribute field must be an integer.',
'ip' => 'The :attribute field must be a valid IP address.',
'ipv4' => 'The :attribute field must be a valid IPv4 address.',
'ipv6' => 'The :attribute field must be a valid IPv6 address.',
'json' => 'The :attribute field must be a valid JSON string.',
'list' => 'The :attribute field must be a list.',
'lowercase' => 'The :attribute field must be lowercase.',
'lt' => [
'array' => 'The :attribute field must have less than :value items.',
'file' => 'The :attribute field must be less than :value kilobytes.',
'numeric' => 'The :attribute field must be less than :value.',
'string' => 'The :attribute field must be less than :value characters.',
],
'lte' => [
'array' => 'The :attribute field must not have more than :value items.',
'file' => 'The :attribute field must be less than or equal to :value kilobytes.',
'numeric' => 'The :attribute field must be less than or equal to :value.',
'string' => 'The :attribute field must be less than or equal to :value characters.',
],
'mac_address' => 'The :attribute field must be a valid MAC address.',
'max' => [
'array' => 'The :attribute field must not have more than :max items.',
'file' => 'The :attribute field must not be greater than :max kilobytes.',
'numeric' => 'The :attribute field must not be greater than :max.',
'string' => 'The :attribute field must not be greater than :max characters.',
],
'max_digits' => 'The :attribute field must not have more than :max digits.',
'mimes' => 'The :attribute field must be a file of type: :values.',
'mimetypes' => 'The :attribute field must be a file of type: :values.',
'min' => [
'array' => 'The :attribute field must have at least :min items.',
'file' => 'The :attribute field must be at least :min kilobytes.',
'numeric' => 'The :attribute field must be at least :min.',
'string' => 'The :attribute field must be at least :min characters.',
],
'min_digits' => 'The :attribute field must have at least :min digits.',
'missing' => 'The :attribute field must be missing.',
'missing_if' => 'The :attribute field must be missing when :other is :value.',
'missing_unless' => 'The :attribute field must be missing unless :other is :value.',
'missing_with' => 'The :attribute field must be missing when :values is present.',
'missing_with_all' => 'The :attribute field must be missing when :values are present.',
'multiple_of' => 'The :attribute field must be a multiple of :value.',
'not_in' => 'The selected :attribute is invalid.',
'not_regex' => 'The :attribute field format is invalid.',
'numeric' => 'The :attribute field must be a number.',
'password' => [
'letters' => 'The :attribute field must contain at least one letter.',
'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.',
'numbers' => 'The :attribute field must contain at least one number.',
'symbols' => 'The :attribute field must contain at least one symbol.',
'uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.',
],
'present' => 'The :attribute field must be present.',
'present_if' => 'The :attribute field must be present when :other is :value.',
'present_unless' => 'The :attribute field must be present unless :other is :value.',
'present_with' => 'The :attribute field must be present when :values is present.',
'present_with_all' => 'The :attribute field must be present when :values are present.',
'prohibited' => 'The :attribute field is prohibited.',
'prohibited_if' => 'The :attribute field is prohibited when :other is :value.',
'prohibited_if_accepted' => 'The :attribute field is prohibited when :other is accepted.',
'prohibited_if_declined' => 'The :attribute field is prohibited when :other is declined.',
'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.',
'prohibits' => 'The :attribute field prohibits :other from being present.',
'regex' => 'The :attribute field format is invalid.',
'required' => 'The :attribute field is required.',
'required_array_keys' => 'The :attribute field must contain entries for: :values.',
'required_if' => 'The :attribute field is required when :other is :value.',
'required_if_accepted' => 'The :attribute field is required when :other is accepted.',
'required_if_declined' => 'The :attribute field is required when :other is declined.',
'required_unless' => 'The :attribute field is required unless :other is in :values.',
'required_with' => 'The :attribute field is required when :values is present.',
'required_with_all' => 'The :attribute field is required when :values are present.',
'required_without' => 'The :attribute field is required when :values is not present.',
'required_without_all' => 'The :attribute field is required when none of :values are present.',
'same' => 'The :attribute field must match :other.',
'size' => [
'array' => 'The :attribute field must contain :size items.',
'file' => 'The :attribute field must be :size kilobytes.',
'numeric' => 'The :attribute field must be :size.',
'string' => 'The :attribute field must be :size characters.',
],
'starts_with' => 'The :attribute field must start with one of the following: :values.',
'string' => 'The :attribute field must be a string.',
'timezone' => 'The :attribute field must be a valid timezone.',
'unique' => 'The :attribute has already been taken.',
'uploaded' => 'The :attribute failed to upload.',
'uppercase' => 'The :attribute field must be uppercase.',
'url' => 'The :attribute field must be a valid URL.',
'ulid' => 'The :attribute field must be a valid ULID.',
'uuid' => 'The :attribute field must be a valid UUID.',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap our attribute placeholder
| with something more reader friendly such as "E-Mail Address" instead
| of "email". This simply helps us make our message more expressive.
|
*/
'attributes' => [],
];
+5
View File
@@ -0,0 +1,5 @@
<?php
return [
'client_error' => 'Kon energieprijzen niet ophalen'
];
+31 -1
View File
@@ -10,6 +10,7 @@
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"@vitejs/plugin-vue": "^6.0.0", "@vitejs/plugin-vue": "^6.0.0",
"axios": "^1.19.0", "axios": "^1.19.0",
"chart.js": "^4.5.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"i18next": "^26.3.6", "i18next": "^26.3.6",
@@ -20,7 +21,8 @@
"tailwindcss": "^4.1.1", "tailwindcss": "^4.1.1",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^8.0.0", "vite": "^8.0.0",
"vue": "^3.5.13" "vue": "^3.5.13",
"vue-chartjs": "^5.3.4"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.19.0", "@eslint/js": "^9.19.0",
@@ -484,6 +486,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@laravel/vite-plugin-wayfinder": { "node_modules/@laravel/vite-plugin-wayfinder": {
"version": "0.1.7", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/@laravel/vite-plugin-wayfinder/-/vite-plugin-wayfinder-0.1.7.tgz", "resolved": "https://registry.npmjs.org/@laravel/vite-plugin-wayfinder/-/vite-plugin-wayfinder-0.1.7.tgz",
@@ -2420,6 +2428,18 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/chart.js": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/cliui": { "node_modules/cliui": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -6856,6 +6876,16 @@
} }
} }
}, },
"node_modules/vue-chartjs": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.4.tgz",
"integrity": "sha512-x3Fqob8RQvrTdssfi9ecsCzEkFOd8JPmNwSkSQzdfKj/uBsRJs/Y88cZcZIEcPsTVfMGwMo4MOoihoDG2DoE/g==",
"license": "MIT",
"peerDependencies": {
"chart.js": "^4.1.1",
"vue": "^3.0.0-0 || ^2.7.0"
}
},
"node_modules/vue-eslint-parser": { "node_modules/vue-eslint-parser": {
"version": "10.4.1", "version": "10.4.1",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.1.tgz", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.1.tgz",
+3 -1
View File
@@ -34,6 +34,7 @@
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"@vitejs/plugin-vue": "^6.0.0", "@vitejs/plugin-vue": "^6.0.0",
"axios": "^1.19.0", "axios": "^1.19.0",
"chart.js": "^4.5.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"i18next": "^26.3.6", "i18next": "^26.3.6",
@@ -44,7 +45,8 @@
"tailwindcss": "^4.1.1", "tailwindcss": "^4.1.1",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^8.0.0", "vite": "^8.0.0",
"vue": "^3.5.13" "vue": "^3.5.13",
"vue-chartjs": "^5.3.4"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.9.5", "@rollup/rollup-linux-x64-gnu": "4.9.5",
+1 -1
View File
@@ -11,7 +11,7 @@ createInertiaApp({
progress: { progress: {
color: '#4B5563', color: '#4B5563',
}, },
layout(name, page) { layout() {
return Layout; return Layout;
}, },
resolve(name) { resolve(name) {
+11
View File
@@ -0,0 +1,11 @@
<script setup lang="ts">
defineProps<{
message: string;
}>();
</script>
<template>
<div class="rounded-xl bg-red-300 p-2 font-semibold text-red-900 shadow-md">
{{ message }}
</div>
</template>
@@ -3,7 +3,7 @@ import moment from 'moment';
defineProps<{ defineProps<{
label: string; label: string;
price: number | null | undefined; price: number;
time: moment.Moment | string; time: moment.Moment | string;
}>(); }>();
</script> </script>
@@ -4,6 +4,7 @@ import moment from 'moment';
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import Price from '../components/Price.vue'; import Price from '../components/Price.vue';
import useDashboardViewModel from '../viewmodels/DashboardViewModel'; import useDashboardViewModel from '../viewmodels/DashboardViewModel';
import Error from '@/components/Error.vue';
const model = useDashboardViewModel(); const model = useDashboardViewModel();
@@ -16,12 +17,15 @@ onMounted(() => {
<h1 class="mb-4 text-3xl font-black text-primary"> <h1 class="mb-4 text-3xl font-black text-primary">
{{ t('dashboard.title') }} {{ t('dashboard.title') }}
</h1> </h1>
<div class="mb-4 flex flex-col gap-2"> <div class="mb-4 flex flex-col gap-2" v-if="model.dashboardData.value">
<h2 class="text-2xl font-black text-primary"> <h2 class="text-2xl font-black text-primary">
{{ t('dashboard.electricity') }} {{ t('dashboard.electricity') }}
</h2> </h2>
<div class="flex flex-wrap gap-4"> <div class="flex flex-wrap gap-4">
<Price <Price
v-if="
model.dashboardData.value?.electricity.totalPriceTaxIncluded
"
:label="t('dashboard.current_electricity_price')" :label="t('dashboard.current_electricity_price')"
:price=" :price="
model.dashboardData.value?.electricity.totalPriceTaxIncluded model.dashboardData.value?.electricity.totalPriceTaxIncluded
@@ -29,6 +33,10 @@ onMounted(() => {
:time="moment(model.dashboardData.value?.electricity.startDate)" :time="moment(model.dashboardData.value?.electricity.startDate)"
/> />
<Price <Price
v-if="
model.dashboardData.value?.electricityCheapest
?.totalPriceTaxIncluded
"
:label="t('dashboard.cheapest_electricity_price')" :label="t('dashboard.cheapest_electricity_price')"
:price=" :price="
model.dashboardData.value?.electricityCheapest model.dashboardData.value?.electricityCheapest
@@ -42,6 +50,10 @@ onMounted(() => {
" "
/> />
<Price <Price
v-if="
model.dashboardData.value?.electricityDurable
?.totalPriceTaxIncluded
"
:label="t('dashboard.durable_electricity_price')" :label="t('dashboard.durable_electricity_price')"
:price=" :price="
model.dashboardData.value?.electricityDurable model.dashboardData.value?.electricityDurable
@@ -55,6 +67,10 @@ onMounted(() => {
" "
/> />
<Price <Price
v-if="
model.dashboardData.value?.electricityPriciest
?.totalPriceTaxIncluded
"
:label="t('dashboard.priciest_electricity_price')" :label="t('dashboard.priciest_electricity_price')"
:price=" :price="
model.dashboardData.value?.electricityPriciest model.dashboardData.value?.electricityPriciest
@@ -69,12 +85,13 @@ onMounted(() => {
/> />
</div> </div>
</div> </div>
<div class="mb-4 flex flex-col gap-2"> <div class="mb-4 flex flex-col gap-2" v-if="model.dashboardData.value">
<h2 class="text-2xl font-black text-primary"> <h2 class="text-2xl font-black text-primary">
{{ t('dashboard.gas') }} {{ t('dashboard.gas') }}
</h2> </h2>
<div class="flex flex-wrap gap-4"> <div class="flex flex-wrap gap-4">
<Price <Price
v-if="model.dashboardData.value?.gas.totalPriceTaxIncluded"
:label="t('dashboard.current_gas_price')" :label="t('dashboard.current_gas_price')"
:price="model.dashboardData.value?.gas.totalPriceTaxIncluded" :price="model.dashboardData.value?.gas.totalPriceTaxIncluded"
:time=" :time="
@@ -84,6 +101,10 @@ onMounted(() => {
" "
/> />
<Price <Price
v-if="
model.dashboardData.value?.gasCheapest
?.totalPriceTaxIncluded
"
:label="t('dashboard.cheapest_gas_price')" :label="t('dashboard.cheapest_gas_price')"
:price=" :price="
model.dashboardData.value?.gasCheapest model.dashboardData.value?.gasCheapest
@@ -96,6 +117,10 @@ onMounted(() => {
" "
/> />
<Price <Price
v-if="
model.dashboardData.value?.gasPriciest
?.totalPriceTaxIncluded
"
:label="t('dashboard.priciest_gas_price')" :label="t('dashboard.priciest_gas_price')"
:price=" :price="
model.dashboardData.value?.gasPriciest model.dashboardData.value?.gasPriciest
@@ -109,4 +134,5 @@ onMounted(() => {
/> />
</div> </div>
</div> </div>
<Error :message="model.error.value" v-if="model.error.value" />
</template> </template>
@@ -4,6 +4,7 @@ import DashboardData = App.Features.Dashboard.Data.DashboardData;
export default function useDashboardViewModel() { export default function useDashboardViewModel() {
const isLoading = ref<boolean>(false); const isLoading = ref<boolean>(false);
const error = ref<string>();
const dashboardData = ref<DashboardData>(); const dashboardData = ref<DashboardData>();
function getDashboardData() { function getDashboardData() {
@@ -15,8 +16,11 @@ export default function useDashboardViewModel() {
.then((res) => { .then((res) => {
dashboardData.value = res.data; dashboardData.value = res.data;
}) })
.catch((err) => {
error.value = err.response.data.message;
})
.finally(() => (isLoading.value = false)); .finally(() => (isLoading.value = false));
} }
return { isLoading, dashboardData, getDashboardData }; return { isLoading, dashboardData, error, getDashboardData };
} }
@@ -0,0 +1,63 @@
<script setup lang="ts">
import { computed } from 'vue';
import { Line } from 'vue-chartjs';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Tooltip,
Legend,
Filler,
} from 'chart.js';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Tooltip,
Legend,
Filler,
);
export interface PricePoint {
label: string;
value: number | null;
}
const props = defineProps<{
data: PricePoint[];
}>();
const chartData = computed(() => ({
labels: props.data.map((d) => d.label),
datasets: [
{
data: props.data.map((d) => d.value),
borderColor: '#4ba66a',
backgroundColor: 'rgba(75, 166, 106, 0.1)',
fill: true,
tension: 0.4,
pointRadius: 2,
},
],
}));
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
};
</script>
<template>
<div class="flex h-64 flex-1">
<Line :data="chartData" :options="chartOptions" />
</div>
</template>
+13 -3
View File
@@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref, watch } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import useHistoryViewModel from '../viewmodels/HistoryViewModel'; import useHistoryViewModel from '../viewmodels/HistoryViewModel';
import moment from 'moment'; import moment from 'moment';
import Chart from '../components/Chart.vue';
const model = useHistoryViewModel(); const model = useHistoryViewModel();
@@ -14,10 +15,19 @@ onMounted(() => {
watch(date, (value) => { watch(date, (value) => {
model.getHistoryData(moment(value).format('YYYY-MM-DD')); model.getHistoryData(moment(value).format('YYYY-MM-DD'));
}); });
const chartData = computed(
() =>
model.historyData.value?.electricity
.filter((e) => e.totalPriceTaxIncluded != null)
.map((e) => ({
label: moment(e.startDate).format('HH:mm'),
value: e.totalPriceTaxIncluded,
})) ?? [],
);
</script> </script>
<template> <template>
<div>
<input type="date" v-model="date" /> <input type="date" v-model="date" />
</div> <Chart :data="chartData" />
</template> </template>
@@ -1,6 +1,7 @@
import { create } from 'axios'; import { create } from 'axios';
import { ref } from 'vue'; import { ref } from 'vue';
import HistoryData = App.Features.History.Data.HistoryData; import HistoryData = App.Features.History.Data.HistoryData;
import moment from 'moment';
export default function useHistoryViewModel() { export default function useHistoryViewModel() {
const isLoading = ref<boolean>(false); const isLoading = ref<boolean>(false);
@@ -17,6 +18,18 @@ export default function useHistoryViewModel() {
}, },
}) })
.then((res) => { .then((res) => {
console.log(
res.data?.electricity.flatMap((e) => {
if (!e.totalPriceTaxIncluded) {
return;
}
return {
label: moment(e.startDate).format('DD-MM-YYYY'),
value: e.totalPriceTaxIncluded?.toPrecision(2),
};
}),
);
historyData.value = res.data; historyData.value = res.data;
}) })
.finally(() => (isLoading.value = false)); .finally(() => (isLoading.value = false));
+6 -6
View File
@@ -17,10 +17,10 @@ declare namespace App {
startDate: string; startDate: string;
endDate: string; endDate: string;
period: string; period: string;
marketPrice: number; marketPrice: number | null;
totalPriceTaxIncluded: number; totalPriceTaxIncluded: number | null;
priceInclHandlingVat: number; priceInclHandlingVat: number | null;
priceTaxWithVat: number; priceTaxWithVat: number | null;
startDateDatetime: string; startDateDatetime: string;
}; };
} }
@@ -41,8 +41,8 @@ declare namespace App {
namespace History { namespace History {
namespace Data { namespace Data {
export type HistoryData = { export type HistoryData = {
electricity: Array<any>; electricity: App.Data.ElectricityData[];
gas: Array<any>; gas: App.Data.GasData[];
}; };
} }
namespace Requests { namespace Requests {
@@ -1,3 +1,3 @@
{ {
"generated.d.ts": "7ca8a9c7f0ae5e5498bdee87bb2f005d" "generated.d.ts": "596cfad66b8ff3f8b6b98271630919ae"
} }
+2 -2
View File
@@ -1,9 +1,9 @@
import i18next from 'i18next'; import i18next from 'i18next';
import resources from './translations'; import resources from './translations';
await i18next.init(() => ({ await i18next.init({
lng: 'nl', lng: 'nl',
resources, resources,
})); });
export default i18next; export default i18next;
+3 -3
View File
@@ -1,5 +1,5 @@
import nl from "./nl"; import nl from './nl';
export default { export default {
nl nl: { translation: nl },
} };
@@ -1,3 +1,12 @@
{ {
"title": "Overzicht" "title": "Overzicht",
"electricity": "Elektriciteit",
"gas": "Gas",
"current_electricity_price": "Actuele elektriciteitsprijs",
"cheapest_electricity_price": "Goedkoopste elektriciteitsprijs",
"durable_electricity_price": "Duurzaamste elektriciteitsprijs",
"priciest_electricity_price": "Duurste elektriciteitsprijs",
"current_gas_price": "Actuele gasprijs",
"cheapest_gas_price": "Goedkoopste gasprijs",
"priciest_gas_price": "Duurste gasprijs"
} }
@@ -0,0 +1,3 @@
{
"title": "Prijshistorie"
}
@@ -1,5 +1,7 @@
import dashboard from './dashboard.json'; import dashboard from './dashboard.json';
import history from './history.json';
export default { export default {
dashboard, dashboard,
history,
}; };
+23 -7
View File
@@ -2,19 +2,35 @@
import { t } from 'i18next'; import { t } from 'i18next';
import dashboard from '@/routes/dashboard'; import dashboard from '@/routes/dashboard';
import history from '@/routes/history'; import history from '@/routes/history';
import { Link, usePage } from '@inertiajs/vue3';
import { computed } from 'vue';
const page = usePage();
const currentUrl = computed(() => page.url);
const navItems = [
{ href: dashboard.index().url, label: 'dashboard.title' },
{ href: history.index().url, label: 'history.title' },
];
</script> </script>
<template> <template>
<nav class="fixed top-0 bottom-0 left-0 w-64 bg-primary p-4"> <nav class="fixed top-0 bottom-0 left-0 w-64 bg-primary p-4">
<img src="/storage/logo.svg" class="mb-4 mix-blend-plus-lighter" /> <img src="/storage/logo.svg" class="mb-4 mix-blend-plus-lighter" />
<ul class="flex-1 gap-4"> <ul class="flex flex-1 flex-col gap-2">
<li> <li v-for="item in navItems" :key="item.href">
<a :href="dashboard.index().url">{{ t('dashboard.title') }}</a> <Link
</li> :href="item.href"
<li> :class="
<a :href="history.index().url">{{ t('history.title') }}</a> currentUrl === item.href
? 'block rounded-lg bg-white/10 p-2 font-semibold text-white'
: 'block rounded-lg p-2 text-white/70 hover:bg-white/5 hover:text-white'
"
>
{{ t(item.label) }}
</Link>
</li> </li>
</ul> </ul>
</nav> </nav>
<div class="absolute top-0 left-64 p-4"><slot /></div> <div class="absolute top-0 right-0 bottom-0 left-64 p-4"><slot /></div>
</template> </template>