Compare commits
5
Commits
f70f3ab054
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90930cd223 | ||
|
|
a0387126de | ||
|
|
a63682ced0 | ||
|
|
fa66b416f3 | ||
|
|
029c29afd1 |
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,10 @@ use Spatie\LaravelData\Data;
|
|||||||
class DashboardData extends Data
|
class DashboardData extends Data
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public ElectricityData $electricity,
|
public ?ElectricityData $electricity,
|
||||||
public ?ElectricityData $electricityCheapest,
|
public ?ElectricityData $electricityCheapest,
|
||||||
public ?ElectricityData $electricityDurable,
|
public ?ElectricityData $electricityDurable,
|
||||||
public ?ElectricityData $electricityPriciest,
|
public ?ElectricityData $electricityPriciest,
|
||||||
public GasData $gas,
|
public ?GasData $gas,
|
||||||
public ?GasData $gasCheapest,
|
|
||||||
public ?GasData $gasPriciest,
|
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,19 +14,25 @@ class DashboardService
|
|||||||
|
|
||||||
public function getDashboardData(): DashboardData
|
public function getDashboardData(): DashboardData
|
||||||
{
|
{
|
||||||
|
$electricity = $this->repository->retrieveElectricity();
|
||||||
|
$gas = $this->repository->retrieveGas();
|
||||||
|
|
||||||
$now = Carbon::now();
|
$now = Carbon::now();
|
||||||
|
|
||||||
$electricity = $this->repository->retrieveElectricity();
|
$eNow = $electricity->first(
|
||||||
|
fn ($item) => $item->startDate instanceof Carbon
|
||||||
$eNow = $electricity->whereBetween('startDate', [$now->copy()->subHour(), $now])->first();
|
&& $item->startDate->gte($now->copy()->subHour())
|
||||||
|
&& $item->startDate->lte($now)
|
||||||
|
);
|
||||||
$eCheapest = $electricity->where('totalPriceTaxIncluded', $electricity->min('totalPriceTaxIncluded'))->first();
|
$eCheapest = $electricity->where('totalPriceTaxIncluded', $electricity->min('totalPriceTaxIncluded'))->first();
|
||||||
$eDurable = $electricity->where('sustainabilityScore', $electricity->max('sustainabilityScore'))->first();
|
$eDurable = $electricity->where('sustainabilityScore', $electricity->max('sustainabilityScore'))->first();
|
||||||
$ePriciest = $electricity->where('totalPriceTaxIncluded', $electricity->max('totalPriceTaxIncluded'))->first();
|
$ePriciest = $electricity->where('totalPriceTaxIncluded', $electricity->max('totalPriceTaxIncluded'))->first();
|
||||||
|
|
||||||
$gas = $this->repository->retrieveGas();
|
$gNow = $gas->first(
|
||||||
$gNow = $gas->whereBetween('startDate', [$now->copy()->setHour(0), $now])->first();
|
fn ($item) => $item->startDate instanceof Carbon
|
||||||
$gCheapest = $gas->where('totalPriceTaxIncluded', $gas->min('totalPriceTaxIncluded'))->first();
|
&& $item->startDate->gte($now->copy()->startOfDay())
|
||||||
$gPriciest = $gas->where('totalPriceTaxIncluded', $gas->max('totalPriceTaxIncluded'))->first();
|
&& $item->startDate->lte($now)
|
||||||
|
);
|
||||||
|
|
||||||
return new DashboardData(
|
return new DashboardData(
|
||||||
$eNow,
|
$eNow,
|
||||||
@@ -34,8 +40,6 @@ class DashboardService
|
|||||||
$eDurable,
|
$eDurable,
|
||||||
$ePriciest,
|
$ePriciest,
|
||||||
$gNow,
|
$gNow,
|
||||||
$gCheapest,
|
|
||||||
$gPriciest
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
namespace App\Features\History\Services;
|
namespace App\Features\History\Services;
|
||||||
|
|
||||||
use App\Contracts\PriceRepository;
|
use App\Contracts\PriceRepository;
|
||||||
use App\Data\ElectricityData;
|
|
||||||
use App\Data\GasData;
|
|
||||||
use App\Features\History\Data\HistoryData;
|
use App\Features\History\Data\HistoryData;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
|
||||||
@@ -16,9 +14,12 @@ class HistoryService
|
|||||||
|
|
||||||
public function getHistoryData(Carbon $date): HistoryData
|
public function getHistoryData(Carbon $date): HistoryData
|
||||||
{
|
{
|
||||||
$electricity = ElectricityData::collect($this->repository->retrieveElectricityForDate($date));
|
$electricity = $this->repository->retrieveElectricityForDate($date);
|
||||||
$gas = GasData::collect($this->repository->retrieveGasForDate($date));
|
$gas = $this->repository->retrieveGasForDate($date);
|
||||||
|
|
||||||
return new HistoryData($electricity, $gas);
|
return new HistoryData(
|
||||||
|
electricity: $electricity,
|
||||||
|
gas: $gas
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,39 +13,74 @@ use Override;
|
|||||||
|
|
||||||
class ZonneplanPriceRepository implements ContractsPriceRepository
|
class ZonneplanPriceRepository implements ContractsPriceRepository
|
||||||
{
|
{
|
||||||
|
private int $cacheTtl;
|
||||||
|
private string $cachePrefix;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public PriceClient $client
|
public PriceClient $client
|
||||||
) {}
|
) {
|
||||||
|
$this->cacheTtl = config('cache_prices.ttl', 3600);
|
||||||
|
$this->cachePrefix = config('cache_prices.prefix', 'zonneplan:');
|
||||||
|
}
|
||||||
|
|
||||||
#[Override]
|
#[Override]
|
||||||
public function retrieveElectricity(): Collection
|
public function retrieveElectricity(): Collection
|
||||||
{
|
{
|
||||||
$data = Cache::remember('electricity:upcomming', 3600, fn () => $this->client->getElectricityPrices()->toArray());
|
$raw = $this->remember(
|
||||||
|
'electricity:upcoming',
|
||||||
|
fn () => $this->client->getElectricityPrices()->toArray()
|
||||||
|
);
|
||||||
|
|
||||||
return ElectricityData::collect($data, Collection::class);
|
return ElectricityData::collect($raw, Collection::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Override]
|
#[Override]
|
||||||
public function retrieveGas(): Collection
|
public function retrieveGas(): Collection
|
||||||
{
|
{
|
||||||
$data = Cache::remember('gas:upcomming', 3600, fn () => $this->client->getGasPrices()->toArray());
|
$raw = $this->remember(
|
||||||
|
'gas:upcoming',
|
||||||
|
fn () => $this->client->getGasPrices()->toArray()
|
||||||
|
);
|
||||||
|
|
||||||
return GasData::collect($data, Collection::class);
|
return GasData::collect($raw, Collection::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Override]
|
#[Override]
|
||||||
public function retrieveElectricityForDate(Carbon $date): Collection
|
public function retrieveElectricityForDate(Carbon $date): Collection
|
||||||
{
|
{
|
||||||
$data = Cache::remember('electricity:'.$date->format('Y-m-d'), 3600, fn () => $this->client->getElectricityPricesForDate($date)->toArray());
|
$raw = $this->remember(
|
||||||
|
'electricity:' . $date->format('Y-m-d'),
|
||||||
|
fn () => $this->client->getElectricityPricesForDate($date)->toArray()
|
||||||
|
);
|
||||||
|
|
||||||
return GasData::collect($data, Collection::class);
|
return ElectricityData::collect($raw, Collection::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Override]
|
#[Override]
|
||||||
public function retrieveGasForDate(Carbon $date): Collection
|
public function retrieveGasForDate(Carbon $date): Collection
|
||||||
{
|
{
|
||||||
$data = Cache::remember('gas:'.$date->format('Y-m-d'), 3600, fn () => $this->client->getGasPricesForDate($date)->toArray());
|
$raw = $this->remember(
|
||||||
|
'gas:' . $date->format('Y-m-d'),
|
||||||
|
fn () => $this->client->getGasPricesForDate($date)->toArray()
|
||||||
|
);
|
||||||
|
|
||||||
return GasData::collect($data, Collection::class);
|
return GasData::collect($raw, Collection::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache via JSON encoding to ensure clean array structure on retrieval.
|
||||||
|
*
|
||||||
|
* @param callable(): array $callback
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function remember(string $key, callable $callback): array
|
||||||
|
{
|
||||||
|
$cached = Cache::remember(
|
||||||
|
$this->cachePrefix . $key,
|
||||||
|
$this->cacheTtl,
|
||||||
|
fn () => json_encode($callback())
|
||||||
|
);
|
||||||
|
|
||||||
|
return json_decode($cached, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache TTL (Time To Live) in seconds
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Hiermee configureer je hoe lang API-responses worden gecachet.
|
||||||
|
| Standaard is dit 1 uur (3600 seconden).
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'ttl' => env('CACHE_TTL', 3600),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Prefix voor cache keys om conflicten te vermijden.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'prefix' => 'zonneplan:',
|
||||||
|
];
|
||||||
@@ -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.',
|
||||||
|
|
||||||
|
];
|
||||||
@@ -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' => '« Previous',
|
||||||
|
'next' => 'Next »',
|
||||||
|
|
||||||
|
];
|
||||||
@@ -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.",
|
||||||
|
|
||||||
|
];
|
||||||
@@ -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' => [],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'client_error' => 'Kon energieprijzen niet ophalen'
|
||||||
|
];
|
||||||
Generated
+118
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "html",
|
"name": "Zonneplan",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
@@ -10,17 +10,20 @@
|
|||||||
"@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",
|
||||||
"i18next-vue": "^5.4.0",
|
"i18next-vue": "^5.4.0",
|
||||||
"laravel-vite-plugin": "^3.1",
|
"laravel-vite-plugin": "^3.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
|
"pinia": "^4.0.2",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"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 +487,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",
|
||||||
@@ -1899,6 +1908,36 @@
|
|||||||
"he": "^1.2.0"
|
"he": "^1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vue/devtools-api": {
|
||||||
|
"version": "8.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz",
|
||||||
|
"integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-kit": "^8.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/devtools-kit": {
|
||||||
|
"version": "8.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.2.1.tgz",
|
||||||
|
"integrity": "sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-shared": "^8.2.1",
|
||||||
|
"birpc": "^2.6.1",
|
||||||
|
"hookable": "^5.5.3",
|
||||||
|
"perfect-debounce": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/devtools-shared": {
|
||||||
|
"version": "8.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.2.1.tgz",
|
||||||
|
"integrity": "sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/@vue/eslint-config-typescript": {
|
"node_modules/@vue/eslint-config-typescript": {
|
||||||
"version": "14.9.0",
|
"version": "14.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.9.0.tgz",
|
||||||
@@ -2300,6 +2339,16 @@
|
|||||||
"node": "18 || 20 || >=22"
|
"node": "18 || 20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/birpc": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/boolbase": {
|
"node_modules/boolbase": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
@@ -2420,6 +2469,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",
|
||||||
@@ -3948,6 +4009,13 @@
|
|||||||
"he": "bin/he"
|
"he": "bin/he"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hookable": {
|
||||||
|
"version": "5.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
||||||
|
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/https-proxy-agent": {
|
"node_modules/https-proxy-agent": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||||
@@ -5133,6 +5201,12 @@
|
|||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nostics": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nostics/-/nostics-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/nth-check": {
|
"node_modules/nth-check": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||||
@@ -5375,6 +5449,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/perfect-debounce": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -5393,6 +5474,31 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pinia": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/pinia/-/pinia-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-yKVVA7bSj5oRZFp/Ab9wLlmyb5gPUYEiIm4ryiWTe/xe7PtkRdMVOp1X1ggvq0c6Uj7Q0Du1HnV2mtAwM0Ks1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"nostics": "^1.1.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/posva"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/devtools-api": "^8.1.5",
|
||||||
|
"typescript": ">=5.6.0",
|
||||||
|
"vue": "^3.5.11"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/devtools-api": {
|
||||||
|
"optional": false
|
||||||
|
},
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/possible-typed-array-names": {
|
"node_modules/possible-typed-array-names": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
@@ -6856,6 +6962,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",
|
||||||
|
|||||||
+4
-1
@@ -34,17 +34,20 @@
|
|||||||
"@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",
|
||||||
"i18next-vue": "^5.4.0",
|
"i18next-vue": "^5.4.0",
|
||||||
"laravel-vite-plugin": "^3.1",
|
"laravel-vite-plugin": "^3.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
|
"pinia": "^4.0.2",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"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",
|
||||||
|
|||||||
+4
-1
@@ -1,17 +1,19 @@
|
|||||||
import { createInertiaApp } from '@inertiajs/vue3';
|
import { createInertiaApp } from '@inertiajs/vue3';
|
||||||
import I18NextVue from 'i18next-vue';
|
import I18NextVue from 'i18next-vue';
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
import type { DefineComponent } from 'vue';
|
import type { DefineComponent } from 'vue';
|
||||||
import i18next from './lang/init';
|
import i18next from './lang/init';
|
||||||
import Layout from './layouts/Layout.vue';
|
import Layout from './layouts/Layout.vue';
|
||||||
|
|
||||||
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
||||||
|
const pinia = createPinia();
|
||||||
|
|
||||||
createInertiaApp({
|
createInertiaApp({
|
||||||
title: (title) => (title ? `${title} - ${appName}` : appName),
|
title: (title) => (title ? `${title} - ${appName}` : appName),
|
||||||
progress: {
|
progress: {
|
||||||
color: '#4B5563',
|
color: '#4B5563',
|
||||||
},
|
},
|
||||||
layout(name, page) {
|
layout() {
|
||||||
return Layout;
|
return Layout;
|
||||||
},
|
},
|
||||||
resolve(name) {
|
resolve(name) {
|
||||||
@@ -33,6 +35,7 @@ createInertiaApp({
|
|||||||
return pages[`./pages/${name}.vue`];
|
return pages[`./pages/${name}.vue`];
|
||||||
},
|
},
|
||||||
withApp(app, options) {
|
withApp(app, options) {
|
||||||
|
app.use(pinia);
|
||||||
app.use(I18NextVue, {
|
app.use(I18NextVue, {
|
||||||
i18next,
|
i18next,
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="size-8 animate-spin rounded-full border-4 border-primary/30 border-t-primary"
|
||||||
|
role="status"
|
||||||
|
aria-label="Laden"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
@@ -13,7 +13,7 @@ defineProps<{
|
|||||||
<div class="font-bold text-primary uppercase">{{ label }}</div>
|
<div class="font-bold text-primary uppercase">{{ label }}</div>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="text-4xl font-black">
|
<div class="text-4xl font-black">
|
||||||
€{{ price?.toPrecision(2).toString().replace('.', ',') }}
|
€{{ price?.toFixed(2).replace('.', ',') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-black/50">
|
<div class="text-sm text-black/50">
|
||||||
{{ moment.isMoment(time) ? time.format('DD-MM-YYYY HH:mm') : time }}
|
{{ moment.isMoment(time) ? time.format('DD-MM-YYYY HH:mm') : time }}
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import { t } from 'i18next';
|
|||||||
import moment from 'moment';
|
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 { useDashboardStore } from '@/stores/dashboard';
|
||||||
|
import Error from '@/components/Error.vue';
|
||||||
|
import Loading from '@/components/Loading.vue';
|
||||||
|
|
||||||
const model = useDashboardViewModel();
|
const store = useDashboardStore();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
model.getDashboardData();
|
store.fetchDashboardData();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -16,97 +18,94 @@ 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">
|
<Loading v-if="store.isLoading" />
|
||||||
<h2 class="text-2xl font-black text-primary">
|
<template v-else-if="store.dashboardData">
|
||||||
{{ t('dashboard.electricity') }}
|
<div class="mb-4 flex flex-col gap-2">
|
||||||
</h2>
|
<h2 class="text-2xl font-black text-primary">
|
||||||
<div class="flex flex-wrap gap-4">
|
{{ t('dashboard.electricity') }}
|
||||||
<Price
|
</h2>
|
||||||
:label="t('dashboard.current_electricity_price')"
|
<div class="flex flex-wrap gap-4">
|
||||||
:price="
|
<Price
|
||||||
model.dashboardData.value?.electricity.totalPriceTaxIncluded
|
v-if="
|
||||||
"
|
store.dashboardData.electricity?.totalPriceTaxIncluded
|
||||||
:time="moment(model.dashboardData.value?.electricity.startDate)"
|
"
|
||||||
/>
|
:label="t('dashboard.current_electricity_price')"
|
||||||
<Price
|
:price="
|
||||||
:label="t('dashboard.cheapest_electricity_price')"
|
store.dashboardData.electricity.totalPriceTaxIncluded
|
||||||
:price="
|
"
|
||||||
model.dashboardData.value?.electricityCheapest
|
:time="moment(store.dashboardData.electricity.startDate)"
|
||||||
?.totalPriceTaxIncluded
|
/>
|
||||||
"
|
<Price
|
||||||
:time="
|
v-if="
|
||||||
moment(
|
store.dashboardData.electricityCheapest
|
||||||
model.dashboardData.value?.electricityCheapest
|
?.totalPriceTaxIncluded
|
||||||
?.startDate,
|
"
|
||||||
)
|
:label="t('dashboard.cheapest_electricity_price')"
|
||||||
"
|
:price="
|
||||||
/>
|
store.dashboardData.electricityCheapest
|
||||||
<Price
|
.totalPriceTaxIncluded
|
||||||
:label="t('dashboard.durable_electricity_price')"
|
"
|
||||||
:price="
|
:time="
|
||||||
model.dashboardData.value?.electricityDurable
|
moment(
|
||||||
?.totalPriceTaxIncluded
|
store.dashboardData.electricityCheapest.startDate,
|
||||||
"
|
)
|
||||||
:time="
|
"
|
||||||
moment(
|
/>
|
||||||
model.dashboardData.value?.electricityDurable
|
<Price
|
||||||
?.startDate,
|
v-if="
|
||||||
)
|
store.dashboardData.electricityDurable
|
||||||
"
|
?.totalPriceTaxIncluded
|
||||||
/>
|
"
|
||||||
<Price
|
:label="t('dashboard.durable_electricity_price')"
|
||||||
:label="t('dashboard.priciest_electricity_price')"
|
:price="
|
||||||
:price="
|
store.dashboardData.electricityDurable
|
||||||
model.dashboardData.value?.electricityPriciest
|
.totalPriceTaxIncluded
|
||||||
?.totalPriceTaxIncluded
|
"
|
||||||
"
|
:time="
|
||||||
:time="
|
moment(store.dashboardData.electricityDurable.startDate)
|
||||||
moment(
|
"
|
||||||
model.dashboardData.value?.electricityPriciest
|
/>
|
||||||
?.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>
|
<div class="mb-4 flex flex-col gap-2">
|
||||||
<div class="mb-4 flex flex-col gap-2">
|
<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="store.dashboardData.gas?.totalPriceTaxIncluded"
|
||||||
:label="t('dashboard.current_gas_price')"
|
:label="t('dashboard.current_gas_price')"
|
||||||
:price="model.dashboardData.value?.gas.totalPriceTaxIncluded"
|
:price="store.dashboardData.gas.totalPriceTaxIncluded"
|
||||||
:time="
|
:time="
|
||||||
moment(model.dashboardData.value?.gas.startDate).format(
|
moment(store.dashboardData.gas.startDate).format(
|
||||||
'DD-MM-YYYY',
|
'DD-MM-YYYY',
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<Price
|
</div>
|
||||||
:label="t('dashboard.cheapest_gas_price')"
|
|
||||||
:price="
|
|
||||||
model.dashboardData.value?.gasCheapest
|
|
||||||
?.totalPriceTaxIncluded
|
|
||||||
"
|
|
||||||
:time="
|
|
||||||
moment(
|
|
||||||
model.dashboardData.value?.gasCheapest?.startDate,
|
|
||||||
).format('DD-MM-YYYY')
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<Price
|
|
||||||
:label="t('dashboard.priciest_gas_price')"
|
|
||||||
:price="
|
|
||||||
model.dashboardData.value?.gasPriciest
|
|
||||||
?.totalPriceTaxIncluded
|
|
||||||
"
|
|
||||||
:time="
|
|
||||||
moment(
|
|
||||||
model.dashboardData.value?.gasPriciest?.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>
|
</div>
|
||||||
|
<Error :message="store.error" v-if="store.error" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,22 +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 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,66 @@
|
|||||||
|
<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[];
|
||||||
|
color?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const chartData = computed(() => ({
|
||||||
|
labels: props.data.map((d) => d.label),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: props.data.map((d) => d.value),
|
||||||
|
borderColor: props.color || '#4ba66a',
|
||||||
|
backgroundColor: props.color
|
||||||
|
? `${props.color}1a`
|
||||||
|
: '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>
|
||||||
@@ -1,23 +1,108 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, watch } from 'vue';
|
||||||
import useHistoryViewModel from '../viewmodels/HistoryViewModel';
|
|
||||||
import moment from 'moment';
|
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 store = useHistoryStore();
|
||||||
|
|
||||||
const date = ref<Date>();
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
model.getHistoryData(moment().format('YYYY-MM-DD'));
|
store.fetchHistoryData(moment().format('YYYY-MM-DD'));
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(date, (value) => {
|
watch(
|
||||||
model.getHistoryData(moment(value).format('YYYY-MM-DD'));
|
() => store.selectedDate,
|
||||||
});
|
(value) => {
|
||||||
|
if (value) {
|
||||||
|
store.fetchHistoryData(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const electricityChartData = computed(
|
||||||
|
() =>
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<h1 class="mb-4 text-3xl font-black text-primary">
|
||||||
<input type="date" v-model="date" />
|
{{ 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>
|
</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>
|
</template>
|
||||||
|
|||||||
@@ -1,26 +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 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;
|
|
||||||
})
|
|
||||||
.finally(() => (isLoading.value = false));
|
|
||||||
}
|
|
||||||
|
|
||||||
return { isLoading, historyData, getHistoryData };
|
|
||||||
}
|
|
||||||
+8
-10
@@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -28,21 +28,19 @@ declare namespace App {
|
|||||||
namespace Dashboard {
|
namespace Dashboard {
|
||||||
namespace Data {
|
namespace Data {
|
||||||
export type DashboardData = {
|
export type DashboardData = {
|
||||||
electricity: App.Data.ElectricityData;
|
electricity: App.Data.ElectricityData | null;
|
||||||
electricityCheapest: App.Data.ElectricityData | null;
|
electricityCheapest: App.Data.ElectricityData | null;
|
||||||
electricityDurable: App.Data.ElectricityData | null;
|
electricityDurable: App.Data.ElectricityData | null;
|
||||||
electricityPriciest: App.Data.ElectricityData | null;
|
electricityPriciest: App.Data.ElectricityData | null;
|
||||||
gas: App.Data.GasData;
|
gas: App.Data.GasData | null;
|
||||||
gasCheapest: App.Data.GasData | null;
|
|
||||||
gasPriciest: App.Data.GasData | null;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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": "50b70ce1a75284fa50634037e0073fb4"
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import nl from "./nl";
|
import nl from './nl';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
nl
|
nl: { translation: nl },
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
{
|
{
|
||||||
"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",
|
||||||
|
"no_data": "Geen prijsgegevens beschikbaar"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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';
|
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Api;
|
||||||
|
|
||||||
|
use App\Contracts\PriceClient;
|
||||||
|
use App\Data\ElectricityData;
|
||||||
|
use App\Data\GasData;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Mockery;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class DashboardApiTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
config(['cache_prices.ttl' => 60]);
|
||||||
|
config(['cache_prices.prefix' => 'test:']);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
Mockery::close();
|
||||||
|
\Illuminate\Support\Facades\Cache::flush();
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_dashboard_endpoint_returns_electricity_and_gas_data(): void
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
|
||||||
|
$electricityResponse = collect([
|
||||||
|
$this->createElectricityArray($now->copy()->subMinutes(30), 15.0, 80),
|
||||||
|
$this->createElectricityArray($now->copy()->addHour(), 12.0, 90),
|
||||||
|
$this->createElectricityArray($now->copy()->addHours(2), 20.0, 60),
|
||||||
|
$this->createElectricityArray($now->copy()->addHours(3), 10.0, 95),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$gasResponse = collect([
|
||||||
|
$this->createGasArray($now->copy()->startOfDay(), 5.0),
|
||||||
|
$this->createGasArray($now->copy()->addDay(), 4.0),
|
||||||
|
$this->createGasArray($now->copy()->addDays(2), 6.0),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$client = Mockery::mock(PriceClient::class);
|
||||||
|
$client->shouldReceive('getElectricityPrices')->once()->andReturn($electricityResponse);
|
||||||
|
$client->shouldReceive('getGasPrices')->once()->andReturn($gasResponse);
|
||||||
|
|
||||||
|
$this->app->bind(PriceClient::class, fn () => $client);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/energy-prices');
|
||||||
|
|
||||||
|
$response->assertOk()
|
||||||
|
->assertJsonStructure([
|
||||||
|
'electricity' => [
|
||||||
|
'startDate',
|
||||||
|
'endDate',
|
||||||
|
'period',
|
||||||
|
'totalPriceTaxIncluded',
|
||||||
|
],
|
||||||
|
'electricityCheapest',
|
||||||
|
'electricityDurable',
|
||||||
|
'electricityPriciest',
|
||||||
|
'gas' => [
|
||||||
|
'startDate',
|
||||||
|
'endDate',
|
||||||
|
'period',
|
||||||
|
'totalPriceTaxIncluded',
|
||||||
|
],
|
||||||
|
'gasCheapest',
|
||||||
|
'gasPriciest',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_dashboard_endpoint_returns_correct_prices(): void
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
|
||||||
|
$electricityResponse = collect([
|
||||||
|
$this->createElectricityArray($now->copy()->subMinutes(30), 15.0, 80),
|
||||||
|
$this->createElectricityArray($now->copy()->addHour(), 12.0, 90),
|
||||||
|
$this->createElectricityArray($now->copy()->addHours(2), 20.0, 60),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$gasResponse = collect([
|
||||||
|
$this->createGasArray($now->copy()->startOfDay(), 5.0),
|
||||||
|
$this->createGasArray($now->copy()->addDay(), 4.0),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$client = Mockery::mock(PriceClient::class);
|
||||||
|
$client->shouldReceive('getElectricityPrices')->once()->andReturn($electricityResponse);
|
||||||
|
$client->shouldReceive('getGasPrices')->once()->andReturn($gasResponse);
|
||||||
|
|
||||||
|
$this->app->bind(PriceClient::class, fn () => $client);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/energy-prices');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
|
||||||
|
$data = $response->json();
|
||||||
|
$this->assertEquals(15.0, $data['electricity']['totalPriceTaxIncluded']);
|
||||||
|
$this->assertEquals(12.0, $data['electricityCheapest']['totalPriceTaxIncluded']);
|
||||||
|
$this->assertEquals(20.0, $data['electricityPriciest']['totalPriceTaxIncluded']);
|
||||||
|
$this->assertEquals(5.0, $data['gas']['totalPriceTaxIncluded']);
|
||||||
|
$this->assertEquals(4.0, $data['gasCheapest']['totalPriceTaxIncluded']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_dashboard_endpoint_handles_empty_data(): void
|
||||||
|
{
|
||||||
|
$client = Mockery::mock(PriceClient::class);
|
||||||
|
$client->shouldReceive('getElectricityPrices')->once()->andReturn(collect());
|
||||||
|
$client->shouldReceive('getGasPrices')->once()->andReturn(collect());
|
||||||
|
|
||||||
|
$this->app->bind(PriceClient::class, fn () => $client);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/energy-prices');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
|
||||||
|
$data = $response->json();
|
||||||
|
$this->assertNull($data['electricity'] ?? null);
|
||||||
|
$this->assertNull($data['gas'] ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createElectricityArray(Carbon $startDate, float $price, int $sustainabilityScore): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'start_date' => $startDate->format('Y-m-d H:i:s'),
|
||||||
|
'end_date' => $startDate->copy()->addHour()->format('Y-m-d H:i:s'),
|
||||||
|
'period' => 'elec_hour',
|
||||||
|
'market_price' => (int) ($price * 100),
|
||||||
|
'total_price_tax_included' => $price,
|
||||||
|
'price_incl_handling_vat' => (int) ($price * 100),
|
||||||
|
'price_tax_with_vat' => (int) ($price * 20),
|
||||||
|
'pricing_profile' => 'dynamic',
|
||||||
|
'carbon_footprint_in_gram' => 500,
|
||||||
|
'sustainability_score' => $sustainabilityScore,
|
||||||
|
'start_date_datetime' => $startDate->format('Y-m-d H:i:s'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createGasArray(Carbon $startDate, float $price): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'start_date' => $startDate->format('Y-m-d H:i:s'),
|
||||||
|
'end_date' => $startDate->copy()->addDay()->format('Y-m-d H:i:s'),
|
||||||
|
'period' => 'gas_day',
|
||||||
|
'market_price' => (int) ($price * 100),
|
||||||
|
'total_price_tax_included' => $price,
|
||||||
|
'price_incl_handling_vat' => (int) ($price * 100),
|
||||||
|
'price_tax_with_vat' => (int) ($price * 20),
|
||||||
|
'start_date_datetime' => $startDate->format('Y-m-d H:i:s'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Api;
|
||||||
|
|
||||||
|
use App\Contracts\PriceClient;
|
||||||
|
use App\Data\ElectricityData;
|
||||||
|
use App\Data\GasData;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Mockery;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class HistoryApiTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
config(['cache_prices.ttl' => 60]);
|
||||||
|
config(['cache_prices.prefix' => 'test:']);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
Mockery::close();
|
||||||
|
\Illuminate\Support\Facades\Cache::flush();
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_history_endpoint_returns_electricity_and_gas_data(): void
|
||||||
|
{
|
||||||
|
$date = Carbon::today();
|
||||||
|
|
||||||
|
$electricityResponse = collect([
|
||||||
|
$this->createElectricityArray($date->copy()->setHour(0), 15.0),
|
||||||
|
$this->createElectricityArray($date->copy()->setHour(1), 16.0),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$gasResponse = collect([
|
||||||
|
$this->createGasArray($date, 5.0),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$client = Mockery::mock(PriceClient::class);
|
||||||
|
$client->shouldReceive('getElectricityPricesForDate')
|
||||||
|
->once()
|
||||||
|
->andReturn($electricityResponse);
|
||||||
|
$client->shouldReceive('getGasPricesForDate')
|
||||||
|
->once()
|
||||||
|
->andReturn($gasResponse);
|
||||||
|
|
||||||
|
$this->app->bind(PriceClient::class, fn () => $client);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/history?date=' . $date->format('Y-m-d'));
|
||||||
|
|
||||||
|
$response->assertOk()
|
||||||
|
->assertJsonStructure([
|
||||||
|
'electricity' => [
|
||||||
|
'*' => [
|
||||||
|
'startDate',
|
||||||
|
'endDate',
|
||||||
|
'period',
|
||||||
|
'totalPriceTaxIncluded',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'gas' => [
|
||||||
|
'*' => [
|
||||||
|
'startDate',
|
||||||
|
'endDate',
|
||||||
|
'period',
|
||||||
|
'totalPriceTaxIncluded',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_history_endpoint_returns_correct_data(): void
|
||||||
|
{
|
||||||
|
$date = Carbon::today();
|
||||||
|
|
||||||
|
$electricityResponse = collect([
|
||||||
|
$this->createElectricityArray($date->copy()->setHour(0), 15.0),
|
||||||
|
$this->createElectricityArray($date->copy()->setHour(1), 16.0),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$gasResponse = collect([
|
||||||
|
$this->createGasArray($date, 5.0),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$client = Mockery::mock(PriceClient::class);
|
||||||
|
$client->shouldReceive('getElectricityPricesForDate')
|
||||||
|
->once()
|
||||||
|
->andReturn($electricityResponse);
|
||||||
|
$client->shouldReceive('getGasPricesForDate')
|
||||||
|
->once()
|
||||||
|
->andReturn($gasResponse);
|
||||||
|
|
||||||
|
$this->app->bind(PriceClient::class, fn () => $client);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/history?date=' . $date->format('Y-m-d'));
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
|
||||||
|
$data = $response->json();
|
||||||
|
$this->assertCount(2, $data['electricity']);
|
||||||
|
$this->assertCount(1, $data['gas']);
|
||||||
|
$this->assertEquals(15.0, $data['electricity'][0]['totalPriceTaxIncluded']);
|
||||||
|
$this->assertEquals(5.0, $data['gas'][0]['totalPriceTaxIncluded']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_history_endpoint_defaults_to_today(): void
|
||||||
|
{
|
||||||
|
$client = Mockery::mock(PriceClient::class);
|
||||||
|
$client->shouldReceive('getElectricityPricesForDate')
|
||||||
|
->once()
|
||||||
|
->andReturn(collect());
|
||||||
|
$client->shouldReceive('getGasPricesForDate')
|
||||||
|
->once()
|
||||||
|
->andReturn(collect());
|
||||||
|
|
||||||
|
$this->app->bind(PriceClient::class, fn () => $client);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/history');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_history_endpoint_handles_empty_data(): void
|
||||||
|
{
|
||||||
|
$date = Carbon::today();
|
||||||
|
|
||||||
|
$client = Mockery::mock(PriceClient::class);
|
||||||
|
$client->shouldReceive('getElectricityPricesForDate')
|
||||||
|
->once()
|
||||||
|
->andReturn(collect());
|
||||||
|
$client->shouldReceive('getGasPricesForDate')
|
||||||
|
->once()
|
||||||
|
->andReturn(collect());
|
||||||
|
|
||||||
|
$this->app->bind(PriceClient::class, fn () => $client);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/history?date=' . $date->format('Y-m-d'));
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
|
||||||
|
$data = $response->json();
|
||||||
|
$this->assertEmpty($data['electricity']);
|
||||||
|
$this->assertEmpty($data['gas']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createElectricityArray(Carbon $startDate, float $price): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'start_date' => $startDate->format('Y-m-d H:i:s'),
|
||||||
|
'end_date' => $startDate->copy()->addHour()->format('Y-m-d H:i:s'),
|
||||||
|
'period' => 'elec_hour',
|
||||||
|
'market_price' => (int) ($price * 100),
|
||||||
|
'total_price_tax_included' => $price,
|
||||||
|
'price_incl_handling_vat' => (int) ($price * 100),
|
||||||
|
'price_tax_with_vat' => (int) ($price * 20),
|
||||||
|
'pricing_profile' => 'dynamic',
|
||||||
|
'carbon_footprint_in_gram' => 500,
|
||||||
|
'sustainability_score' => 80,
|
||||||
|
'start_date_datetime' => $startDate->format('Y-m-d H:i:s'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createGasArray(Carbon $startDate, float $price): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'start_date' => $startDate->format('Y-m-d H:i:s'),
|
||||||
|
'end_date' => $startDate->copy()->addDay()->format('Y-m-d H:i:s'),
|
||||||
|
'period' => 'gas_day',
|
||||||
|
'market_price' => (int) ($price * 100),
|
||||||
|
'total_price_tax_included' => $price,
|
||||||
|
'price_incl_handling_vat' => (int) ($price * 100),
|
||||||
|
'price_tax_with_vat' => (int) ($price * 20),
|
||||||
|
'start_date_datetime' => $startDate->format('Y-m-d H:i:s'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Clients;
|
||||||
|
|
||||||
|
use App\Clients\ZonneplanPriceClient;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Http\Client\Factory;
|
||||||
|
use Illuminate\Http\Client\RequestException;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Mockery;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ZonneplanPriceClientTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
Mockery::close();
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_electricity_prices_returns_collection(): void
|
||||||
|
{
|
||||||
|
$response = Mockery::mock();
|
||||||
|
$response->shouldReceive('throw')->once();
|
||||||
|
$response->shouldReceive('json')
|
||||||
|
->once()
|
||||||
|
->with('data', [])
|
||||||
|
->andReturn([
|
||||||
|
['start_date' => Carbon::now()->format('Y-m-d H:i:s'), 'total_price_tax_included' => 15.0],
|
||||||
|
]);
|
||||||
|
$response->shouldReceive('status')->once()->andReturn(200);
|
||||||
|
|
||||||
|
$http = Mockery::mock(Factory::class);
|
||||||
|
$http->shouldReceive('baseUrl')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('withQueryParameters')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('timeout')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('retry')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('get')->once()->andReturn($response);
|
||||||
|
|
||||||
|
$client = new ZonneplanPriceClient($http);
|
||||||
|
$result = $client->getElectricityPrices();
|
||||||
|
|
||||||
|
$this->assertCount(1, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_gas_prices_returns_collection(): void
|
||||||
|
{
|
||||||
|
$response = Mockery::mock();
|
||||||
|
$response->shouldReceive('throw')->once();
|
||||||
|
$response->shouldReceive('json')
|
||||||
|
->once()
|
||||||
|
->with('data', [])
|
||||||
|
->andReturn([
|
||||||
|
['start_date' => Carbon::now()->format('Y-m-d H:i:s'), 'total_price_tax_included' => 5.0],
|
||||||
|
]);
|
||||||
|
$response->shouldReceive('status')->once()->andReturn(200);
|
||||||
|
|
||||||
|
$http = Mockery::mock(Factory::class);
|
||||||
|
$http->shouldReceive('baseUrl')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('withQueryParameters')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('timeout')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('retry')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('get')->once()->andReturn($response);
|
||||||
|
|
||||||
|
$client = new ZonneplanPriceClient($http);
|
||||||
|
$result = $client->getGasPrices();
|
||||||
|
|
||||||
|
$this->assertCount(1, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_electricity_prices_for_date_returns_collection(): void
|
||||||
|
{
|
||||||
|
$date = Carbon::today();
|
||||||
|
$response = Mockery::mock();
|
||||||
|
$response->shouldReceive('throw')->once();
|
||||||
|
$response->shouldReceive('json')
|
||||||
|
->once()
|
||||||
|
->with('data', [])
|
||||||
|
->andReturn([
|
||||||
|
['start_date' => $date->format('Y-m-d H:i:s'), 'total_price_tax_included' => 15.0],
|
||||||
|
]);
|
||||||
|
$response->shouldReceive('status')->once()->andReturn(200);
|
||||||
|
|
||||||
|
$http = Mockery::mock(Factory::class);
|
||||||
|
$http->shouldReceive('baseUrl')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('withQueryParameters')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('timeout')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('retry')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('get')->once()->andReturn($response);
|
||||||
|
|
||||||
|
$client = new ZonneplanPriceClient($http);
|
||||||
|
$result = $client->getElectricityPricesForDate($date);
|
||||||
|
|
||||||
|
$this->assertCount(1, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_gas_prices_for_date_returns_collection(): void
|
||||||
|
{
|
||||||
|
$date = Carbon::today();
|
||||||
|
$response = Mockery::mock();
|
||||||
|
$response->shouldReceive('throw')->once();
|
||||||
|
$response->shouldReceive('json')
|
||||||
|
->once()
|
||||||
|
->with('data', [])
|
||||||
|
->andReturn([
|
||||||
|
['start_date' => $date->format('Y-m-d H:i:s'), 'total_price_tax_included' => 5.0],
|
||||||
|
]);
|
||||||
|
$response->shouldReceive('status')->once()->andReturn(200);
|
||||||
|
|
||||||
|
$http = Mockery::mock(Factory::class);
|
||||||
|
$http->shouldReceive('baseUrl')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('withQueryParameters')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('timeout')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('retry')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('get')->once()->andReturn($response);
|
||||||
|
|
||||||
|
$client = new ZonneplanPriceClient($http);
|
||||||
|
$result = $client->getGasPricesForDate($date);
|
||||||
|
|
||||||
|
$this->assertCount(1, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_client_throws_exception_on_api_failure(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\Exception::class);
|
||||||
|
$this->expectExceptionMessage('Kon energieprijzen niet ophalen');
|
||||||
|
|
||||||
|
$response = Mockery::mock();
|
||||||
|
$response->shouldReceive('throw')->once()->andThrow(
|
||||||
|
new \Illuminate\Http\Client\ConnectionException('API Error')
|
||||||
|
);
|
||||||
|
|
||||||
|
$http = Mockery::mock(Factory::class);
|
||||||
|
$http->shouldReceive('baseUrl')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('withQueryParameters')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('timeout')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('retry')->once()->andReturnSelf();
|
||||||
|
$http->shouldReceive('get')->once()->andReturn($response);
|
||||||
|
|
||||||
|
$client = new ZonneplanPriceClient($http);
|
||||||
|
$client->getElectricityPrices();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Repositories;
|
||||||
|
|
||||||
|
use App\Contracts\PriceClient;
|
||||||
|
use App\Data\ElectricityData;
|
||||||
|
use App\Data\GasData;
|
||||||
|
use App\Repositories\ZonneplanPriceRepository;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Mockery;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ZonneplanPriceRepositoryTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
config(['cache_prices.ttl' => 60]);
|
||||||
|
config(['cache_prices.prefix' => 'test:']);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
Mockery::close();
|
||||||
|
Cache::flush();
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_retrieve_electricity_returns_collection_of_electricity_data(): void
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
$electricityResponse = collect([
|
||||||
|
[
|
||||||
|
'start_date' => $now->format('Y-m-d H:i:s'),
|
||||||
|
'end_date' => $now->copy()->addHour()->format('Y-m-d H:i:s'),
|
||||||
|
'period' => 'elec_hour',
|
||||||
|
'market_price' => 1500,
|
||||||
|
'total_price_tax_included' => 15.50,
|
||||||
|
'price_incl_handling_vat' => 1550,
|
||||||
|
'price_tax_with_vat' => 300,
|
||||||
|
'pricing_profile' => 'dynamic',
|
||||||
|
'carbon_footprint_in_gram' => 500,
|
||||||
|
'sustainability_score' => 80,
|
||||||
|
'start_date_datetime' => $now->format('Y-m-d H:i:s'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$client = Mockery::mock(PriceClient::class);
|
||||||
|
$client->shouldReceive('getElectricityPrices')
|
||||||
|
->once()
|
||||||
|
->andReturn($electricityResponse);
|
||||||
|
|
||||||
|
$repository = new ZonneplanPriceRepository($client);
|
||||||
|
$result = $repository->retrieveElectricity();
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Collection::class, $result);
|
||||||
|
$this->assertCount(1, $result);
|
||||||
|
$this->assertInstanceOf(ElectricityData::class, $result->first());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_retrieve_gas_returns_collection_of_gas_data(): void
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
$gasResponse = collect([
|
||||||
|
[
|
||||||
|
'start_date' => $now->format('Y-m-d H:i:s'),
|
||||||
|
'end_date' => $now->copy()->addDay()->format('Y-m-d H:i:s'),
|
||||||
|
'period' => 'gas_day',
|
||||||
|
'market_price' => 500,
|
||||||
|
'total_price_tax_included' => 5.50,
|
||||||
|
'price_incl_handling_vat' => 550,
|
||||||
|
'price_tax_with_vat' => 100,
|
||||||
|
'start_date_datetime' => $now->format('Y-m-d H:i:s'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$client = Mockery::mock(PriceClient::class);
|
||||||
|
$client->shouldReceive('getGasPrices')
|
||||||
|
->once()
|
||||||
|
->andReturn($gasResponse);
|
||||||
|
|
||||||
|
$repository = new ZonneplanPriceRepository($client);
|
||||||
|
$result = $repository->retrieveGas();
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Collection::class, $result);
|
||||||
|
$this->assertCount(1, $result);
|
||||||
|
$this->assertInstanceOf(GasData::class, $result->first());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_retrieve_electricity_for_date_returns_collection(): void
|
||||||
|
{
|
||||||
|
$date = Carbon::today();
|
||||||
|
$electricityResponse = collect([
|
||||||
|
[
|
||||||
|
'start_date' => $date->copy()->setHour(0)->format('Y-m-d H:i:s'),
|
||||||
|
'end_date' => $date->copy()->setHour(1)->format('Y-m-d H:i:s'),
|
||||||
|
'period' => 'elec_hour',
|
||||||
|
'market_price' => 1500,
|
||||||
|
'total_price_tax_included' => 15.50,
|
||||||
|
'price_incl_handling_vat' => 1550,
|
||||||
|
'price_tax_with_vat' => 300,
|
||||||
|
'pricing_profile' => 'dynamic',
|
||||||
|
'carbon_footprint_in_gram' => 500,
|
||||||
|
'sustainability_score' => 80,
|
||||||
|
'start_date_datetime' => $date->copy()->setHour(0)->format('Y-m-d H:i:s'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$client = Mockery::mock(PriceClient::class);
|
||||||
|
$client->shouldReceive('getElectricityPricesForDate')
|
||||||
|
->once()
|
||||||
|
->with($date)
|
||||||
|
->andReturn($electricityResponse);
|
||||||
|
|
||||||
|
$repository = new ZonneplanPriceRepository($client);
|
||||||
|
$result = $repository->retrieveElectricityForDate($date);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Collection::class, $result);
|
||||||
|
$this->assertCount(1, $result);
|
||||||
|
$this->assertInstanceOf(ElectricityData::class, $result->first());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_retrieve_gas_for_date_returns_collection(): void
|
||||||
|
{
|
||||||
|
$date = Carbon::today();
|
||||||
|
$gasResponse = collect([
|
||||||
|
[
|
||||||
|
'start_date' => $date->format('Y-m-d H:i:s'),
|
||||||
|
'end_date' => $date->copy()->addDay()->format('Y-m-d H:i:s'),
|
||||||
|
'period' => 'gas_day',
|
||||||
|
'market_price' => 500,
|
||||||
|
'total_price_tax_included' => 5.50,
|
||||||
|
'price_incl_handling_vat' => 550,
|
||||||
|
'price_tax_with_vat' => 100,
|
||||||
|
'start_date_datetime' => $date->format('Y-m-d H:i:s'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$client = Mockery::mock(PriceClient::class);
|
||||||
|
$client->shouldReceive('getGasPricesForDate')
|
||||||
|
->once()
|
||||||
|
->with($date)
|
||||||
|
->andReturn($gasResponse);
|
||||||
|
|
||||||
|
$repository = new ZonneplanPriceRepository($client);
|
||||||
|
$result = $repository->retrieveGasForDate($date);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Collection::class, $result);
|
||||||
|
$this->assertCount(1, $result);
|
||||||
|
$this->assertInstanceOf(GasData::class, $result->first());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_retrieve_electricity_caches_result(): void
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
$electricityResponse = collect([
|
||||||
|
[
|
||||||
|
'start_date' => $now->format('Y-m-d H:i:s'),
|
||||||
|
'end_date' => $now->copy()->addHour()->format('Y-m-d H:i:s'),
|
||||||
|
'period' => 'elec_hour',
|
||||||
|
'market_price' => 1500,
|
||||||
|
'total_price_tax_included' => 15.50,
|
||||||
|
'price_incl_handling_vat' => 1550,
|
||||||
|
'price_tax_with_vat' => 300,
|
||||||
|
'pricing_profile' => 'dynamic',
|
||||||
|
'carbon_footprint_in_gram' => 500,
|
||||||
|
'sustainability_score' => 80,
|
||||||
|
'start_date_datetime' => $now->format('Y-m-d H:i:s'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$client = Mockery::mock(PriceClient::class);
|
||||||
|
$client->shouldReceive('getElectricityPrices')
|
||||||
|
->once()
|
||||||
|
->andReturn($electricityResponse);
|
||||||
|
|
||||||
|
$repository = new ZonneplanPriceRepository($client);
|
||||||
|
|
||||||
|
$result1 = $repository->retrieveElectricity();
|
||||||
|
$result2 = $repository->retrieveElectricity();
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
$result1->first()->totalPriceTaxIncluded,
|
||||||
|
$result2->first()->totalPriceTaxIncluded
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Services;
|
||||||
|
|
||||||
|
use App\Contracts\PriceRepository;
|
||||||
|
use App\Data\ElectricityData;
|
||||||
|
use App\Data\GasData;
|
||||||
|
use App\Features\Dashboard\Services\DashboardService;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Mockery;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class DashboardServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
Mockery::close();
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_dashboard_data_returns_current_cheapest_sustainable_and_expensive_prices(): void
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
|
||||||
|
$electricityData = collect([
|
||||||
|
$this->createElectricityData($now->copy()->subMinutes(30), 15.0, 80),
|
||||||
|
$this->createElectricityData($now->copy()->addHour(), 12.0, 90),
|
||||||
|
$this->createElectricityData($now->copy()->addHours(2), 20.0, 60),
|
||||||
|
$this->createElectricityData($now->copy()->addHours(3), 10.0, 95),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$gasData = collect([
|
||||||
|
$this->createGasData($now->copy()->startOfDay(), 5.0),
|
||||||
|
$this->createGasData($now->copy()->addDay(), 4.0),
|
||||||
|
$this->createGasData($now->copy()->addDays(2), 6.0),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$repository = Mockery::mock(PriceRepository::class);
|
||||||
|
$repository->shouldReceive('retrieveElectricity')->once()->andReturn($electricityData);
|
||||||
|
$repository->shouldReceive('retrieveGas')->once()->andReturn($gasData);
|
||||||
|
|
||||||
|
$service = new DashboardService($repository);
|
||||||
|
$result = $service->getDashboardData();
|
||||||
|
|
||||||
|
$this->assertEquals(15.0, $result->electricity->totalPriceTaxIncluded);
|
||||||
|
$this->assertEquals(10.0, $result->electricityCheapest->totalPriceTaxIncluded);
|
||||||
|
$this->assertEquals(95, $result->electricityDurable->sustainabilityScore);
|
||||||
|
$this->assertEquals(20.0, $result->electricityPriciest->totalPriceTaxIncluded);
|
||||||
|
|
||||||
|
$this->assertEquals(5.0, $result->gas->totalPriceTaxIncluded);
|
||||||
|
$this->assertEquals(4.0, $result->gasCheapest->totalPriceTaxIncluded);
|
||||||
|
$this->assertEquals(6.0, $result->gasPriciest->totalPriceTaxIncluded);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_dashboard_data_returns_null_for_missing_current_price(): void
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
|
||||||
|
$electricityData = collect([
|
||||||
|
$this->createElectricityData($now->copy()->addHours(5), 15.0, 80),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$gasData = collect([
|
||||||
|
$this->createGasData($now->copy()->addDays(5), 5.0),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$repository = Mockery::mock(PriceRepository::class);
|
||||||
|
$repository->shouldReceive('retrieveElectricity')->once()->andReturn($electricityData);
|
||||||
|
$repository->shouldReceive('retrieveGas')->once()->andReturn($gasData);
|
||||||
|
|
||||||
|
$service = new DashboardService($repository);
|
||||||
|
$result = $service->getDashboardData();
|
||||||
|
|
||||||
|
$this->assertNull($result->electricity);
|
||||||
|
$this->assertNull($result->gas);
|
||||||
|
$this->assertNotNull($result->electricityCheapest);
|
||||||
|
$this->assertNotNull($result->gasCheapest);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_dashboard_data_with_empty_collections(): void
|
||||||
|
{
|
||||||
|
$repository = Mockery::mock(PriceRepository::class);
|
||||||
|
$repository->shouldReceive('retrieveElectricity')->once()->andReturn(collect());
|
||||||
|
$repository->shouldReceive('retrieveGas')->once()->andReturn(collect());
|
||||||
|
|
||||||
|
$service = new DashboardService($repository);
|
||||||
|
$result = $service->getDashboardData();
|
||||||
|
|
||||||
|
$this->assertNull($result->electricity);
|
||||||
|
$this->assertNull($result->electricityCheapest);
|
||||||
|
$this->assertNull($result->electricityDurable);
|
||||||
|
$this->assertNull($result->electricityPriciest);
|
||||||
|
$this->assertNull($result->gas);
|
||||||
|
$this->assertNull($result->gasCheapest);
|
||||||
|
$this->assertNull($result->gasPriciest);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createElectricityData(Carbon $startDate, float $price, int $sustainabilityScore): ElectricityData
|
||||||
|
{
|
||||||
|
return new ElectricityData(
|
||||||
|
startDate: $startDate,
|
||||||
|
endDate: $startDate->copy()->addHour(),
|
||||||
|
period: 'elec_hour',
|
||||||
|
marketPrice: (int) ($price * 100),
|
||||||
|
totalPriceTaxIncluded: $price,
|
||||||
|
priceInclHandlingVat: (int) ($price * 100),
|
||||||
|
priceTaxWithVat: (int) ($price * 20),
|
||||||
|
pricingProfile: 'dynamic',
|
||||||
|
carbonFootprintInGram: 500,
|
||||||
|
sustainabilityScore: $sustainabilityScore,
|
||||||
|
startDateDatetime: $startDate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createGasData(Carbon $startDate, float $price): GasData
|
||||||
|
{
|
||||||
|
return new GasData(
|
||||||
|
startDate: $startDate,
|
||||||
|
endDate: $startDate->copy()->addDay(),
|
||||||
|
period: 'gas_day',
|
||||||
|
marketPrice: (int) ($price * 100),
|
||||||
|
totalPriceTaxIncluded: $price,
|
||||||
|
priceInclHandlingVat: (int) ($price * 100),
|
||||||
|
priceTaxWithVat: (int) ($price * 20),
|
||||||
|
startDateDatetime: $startDate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Services;
|
||||||
|
|
||||||
|
use App\Contracts\PriceRepository;
|
||||||
|
use App\Data\ElectricityData;
|
||||||
|
use App\Data\GasData;
|
||||||
|
use App\Features\History\Services\HistoryService;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Mockery;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class HistoryServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
Mockery::close();
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_history_data_returns_electricity_and_gas_for_date(): void
|
||||||
|
{
|
||||||
|
$date = Carbon::today();
|
||||||
|
|
||||||
|
$electricityData = collect([
|
||||||
|
$this->createElectricityData($date->copy()->setHour(0), 15.0),
|
||||||
|
$this->createElectricityData($date->copy()->setHour(1), 16.0),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$gasData = collect([
|
||||||
|
$this->createGasData($date, 5.0),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$repository = Mockery::mock(PriceRepository::class);
|
||||||
|
$repository->shouldReceive('retrieveElectricityForDate')
|
||||||
|
->once()
|
||||||
|
->with($date)
|
||||||
|
->andReturn($electricityData);
|
||||||
|
$repository->shouldReceive('retrieveGasForDate')
|
||||||
|
->once()
|
||||||
|
->with($date)
|
||||||
|
->andReturn($gasData);
|
||||||
|
|
||||||
|
$service = new HistoryService($repository);
|
||||||
|
$result = $service->getHistoryData($date);
|
||||||
|
|
||||||
|
$this->assertCount(2, $result->electricity);
|
||||||
|
$this->assertCount(1, $result->gas);
|
||||||
|
$this->assertEquals(15.0, $result->electricity->first()->totalPriceTaxIncluded);
|
||||||
|
$this->assertEquals(5.0, $result->gas->first()->totalPriceTaxIncluded);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_history_data_with_empty_collections(): void
|
||||||
|
{
|
||||||
|
$date = Carbon::today();
|
||||||
|
|
||||||
|
$repository = Mockery::mock(PriceRepository::class);
|
||||||
|
$repository->shouldReceive('retrieveElectricityForDate')
|
||||||
|
->once()
|
||||||
|
->andReturn(collect());
|
||||||
|
$repository->shouldReceive('retrieveGasForDate')
|
||||||
|
->once()
|
||||||
|
->andReturn(collect());
|
||||||
|
|
||||||
|
$service = new HistoryService($repository);
|
||||||
|
$result = $service->getHistoryData($date);
|
||||||
|
|
||||||
|
$this->assertCount(0, $result->electricity);
|
||||||
|
$this->assertCount(0, $result->gas);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createElectricityData(Carbon $startDate, float $price): ElectricityData
|
||||||
|
{
|
||||||
|
return new ElectricityData(
|
||||||
|
startDate: $startDate,
|
||||||
|
endDate: $startDate->copy()->addHour(),
|
||||||
|
period: 'elec_hour',
|
||||||
|
marketPrice: (int) ($price * 100),
|
||||||
|
totalPriceTaxIncluded: $price,
|
||||||
|
priceInclHandlingVat: (int) ($price * 100),
|
||||||
|
priceTaxWithVat: (int) ($price * 20),
|
||||||
|
pricingProfile: 'dynamic',
|
||||||
|
carbonFootprintInGram: 500,
|
||||||
|
sustainabilityScore: 80,
|
||||||
|
startDateDatetime: $startDate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createGasData(Carbon $startDate, float $price): GasData
|
||||||
|
{
|
||||||
|
return new GasData(
|
||||||
|
startDate: $startDate,
|
||||||
|
endDate: $startDate->copy()->addDay(),
|
||||||
|
period: 'gas_day',
|
||||||
|
marketPrice: (int) ($price * 100),
|
||||||
|
totalPriceTaxIncluded: $price,
|
||||||
|
priceInclHandlingVat: (int) ($price * 100),
|
||||||
|
priceTaxWithVat: (int) ($price * 20),
|
||||||
|
startDateDatetime: $startDate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user