diff --git a/app/Api/V1/Controllers/Chart/AccountController.php b/app/Api/V1/Controllers/Chart/AccountController.php new file mode 100644 index 0000000000..2bb85a0530 --- /dev/null +++ b/app/Api/V1/Controllers/Chart/AccountController.php @@ -0,0 +1,108 @@ +middleware( + function ($request, $next) { + /** @var User $user */ + $user = auth()->user(); + $this->repository = app(AccountRepositoryInterface::class); + $this->repository->setUser($user); + + return $next($request); + } + ); + } + + /** + * @param Request $request + * + * @return JsonResponse + * @throws FireflyException + */ + public function overview(Request $request): JsonResponse + { + // parameters for chart: + $start = (string)$request->get('start'); + $end = (string)$request->get('end'); + if ('' === $start || '' === $end) { + throw new FireflyException('Start and end are mandatory parameters.'); + } + + $start = Carbon::createFromFormat('Y-m-d', $start); + $end = Carbon::createFromFormat('Y-m-d', $end); + + // user's preferences + $defaultSet = $this->repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET])->pluck('id')->toArray(); + $frontPage = app('preferences')->get('frontPageAccounts', $defaultSet); + $default = app('amount')->getDefaultCurrency(); + if (0 === \count($frontPage->data)) { + $frontPage->data = $defaultSet; + $frontPage->save(); + } + + // get accounts: + $accounts = $this->repository->getAccountsById($frontPage->data); + $chartData = []; + /** @var Account $account */ + foreach ($accounts as $account) { + $currency = $this->repository->getAccountCurrency($account); + if (null === $currency) { + $currency = $default; + } + $currentSet = [ + 'label' => $account->name, + 'currency_id' => $currency->id, + 'currency_code' => $currency->code, + 'currency_symbol' => $currency->symbol, + 'currency_decimal_places' => $currency->decimal_places, + 'type' => 'line', // line, area or bar + 'yAxisID' => 0, // 0, 1, 2 + 'fill' => null, // true, false, null + 'backgroundColor' => null, // null or hex + 'entries' => [], + ]; + + $currentStart = clone $start; + $range = app('steam')->balanceInRange($account, $start, clone $end); + $previous = round(array_values($range)[0], 12); + while ($currentStart <= $end) { + $format = $currentStart->format('Y-m-d'); + $label = $currentStart->formatLocalized((string)trans('config.month_and_day')); + $balance = isset($range[$format]) ? round($range[$format], 12) : $previous; + $previous = $balance; + $currentStart->addDay(); + $currentSet['entries'][$label] = $balance; + } + $chartData[] = $currentSet; + } + + return response()->json($chartData); + } + +} \ No newline at end of file diff --git a/app/Api/V1/Controllers/SummaryController.php b/app/Api/V1/Controllers/SummaryController.php new file mode 100644 index 0000000000..f617db72b7 --- /dev/null +++ b/app/Api/V1/Controllers/SummaryController.php @@ -0,0 +1,392 @@ +middleware( + function ($request, $next) { + /** @var User $user */ + $user = auth()->user(); + $this->currencyRepos = app(CurrencyRepositoryInterface::class); + $this->billRepository = app(BillRepositoryInterface::class); + $this->budgetRepository = app(BudgetRepositoryInterface::class); + $this->accountRepository = app(AccountRepositoryInterface::class); + + $this->billRepository->setUser($user); + $this->currencyRepos->setUser($user); + $this->budgetRepository->setUser($user); + $this->accountRepository->setUser($user); + + + return $next($request); + } + ); + } + + /** + * @param Request $request + * + * @return JsonResponse + * @throws FireflyException + */ + public function basic(Request $request): JsonResponse + { + // parameters for boxes: + $start = (string)$request->get('start'); + $end = (string)$request->get('end'); + if ('' === $start || '' === $end) { + throw new FireflyException('Start and end are mandatory parameters.'); + } + $start = Carbon::createFromFormat('Y-m-d', $start); + $end = Carbon::createFromFormat('Y-m-d', $end); + // balance information: + $balanceData = $this->getBalanceInformation($start, $end); + $billData = $this->getBillInformation($start, $end); + $spentData = $this->getLeftToSpendInfo($start, $end); + $networthData = $this->getNetWorthInfo($start, $end); + $total = array_merge($balanceData, $billData, $spentData, $networthData); + // TODO: liabilities with icon line-chart + + return response()->json($total); + + } + + /** + * Check if date is outside session range. + * + * @param Carbon $date + * + * @param Carbon $start + * @param Carbon $end + * + * @return bool + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + protected function notInDateRange(Carbon $date, Carbon $start, Carbon $end): bool // Validate a preference + { + $result = false; + if ($start->greaterThanOrEqualTo($date) && $end->greaterThanOrEqualTo($date)) { + $result = true; + } + // start and end in the past? use $end + if ($start->lessThanOrEqualTo($date) && $end->lessThanOrEqualTo($date)) { + $result = true; + } + + return $result; + } + + /** + * This method will scroll through the results of the spentInPeriodMc() array and return the correct info. + * + * @param array $spentInfo + * @param TransactionCurrency $currency + * + * @return float + */ + private function findInSpentArray(array $spentInfo, TransactionCurrency $currency): float + { + foreach ($spentInfo as $array) { + if ($array['currency_id'] === $currency->id) { + return $array['amount']; + } + } + + return 0.0; + } + + /** + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + private function getBalanceInformation(Carbon $start, Carbon $end): array + { + // prep some arrays: + $incomes = []; + $expenses = []; + $sums = []; + $return = []; + + // collect income of user: + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($start, $end) + ->setTypes([TransactionType::DEPOSIT]) + ->withOpposingAccount(); + $set = $collector->getTransactions(); + /** @var Transaction $transaction */ + foreach ($set as $transaction) { + $currencyId = (int)$transaction->transaction_currency_id; + $incomes[$currencyId] = $incomes[$currencyId] ?? '0'; + $incomes[$currencyId] = bcadd($incomes[$currencyId], $transaction->transaction_amount); + $sums[$currencyId] = $sums[$currencyId] ?? '0'; + $sums[$currencyId] = bcadd($sums[$currencyId], $transaction->transaction_amount); + } + + // collect expenses: + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($start, $end) + ->setTypes([TransactionType::WITHDRAWAL]) + ->withOpposingAccount(); + $set = $collector->getTransactions(); + /** @var Transaction $transaction */ + foreach ($set as $transaction) { + $currencyId = (int)$transaction->transaction_currency_id; + $expenses[$currencyId] = $expenses[$currencyId] ?? '0'; + $expenses[$currencyId] = bcadd($expenses[$currencyId], $transaction->transaction_amount); + $sums[$currencyId] = $sums[$currencyId] ?? '0'; + $sums[$currencyId] = bcadd($sums[$currencyId], $transaction->transaction_amount); + } + + // format amounts: + $keys = array_keys($sums); + foreach ($keys as $currencyId) { + $currency = $this->currencyRepos->findNull($currencyId); + if (null === $currency) { + continue; + } + // create objects for big array. + $return[] = [ + 'key' => sprintf('balance-in-%s', $currency->code), + 'title' => trans('firefly.box_balance_in_currency', ['currency' => $currency->symbol]), + 'monetary_value' => round($sums[$currencyId] ?? 0, $currency->decimal_places), + 'currency_id' => $currency->id, + 'currency_code' => $currency->code, + 'currency_symbol' => $currency->symbol, + 'currency_decimal_places' => $currency->decimal_places, + 'value_parsed' => app('amount')->formatAnything($currency, $sums[$currencyId] ?? '0', false), + 'local_icon' => 'balance-scale', + 'sub_title' => app('amount')->formatAnything($currency, $expenses[$currencyId] ?? '0', false) . + ' + ' . app('amount')->formatAnything($currency, $incomes[$currencyId] ?? '0', false), + ]; + $return[] = [ + 'key' => sprintf('spent-in-%s', $currency->code), + 'title' => trans('firefly.box_spent_in_currency', ['currency' => $currency->symbol]), + 'monetary_value' => round($expenses[$currencyId] ?? 0, $currency->decimal_places), + 'currency_id' => $currency->id, + 'currency_code' => $currency->code, + 'currency_symbol' => $currency->symbol, + 'currency_decimal_places' => $currency->decimal_places, + 'value_parsed' => app('amount')->formatAnything($currency, $expenses[$currencyId] ?? '0', false), + 'local_icon' => 'balance-scale', + 'sub_title' => '', + ]; + $return[] = [ + 'key' => sprintf('earned-in-%s', $currency->code), + 'title' => trans('firefly.box_earned_in_currency', ['currency' => $currency->symbol]), + 'monetary_value' => round($incomes[$currencyId] ?? 0, $currency->decimal_places), + 'currency_id' => $currency->id, + 'currency_code' => $currency->code, + 'currency_symbol' => $currency->symbol, + 'currency_decimal_places' => $currency->decimal_places, + 'value_parsed' => app('amount')->formatAnything($currency, $incomes[$currencyId] ?? '0', false), + 'local_icon' => 'balance-scale', + 'sub_title' => '', + ]; + } + + return $return; + } + + /** + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + private function getBillInformation(Carbon $start, Carbon $end): array + { + /* + * Since both this method and the chart use the exact same data, we can suffice + * with calling the one method in the bill repository that will get this amount. + */ + $paidAmount = $this->billRepository->getBillsPaidInRangePerCurrency($start, $end); + $unpaidAmount = $this->billRepository->getBillsUnpaidInRangePerCurrency($start, $end); + $return = []; + foreach ($paidAmount as $currencyId => $amount) { + $amount = bcmul($amount, '-1'); + $currency = $this->currencyRepos->findNull((int)$currencyId); + if (null === $currency) { + continue; + } + $return[] = [ + 'key' => sprintf('bills-paid-in-%s', $currency->code), + 'title' => trans('firefly.box_bill_paid_in_currency', ['currency' => $currency->symbol]), + 'monetary_value' => round($amount, $currency->decimal_places), + 'currency_id' => $currency->id, + 'currency_code' => $currency->code, + 'currency_symbol' => $currency->symbol, + 'currency_decimal_places' => $currency->decimal_places, + 'value_parsed' => app('amount')->formatAnything($currency, $amount, false), + 'local_icon' => 'check', + 'sub_title' => '', + ]; + } + + foreach ($unpaidAmount as $currencyId => $amount) { + $amount = bcmul($amount, '-1'); + $currency = $this->currencyRepos->findNull((int)$currencyId); + if (null === $currency) { + continue; + } + $return[] = [ + 'key' => sprintf('bills-unpaid-in-%s', $currency->code), + 'title' => trans('firefly.box_bill_unpaid_in_currency', ['currency' => $currency->symbol]), + 'monetary_value' => round($amount, $currency->decimal_places), + 'currency_id' => $currency->id, + 'currency_code' => $currency->code, + 'currency_symbol' => $currency->symbol, + 'currency_decimal_places' => $currency->decimal_places, + 'value_parsed' => app('amount')->formatAnything($currency, $amount, false), + 'local_icon' => 'calendar-o', + 'sub_title' => '', + ]; + } + + return $return; + } + + /** + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + private function getLeftToSpendInfo(Carbon $start, Carbon $end): array + { + $return = []; + $today = new Carbon; + $available = $this->budgetRepository->getAvailableBudgetWithCurrency($start, $end); + $budgets = $this->budgetRepository->getActiveBudgets(); + $spentInfo = $this->budgetRepository->spentInPeriodMc($budgets, new Collection, $start, $end); + foreach ($available as $currencyId => $amount) { + $currency = $this->currencyRepos->findNull($currencyId); + if (null === $currency) { + continue; + } + $spentInCurrency = (string)$this->findInSpentArray($spentInfo, $currency); + $leftToSpend = bcadd($amount, $spentInCurrency); + + $days = $today->diffInDays($end) + 1; + $perDay = '0'; + if (0 !== $days && bccomp($leftToSpend, '0') > -1) { + $perDay = bcdiv($leftToSpend, (string)$days); + } + + $return[] = [ + 'key' => sprintf('left-to-spend-in-%s', $currency->code), + 'title' => trans('firefly.box_left_to_spend_in_currency', ['currency' => $currency->symbol]), + 'monetary_value' => round($leftToSpend, $currency->decimal_places), + 'currency_id' => $currency->id, + 'currency_code' => $currency->code, + 'currency_symbol' => $currency->symbol, + 'currency_decimal_places' => $currency->decimal_places, + 'value_parsed' => app('amount')->formatAnything($currency, $leftToSpend, false), + 'local_icon' => 'money', + 'sub_title' => (string)trans('firefly.box_spend_per_day', ['amount' => app('amount')->formatAnything($currency, $perDay, false)]), + ]; + } + + return $return; + } + + /** + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + private function getNetWorthInfo(Carbon $start, Carbon $end): array + { + $date = Carbon::create()->startOfDay(); + + // start and end in the future? use $end + if ($this->notInDateRange($date, $start, $end)) { + /** @var Carbon $date */ + $date = session('end', Carbon::now()->endOfMonth()); + } + + /** @var NetWorthInterface $netWorthHelper */ + $netWorthHelper = app(NetWorthInterface::class); + $netWorthHelper->setUser(auth()->user()); + $allAccounts = $this->accountRepository->getActiveAccountsByType([AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE]); + + // filter list on preference of being included. + $filtered = $allAccounts->filter( + function (Account $account) { + $includeNetWorth = $this->accountRepository->getMetaValue($account, 'include_net_worth'); + + return null === $includeNetWorth ? true : '1' === $includeNetWorth; + } + ); + + $netWorthSet = $netWorthHelper->getNetWorthByCurrency($filtered, $date); + $return = []; + foreach ($netWorthSet as $index => $data) { + /** @var TransactionCurrency $currency */ + $currency = $data['currency']; + $amount = round($data['balance'], $currency->decimal_places); + if ($amount === 0.0) { + continue; + } + // return stuff + $return[] = [ + 'key' => sprintf('net-worth-in-%s', $currency->code), + 'title' => trans('firefly.box_net_worth_in_currency', ['currency' => $currency->symbol]), + 'monetary_value' => $amount, + 'currency_id' => $currency->id, + 'currency_code' => $currency->code, + 'currency_symbol' => $currency->symbol, + 'currency_decimal_places' => $currency->decimal_places, + 'value_parsed' => app('amount')->formatAnything($currency, $data['balance'], false), + 'local_icon' => 'line-chart', + 'sub_title' => '', + ]; + } + + return $return; + } + +} \ No newline at end of file diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index 452ee2cfd9..35ae66946b 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -400,6 +400,26 @@ class BudgetRepository implements BudgetRepositoryInterface return $amount; } + /** + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function getAvailableBudgetWithCurrency(Carbon $start, Carbon $end): array + { + $return = []; + $availableBudgets = $this->user->availableBudgets() + ->where('start_date', $start->format('Y-m-d 00:00:00')) + ->where('end_date', $end->format('Y-m-d 00:00:00'))->get(); + /** @var AvailableBudget $availableBudget */ + foreach ($availableBudgets as $availableBudget) { + $return[$availableBudget->transaction_currency_id] = $availableBudget->amount; + } + + return $return; + } + /** * Returns all available budget objects. * @@ -440,6 +460,8 @@ class BudgetRepository implements BudgetRepositoryInterface return bcdiv($total, (string)$days); } + /** @noinspection MoreThanThreeArgumentsInspection */ + /** * @param Budget $budget * @param Carbon $start @@ -505,7 +527,6 @@ class BudgetRepository implements BudgetRepositoryInterface return $set; } - /** @noinspection MoreThanThreeArgumentsInspection */ /** * This method is being used to generate the budget overview in the year/multi-year report. Its used * in both the year/multi-year budget overview AND in the accompanying chart. @@ -599,6 +620,8 @@ class BudgetRepository implements BudgetRepositoryInterface return $set; } + /** @noinspection MoreThanThreeArgumentsInspection */ + /** * @param Collection $accounts * @param Carbon $start @@ -633,7 +656,6 @@ class BudgetRepository implements BudgetRepositoryInterface return $result; } - /** @noinspection MoreThanThreeArgumentsInspection */ /** * @param TransactionCurrency $currency * @param Carbon $start @@ -661,6 +683,8 @@ class BudgetRepository implements BudgetRepositoryInterface return $availableBudget; } + /** @noinspection MoreThanThreeArgumentsInspection */ + /** * @param Budget $budget * @param int $order @@ -671,8 +695,6 @@ class BudgetRepository implements BudgetRepositoryInterface $budget->save(); } - /** @noinspection MoreThanThreeArgumentsInspection */ - /** * @param User $user */ @@ -813,7 +835,7 @@ class BudgetRepository implements BudgetRepositoryInterface $collector->setAllAssetAccounts(); } - $set = $collector->getTransactions(); + $set = $collector->getTransactions(); $return = []; $total = []; $currencies = []; @@ -922,6 +944,8 @@ class BudgetRepository implements BudgetRepositoryInterface return $budget; } + /** @noinspection MoreThanThreeArgumentsInspection */ + /** * @param AvailableBudget $availableBudget * @param array $data @@ -951,8 +975,6 @@ class BudgetRepository implements BudgetRepositoryInterface } - /** @noinspection MoreThanThreeArgumentsInspection */ - /** * @param BudgetLimit $budgetLimit * @param array $data diff --git a/app/Repositories/Budget/BudgetRepositoryInterface.php b/app/Repositories/Budget/BudgetRepositoryInterface.php index 627687d6e5..decd8d2d21 100644 --- a/app/Repositories/Budget/BudgetRepositoryInterface.php +++ b/app/Repositories/Budget/BudgetRepositoryInterface.php @@ -128,6 +128,14 @@ interface BudgetRepositoryInterface */ public function getAvailableBudget(TransactionCurrency $currency, Carbon $start, Carbon $end): string; + /** + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function getAvailableBudgetWithCurrency(Carbon $start, Carbon $end): array; + /** * Returns all available budget objects. * diff --git a/routes/api.php b/routes/api.php index 63e4df5b4a..71a3f4b35b 100644 --- a/routes/api.php +++ b/routes/api.php @@ -138,6 +138,18 @@ Route::group( } ); + +Route::group( + ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers\Chart', 'prefix' => 'chart/account', + 'as' => 'api.v1.chart.account.'], + function () { + + // Overview API routes: + Route::get('overview', ['uses' => 'AccountController@overview', 'as' => 'overview']); + + } +); + Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'configuration', 'as' => 'api.v1.configuration.'], function () { @@ -266,6 +278,17 @@ Route::group( } ); +Route::group( + ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'summary', + 'as' => 'api.v1.summary.'], + function () { + + // Overview API routes: + Route::get('basic', ['uses' => 'SummaryController@basic', 'as' => 'basic']); + + } +); + Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'currencies', 'as' => 'api.v1.currencies.'], function () {