Account chart can do live update

This commit is contained in:
James Cole
2025-07-27 07:36:23 +02:00
parent a16cc73c77
commit 28b2ddde18
7 changed files with 142 additions and 66 deletions

View File

@@ -24,16 +24,16 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Chart;
use FireflyIII\Exceptions\ValidationException;
use FireflyIII\Models\TransactionCurrency;
use Carbon\Carbon;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Chart\ChartRequest;
use FireflyIII\Api\V1\Requests\Data\DateRequest;
use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Exceptions\ValidationException;
use FireflyIII\Models\Account;
use FireflyIII\Models\Preference;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Support\Chart\ChartData;
use FireflyIII\Support\Facades\Preferences;
@@ -42,6 +42,7 @@ use FireflyIII\Support\Http\Api\ApiSupport;
use FireflyIII\Support\Http\Api\CollectsAccountsFromFilter;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
/**
* Class AccountController
@@ -90,6 +91,7 @@ class AccountController extends Controller
// loop each account, and collect info:
/** @var Account $account */
foreach ($accounts as $account) {
Log::debug(sprintf('Account #%d ("%s")', $account->id, $account->name));
$this->renderAccountData($queryParameters, $account);
}
@@ -101,15 +103,22 @@ class AccountController extends Controller
*/
private function renderAccountData(array $params, Account $account): void
{
$currency = $this->repository->getAccountCurrency($account);
Log::debug(sprintf('Now in %s(array, #%d)', __METHOD__, $account->id));
$currency = $this->repository->getAccountCurrency($account);
$currentStart = clone $params['start'];
$range = Steam::finalAccountBalanceInRange($account, $params['start'], clone $params['end'], $this->convertToNative);
$previous = array_values($range)[0]['balance'];
$nativePrevious = null;
if (!$currency instanceof TransactionCurrency) {
$currency = $this->default;
}
$currentSet = [
$currentSet = [
'label' => $account->name,
// the currency that belongs to the account.
'currency_id' => (string) $currency->id,
'currency_id' => (string)$currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
@@ -121,18 +130,33 @@ class AccountController extends Controller
'period' => '1D',
'entries' => [],
];
$currentStart = clone $params['start'];
$range = Steam::finalAccountBalanceInRange($account, $params['start'], clone $params['end'], $this->convertToNative);
if ($this->convertToNative) {
$currentSet['native_entries'] = [];
$currentSet['native_currency_id'] = (string)$this->nativeCurrency->id;
$currentSet['native_currency_code'] = $this->nativeCurrency->code;
$currentSet['native_currency_symbol'] = $this->nativeCurrency->symbol;
$currentSet['native_currency_decimal_places'] = $this->nativeCurrency->decimal_places;
$nativePrevious = array_values($range)[0]['native_balance'];
}
$previous = array_values($range)[0]['balance'];
while ($currentStart <= $params['end']) {
$format = $currentStart->format('Y-m-d');
$label = $currentStart->toAtomString();
$balance = array_key_exists($format, $range) ? $range[$format]['balance'] : $previous;
$previous = $balance;
$format = $currentStart->format('Y-m-d');
$label = $currentStart->toAtomString();
$balance = array_key_exists($format, $range) ? $range[$format]['balance'] : $previous;
$previous = $balance;
$currentSet['entries'][$label] = $balance;
// do the same for the native balance, if relevant:
$nativeBalance = null;
if ($this->convertToNative) {
$nativeBalance = array_key_exists($format, $range) ? $range[$format]['native_balance'] : $nativePrevious;
$nativePrevious = $nativeBalance;
$currentSet['native_entries'][$label] = $nativeBalance;
}
$currentStart->addDay();
$currentSet['entries'][$label] = $balance;
}
$this->chartData->add($currentSet);
}
@@ -146,40 +170,34 @@ class AccountController extends Controller
public function overview(DateRequest $request): JsonResponse
{
// parameters for chart:
$dates = $request->getAll();
$dates = $request->getAll();
/** @var Carbon $start */
$start = $dates['start'];
$start = $dates['start'];
/** @var Carbon $end */
$end = $dates['end'];
$end = $dates['end'];
// set dates to end of day + start of day:
$start->startOfDay();
$end->endOfDay();
// user's preferences
$defaultSet = $this->repository->getAccountsByType([AccountTypeEnum::ASSET->value])->pluck('id')->toArray();
/** @var Preference $frontpage */
$frontpage = Preferences::get('frontpageAccounts', $defaultSet);
if (!(is_array($frontpage->data) && count($frontpage->data) > 0)) {
$frontpage->data = $defaultSet;
$frontpage->save();
}
// get accounts:
$accounts = $this->repository->getAccountsById($frontpage->data);
$chartData = [];
$frontPageIds = $this->getFrontPageAccountIds();
$accounts = $this->repository->getAccountsById($frontPageIds);
$chartData = [];
/** @var Account $account */
foreach ($accounts as $account) {
$currency = $this->repository->getAccountCurrency($account) ?? $this->nativeCurrency;
$field = $this->convertToNative && $currency->id !== $this->nativeCurrency->id ? 'native_balance' : 'balance';
$currentSet = [
Log::debug(sprintf('Rendering chart data for account %s (%d)', $account->name, $account->id));
$currency = $this->repository->getAccountCurrency($account) ?? $this->nativeCurrency;
$currentStart = clone $start;
$range = Steam::finalAccountBalanceInRange($account, $start, clone $end, $this->convertToNative);
$previous = array_values($range)[0]['balance'];
$nativePrevious = null;
$currentSet = [
'label' => $account->name,
'currency_id' => (string) $currency->id,
'currency_id' => (string)$currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
@@ -189,21 +207,56 @@ class AccountController extends Controller
'yAxisID' => 0, // 0, 1, 2
'entries' => [],
];
// TODO this code is also present in the V2 chart account controller so this method is due to be deprecated.
$currentStart = clone $start;
$range = Steam::finalAccountBalanceInRange($account, $start, clone $end, $this->convertToNative);
$previous = array_values($range)[0][$field];
while ($currentStart <= $end) {
$format = $currentStart->format('Y-m-d');
$label = $currentStart->toAtomString();
$balance = array_key_exists($format, $range) ? $range[$format][$field] : $previous;
$previous = $balance;
$currentStart->addDay();
$currentSet['entries'][$label] = $balance;
// add "native_entries" if convertToNative is true:
if ($this->convertToNative) {
$currentSet['native_entries'] = [];
$currentSet['native_currency_id'] = (string)$this->nativeCurrency->id;
$currentSet['native_currency_code'] = $this->nativeCurrency->code;
$currentSet['native_currency_symbol'] = $this->nativeCurrency->symbol;
$currentSet['native_currency_decimal_places'] = $this->nativeCurrency->decimal_places;
$nativePrevious = array_values($range)[0]['native_balance'];
}
$chartData[] = $currentSet;
// also get the native balance if convertToNative is true:
while ($currentStart <= $end) {
$format = $currentStart->format('Y-m-d');
$label = $currentStart->toAtomString();
// balance is based on "balance" from the $range variable.
$balance = array_key_exists($format, $range) ? $range[$format]['balance'] : $previous;
$previous = $balance;
$currentSet['entries'][$label] = $balance;
// do the same for the native balance, if relevant:
$nativeBalance = null;
if ($this->convertToNative) {
$nativeBalance = array_key_exists($format, $range) ? $range[$format]['native_balance'] : $nativePrevious;
$nativePrevious = $nativeBalance;
$currentSet['native_entries'][$label] = $nativeBalance;
}
$currentStart->addDay();
}
$chartData[] = $currentSet;
}
return response()->json($chartData);
}
private function getFrontPageAccountIds(): array
{
$defaultSet = $this->repository->getAccountsByType([AccountTypeEnum::ASSET->value])->pluck('id')->toArray();
/** @var Preference $frontpage */
$frontpage = Preferences::get('frontpageAccounts', $defaultSet);
if (!(is_array($frontpage->data) && count($frontpage->data) > 0)) {
$frontpage->data = $defaultSet;
$frontpage->save();
}
return $frontpage->data ?? $defaultSet;
}
}

View File

@@ -120,8 +120,6 @@ class Amount
if (!$user instanceof User) {
return true === Preferences::get('convert_to_native', false)->data && true === config('cer.enabled');
}
Log::debug('convertToNative setting', Preferences::getForUser($user, 'convert_to_native', false));
return true === Preferences::getForUser($user, 'convert_to_native', false)->data && true === config('cer.enabled');
}

View File

@@ -93,7 +93,7 @@ class Steam
return [];
}
$defaultCurrency = app('amount')->getNativeCurrency();
$defaultCurrency = Amount::getNativeCurrency();
if ($convertToNative) {
if ($defaultCurrency->id === $currency?->id) {
Log::debug(sprintf('Unset [%s] for account #%d (no longer unset "native_balance")', $defaultCurrency->code, $account->id));
@@ -224,7 +224,7 @@ class Steam
$request->subDay()->endOfDay();
Log::debug(sprintf('finalAccountBalanceInRange: Call finalAccountBalance with date/time "%s"', $request->toIso8601String()));
$startBalance = $this->finalAccountBalance($account, $request);
$nativeCurrency = app('amount')->getNativeCurrencyByUserGroup($account->user->userGroup);
$nativeCurrency = Amount::getNativeCurrencyByUserGroup($account->user->userGroup);
$accountCurrency = $this->getAccountCurrency($account);
$hasCurrency = $accountCurrency instanceof TransactionCurrency;
$currency = $accountCurrency ?? $nativeCurrency;
@@ -294,7 +294,7 @@ class Steam
$currentBalance[$entryCurrency->code] ??= '0';
$currentBalance[$entryCurrency->code] = bcadd($sumOfDay, (string) $currentBalance[$entryCurrency->code]);
// if not convert to native, add the amount to "balance", do nothing else.
// if not requested to convert to native, add the amount to "balance", do nothing else.
if (!$convertToNative) {
$currentBalance['balance'] = bcadd((string) $currentBalance['balance'], $sumOfDay);
}
@@ -302,13 +302,13 @@ class Steam
// if there is a request to convert, convert to "native_balance" and use "balance" for whichever amount is in the native currency.
if ($convertToNative) {
$nativeSumOfDay = $converter->convert($entryCurrency, $nativeCurrency, $carbon, $sumOfDay);
$currentBalance['native_balance'] = bcadd((string) $currentBalance['native_balance'], $nativeSumOfDay);
$currentBalance['native_balance'] = bcadd((string) ($currentBalance['native_balance'] ?? '0'), $nativeSumOfDay);
// if it's the same currency as the entry, also add to balance (see other code).
if ($currency->id === $entryCurrency->id) {
$currentBalance['balance'] = bcadd((string) $currentBalance['balance'], $sumOfDay);
}
}
// just set it.
// add to final array.
$balances[$carbonKey] = $currentBalance;
Log::debug(sprintf('Updated entry [%s]', $carbonKey), $currentBalance);
}

View File

@@ -53,6 +53,11 @@ export default () => ({
eventListeners: {
['@convert-to-native.window'](event){
console.log('I heard that! it is now ' + event.detail);
this.convertToNative = event.detail;
this.accountList = [];
chartData = null;
this.loadChart();
this.loadAccounts();
}
},
@@ -61,6 +66,7 @@ export default () => ({
console.log('doSomeReload');
},
getFreshData() {
console.log('get fresh data');
const start = new Date(window.store.get('start'));
const end = new Date(window.store.get('end'));
const chartCacheKey = getCacheKey(this.localCacheKey('chart'), {start: start, end: end})
@@ -101,18 +107,20 @@ export default () => ({
dataset.label = current.label;
// use the "native" currency code and use the "native_entries" as array
// if (this.convertToNative) {
// currencies.push(current.native_currency_code);
// dataset.currency_code = current.native_currency_code;
// collection = Object.values(current.native_entries);
// yAxis = 'y' + current.native_currency_code;
// }
// if (!this.convertToNative) {
if (this.convertToNative) {
console.log('Convert to native!');
currencies.push(current.native_currency_code);
dataset.currency_code = current.native_currency_code;
collection = Object.values(current.native_entries);
yAxis = 'y' + current.native_currency_code;
}
if (!this.convertToNative) {
console.log('NO convert to native!', this.convertToNative);
yAxis = 'y' + current.currency_code;
dataset.currency_code = current.currency_code;
currencies.push(current.currency_code);
collection = Object.values(current.entries);
// }
}
dataset.yAxisID = yAxis;
dataset.data = collection;
@@ -147,7 +155,9 @@ export default () => ({
return options;
},
loadChart() {
console.log('loadChart');
if (true === this.loading) {
console.log('already loading chart');
return;
}
this.loading = true;
@@ -165,6 +175,7 @@ export default () => ({
chart.options = options.options;
chart.data = options.data;
chart.update();
console.log('refresh chart');
return;
}
chart = new Chart(document.querySelector("#account-chart"), options);
@@ -294,6 +305,7 @@ export default () => ({
this.convertToNative = values[1] && values[3];
this.convertToNativeAvailable = values[3];
afterPromises = true;
console.log('convertToNative in accounts.js: ', values);
// main dashboard chart:
this.loadChart();

View File

@@ -78,7 +78,6 @@ let index = function () {
init() {
Promise.all([getVariable('convert_to_native', false)]).then((values) => {
this.convertToNative = values[0];
console.log('convert_to_native: ' + this.convertToNative);
});
}
}

View File

@@ -27,19 +27,29 @@ export function getConfiguration(name, defaultValue = null) {
// to make things available quicker than if the store has to grab it through the API.
// then again, it's not that slow.
if (validCache && window.hasOwnProperty(name)) {
// console.log('Get from window');
console.log('Return configuration "' + name + '" from window: ' + window[name]);
return Promise.resolve(window[name]);
}
// load from store2, if it's present.
const fromStore = window.store.get(name);
if (validCache && typeof fromStore !== 'undefined') {
console.log('Return configuration "' + name + '" from store: ' + fromStore);
return Promise.resolve(fromStore);
}
let getter = (new Get);
return getter.getByName(name).then((response) => {
// console.log('Get "' + name + '" from API');
return Promise.resolve(parseResponse(name, response));
}).catch(() => {
console.log('Return configuration "' + name + '" from API: ' + parseConfigurationResponse(name, response));
return Promise.resolve(parseConfigurationResponse(name, response));
}).catch((error) => {
console.log('Returning "'+name+'" from DEFAULT: ' + defaultValue);
console.warn(error);
return defaultValue;
});
}
export function parseConfigurationResponse(name, response) {
let value = response.data.data.value;
window.store.set(name, value);
return value;
}

View File

@@ -27,16 +27,19 @@ export function getVariable(name, defaultValue = null) {
// to make things available quicker than if the store has to grab it through the API.
// then again, it's not that slow.
if (validCache && window.hasOwnProperty(name)) {
console.log('Returning "'+name+'" from window: ' + window[name]);
return Promise.resolve(window[name]);
}
// load from store2, if it's present.
const fromStore = window.store.get(name);
if (validCache && typeof fromStore !== 'undefined') {
console.log('Returning "'+name+'" from store: ' + fromStore);
return Promise.resolve(fromStore);
}
let getter = (new Get);
return getter.getByName(name).then((response) => {
console.log('Returning "'+name+'" from server: ' + parseResponse(name, response));
return Promise.resolve(parseResponse(name, response));
}).catch((error) => {
if('' === defaultValue) {
@@ -47,6 +50,7 @@ export function getVariable(name, defaultValue = null) {
// POST it and then return it anyway.
let poster = (new Post);
return poster.post(name, defaultValue).then((response) => {
console.log('Returning "'+name+'" from POST: ' + parseResponse(name, response));
return Promise.resolve(parseResponse(name, response));
});
});