From 5a8146aa34dfe193fbed6458dda60d76d0cee7e9 Mon Sep 17 00:00:00 2001 From: James Cole Date: Tue, 3 Sep 2019 15:37:59 +0200 Subject: [PATCH] Revamped tag report as well. --- .../Report/Budget/MonthReportGenerator.php | 2 +- .../Report/Category/MonthReportGenerator.php | 2 +- app/Generator/Report/Support.php | 211 ------- .../Report/Tag/MonthReportGenerator.php | 100 +-- .../Chart/CategoryReportController.php | 5 +- .../Controllers/Chart/TagReportController.php | 558 +++++++++++------ app/Http/Controllers/Report/TagController.php | 573 +++++++++++++++++ app/Providers/TagServiceProvider.php | 16 + app/Repositories/Tag/OperationsRepository.php | 242 +++++++ .../Tag/OperationsRepositoryInterface.php | 93 +++ public/v1/js/ff/reports/category/month.js | 27 +- public/v1/js/ff/reports/tag/month.js | 68 +- resources/lang/en_US/firefly.php | 6 + resources/views/v1/reports/tag/month.twig | 589 ++++++------------ .../reports/tag/partials/account-per-tag.twig | 40 ++ .../v1/reports/tag/partials/accounts.twig | 46 ++ .../v1/reports/tag/partials/avg-expenses.twig | 46 ++ .../v1/reports/tag/partials/avg-income.twig | 46 ++ .../views/v1/reports/tag/partials/tags.twig | 64 ++ .../v1/reports/tag/partials/top-expenses.twig | 54 ++ .../v1/reports/tag/partials/top-income.twig | 54 ++ routes/web.php | 67 +- 22 files changed, 1917 insertions(+), 992 deletions(-) delete mode 100644 app/Generator/Report/Support.php create mode 100644 app/Http/Controllers/Report/TagController.php create mode 100644 app/Repositories/Tag/OperationsRepository.php create mode 100644 app/Repositories/Tag/OperationsRepositoryInterface.php create mode 100644 resources/views/v1/reports/tag/partials/account-per-tag.twig create mode 100644 resources/views/v1/reports/tag/partials/accounts.twig create mode 100644 resources/views/v1/reports/tag/partials/avg-expenses.twig create mode 100644 resources/views/v1/reports/tag/partials/avg-income.twig create mode 100644 resources/views/v1/reports/tag/partials/tags.twig create mode 100644 resources/views/v1/reports/tag/partials/top-expenses.twig create mode 100644 resources/views/v1/reports/tag/partials/top-income.twig diff --git a/app/Generator/Report/Budget/MonthReportGenerator.php b/app/Generator/Report/Budget/MonthReportGenerator.php index 33418683ab..20bb4c292b 100644 --- a/app/Generator/Report/Budget/MonthReportGenerator.php +++ b/app/Generator/Report/Budget/MonthReportGenerator.php @@ -39,7 +39,7 @@ use Throwable; * * @codeCoverageIgnore */ -class MonthReportGenerator extends Support implements ReportGeneratorInterface +class MonthReportGenerator implements ReportGeneratorInterface { /** @var Collection The accounts in the report. */ private $accounts; diff --git a/app/Generator/Report/Category/MonthReportGenerator.php b/app/Generator/Report/Category/MonthReportGenerator.php index 5646ddee3e..6a20c5814a 100644 --- a/app/Generator/Report/Category/MonthReportGenerator.php +++ b/app/Generator/Report/Category/MonthReportGenerator.php @@ -39,7 +39,7 @@ use Throwable; * * @codeCoverageIgnore */ -class MonthReportGenerator extends Support implements ReportGeneratorInterface +class MonthReportGenerator implements ReportGeneratorInterface { /** @var Collection The included accounts */ private $accounts; diff --git a/app/Generator/Report/Support.php b/app/Generator/Report/Support.php deleted file mode 100644 index 1bde7afdbf..0000000000 --- a/app/Generator/Report/Support.php +++ /dev/null @@ -1,211 +0,0 @@ -. - */ -/** @noinspection PhpUndefinedMethodInspection */ -declare(strict_types=1); - -namespace FireflyIII\Generator\Report; - -use FireflyIII\Models\TransactionType; - -/** - * Class Support. - * @method array getExpenses() - * @method array getIncome() - * - * @codeCoverageIgnore - */ -class Support -{ - /** - * Get the top expenses. - * - * @return array - */ - public function getTopExpenses(): array - { - $expenses = $this->getExpenses(); - usort($expenses, function ($a, $b) { - return $a['amount'] <=> $b['amount']; - }); - - return $expenses; - } - - /** - * Get the top income. - * - * @return array - */ - public function getTopIncome(): array - { - $income = $this->getIncome(); - usort($income, function ($a, $b) { - return $b['amount'] <=> $a['amount']; - }); - - return $income; - } - - /** - * Get averages from a collection. - * - * @param array $array - * @param int $sortFlag - * - * @return array - */ - protected function getAverages(array $array, int $sortFlag): array - { - $result = []; - /** @var array $journal */ - foreach ($array as $journal) { - // opposing name and ID: - $opposingId = $journal['destination_account_id']; - - // is not set? - if (!isset($result[$opposingId])) { - $name = $journal['destination_account_name']; - $result[$opposingId] = [ - 'name' => $name, - 'count' => 1, - 'id' => $opposingId, - 'average' => $journal['amount'], - 'sum' => $journal['amount'], - ]; - continue; - } - ++$result[$opposingId]['count']; - $result[$opposingId]['sum'] = bcadd($result[$opposingId]['sum'], $journal['amount']); - $result[$opposingId]['average'] = bcdiv($result[$opposingId]['sum'], (string)$result[$opposingId]['count']); - } - - // sort result by average: - $average = []; - foreach ($result as $key => $row) { - $average[$key] = (float)$row['average']; - } - - array_multisort($average, $sortFlag, $result); - - return $result; - } - - /** - * Summarize collection by earned and spent data. - * - * // it's exactly five. - * - * @param array $spent - * @param array $earned - * - * @return array - */ - protected function getObjectSummary(array $spent, array $earned): array - { - $return = [ - 'sum' => [ - 'spent' => '0', - 'earned' => '0', - ], - ]; - - /** - * @var int - * @var string $entry - */ - foreach ($spent as $objectId => $entry) { - if (!isset($return[$objectId])) { - $return[$objectId] = ['spent' => '0', 'earned' => '0']; - } - - $return[$objectId]['spent'] = $entry; - $return['sum']['spent'] = bcadd($return['sum']['spent'], $entry); - } - - /** - * @var int - * @var string $entry - */ - foreach ($earned as $objectId => $entry) { - $entry = bcmul($entry, '-1'); - if (!isset($return[$objectId])) { - $return[$objectId] = ['spent' => '0', 'earned' => '0']; - } - - $return[$objectId]['earned'] = $entry; - $return['sum']['earned'] = bcadd($return['sum']['earned'], $entry); - } - - return $return; - } - - /** - * Summarize the data by account. - * - * @param array $array - * - * @return array - */ - protected function summarizeByAccount(array $array): array - { - $result = []; - /** @var array $journal */ - foreach ($array as $journal) { - $accountId = $journal['source_account_id'] ?? 0; - $result[$accountId] = $result[$accountId] ?? '0'; - $result[$accountId] = bcadd($journal['amount'], $result[$accountId]); - } - - return $result; - } - - /** - * Summarize the data by the asset account or liability, depending on the type. - * - * In case of transfers, it will choose the source account. - * - * @param array $array - * - * @return array - */ - protected function summarizeByAssetAccount(array $array): array - { - $result = []; - /** @var array $journal */ - foreach ($array as $journal) { - $accountId = 0; - switch ($journal['transaction_type_type']) { - case TransactionType::WITHDRAWAL: - case TransactionType::TRANSFER: - $accountId = $journal['source_account_id'] ?? 0; - break; - case TransactionType::DEPOSIT: - $accountId = $journal['destination_account_id'] ?? 0; - break; - } - - $result[$accountId] = $result[$accountId] ?? '0'; - $result[$accountId] = bcadd($journal['amount'], $result[$accountId]); - } - - return $result; - } -} diff --git a/app/Generator/Report/Tag/MonthReportGenerator.php b/app/Generator/Report/Tag/MonthReportGenerator.php index 706bc1da69..17f63a0765 100644 --- a/app/Generator/Report/Tag/MonthReportGenerator.php +++ b/app/Generator/Report/Tag/MonthReportGenerator.php @@ -28,8 +28,6 @@ namespace FireflyIII\Generator\Report\Tag; use Carbon\Carbon; use FireflyIII\Generator\Report\ReportGeneratorInterface; use FireflyIII\Generator\Report\Support; -use FireflyIII\Helpers\Collector\GroupCollectorInterface; -use FireflyIII\Models\TransactionType; use Illuminate\Support\Collection; use Log; use Throwable; @@ -39,7 +37,7 @@ use Throwable; * * @codeCoverageIgnore */ -class MonthReportGenerator extends Support implements ReportGeneratorInterface +class MonthReportGenerator implements ReportGeneratorInterface { /** @var Collection The accounts involved */ private $accounts; @@ -71,30 +69,18 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface */ public function generate(): string { - $accountIds = implode(',', $this->accounts->pluck('id')->toArray()); - $tagTags = implode(',', $this->tags->pluck('tag')->toArray()); - $tagIds = implode(',', $this->tags->pluck('id')->toArray()); - $reportType = 'tag'; - $expenses = $this->getExpenses(); - $income = $this->getIncome(); - $accountSummary = $this->getObjectSummary($this->summarizeByAssetAccount($expenses), $this->summarizeByAssetAccount($income)); - $tagSummary = $this->getObjectSummary($this->summarizeByTag($expenses), $this->summarizeByTag($income)); - $averageExpenses = $this->getAverages($expenses, SORT_ASC); - $averageIncome = $this->getAverages($income, SORT_DESC); - $topExpenses = $this->getTopExpenses(); - $topIncome = $this->getTopIncome(); + $accountIds = implode(',', $this->accounts->pluck('id')->toArray()); + $tagIds = implode(',', $this->tags->pluck('id')->toArray()); + $reportType = 'tag'; // render! try { $result = view( - 'reports.tag.month', compact( - 'accountIds', 'tagTags', 'reportType', 'accountSummary', 'tagSummary', 'averageExpenses', 'averageIncome', 'topIncome', - 'topExpenses', 'tagIds' - ) + 'reports.tag.month', compact('accountIds', 'reportType', 'tagIds') )->with('start', $this->start)->with('end', $this->end)->with('tags', $this->tags)->with('accounts', $this->accounts)->render(); } catch (Throwable $e) { Log::error(sprintf('Cannot render reports.tag.month: %s', $e->getMessage())); - $result = 'Could not render report view.'; + $result = sprintf('Could not render report view: %s', $e->getMessage()); } return $result; @@ -192,79 +178,5 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface return $this; } - /** - * Get expense collection for report. - * - * @return array - */ - protected function getExpenses(): array - { - if (count($this->expenses) > 0) { - Log::debug('Return previous set of expenses.'); - return $this->expenses; - } - - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setAccounts($this->accounts)->setRange($this->start, $this->end) - ->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER]) - ->setTags($this->tags)->withAccountInformation(); - - $journals = $collector->getExtractedJournals(); - $this->expenses = $journals; - - return $journals; - } - - /** - * Get the income for this report. - * - * @return array - */ - protected function getIncome(): array - { - if (count($this->income) > 0) { - return $this->income; - } - - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setAccounts($this->accounts)->setRange($this->start, $this->end) - ->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER]) - ->setTags($this->tags)->withAccountInformation(); - - $journals = $collector->getExtractedJournals(); - $this->income = $journals; - - return $journals; - } - - /** - * Summarize by tag. - * - * @param array $array - * - * @return array - */ - protected function summarizeByTag(array $array): array - { - $tagIds = array_map('\intval', $this->tags->pluck('id')->toArray()); - $result = []; - /** @var array $journal */ - foreach ($array as $journal) { - /** - * @var int $id - * @var array $tag - */ - foreach ($journal['tags'] as $id => $tag) { - if (in_array($id, $tagIds, true)) { - $result[$id] = $result[$id] ?? '0'; - $result[$id] = bcadd($journal['amount'], $result[$id]); - } - } - } - - return $result; - } } diff --git a/app/Http/Controllers/Chart/CategoryReportController.php b/app/Http/Controllers/Chart/CategoryReportController.php index 0c28b340f8..5a90297504 100644 --- a/app/Http/Controllers/Chart/CategoryReportController.php +++ b/app/Http/Controllers/Chart/CategoryReportController.php @@ -296,7 +296,7 @@ class CategoryReportController extends Controller /** * @param Collection $accounts - * @param Category $category + * @param Category $category * @param Carbon $start * @param Carbon $end * @@ -307,7 +307,7 @@ class CategoryReportController extends Controller { $chartData = []; $spent = $this->opsRepository->listExpenses($start, $end, $accounts, new Collection([$category])); - $earned = $this->opsRepository->listIncome($start, $end, $accounts, new Collection([$category])); + $earned = $this->opsRepository->listIncome($start, $end, $accounts, new Collection([$category])); $format = app('navigation')->preferredCarbonLocalizedFormat($start, $end); // loop expenses. @@ -435,6 +435,7 @@ class CategoryReportController extends Controller /** * TODO duplicate function + * * @param Carbon $start * @param Carbon $end * diff --git a/app/Http/Controllers/Chart/TagReportController.php b/app/Http/Controllers/Chart/TagReportController.php index 72cc61a6b4..7f33a5469f 100644 --- a/app/Http/Controllers/Chart/TagReportController.php +++ b/app/Http/Controllers/Chart/TagReportController.php @@ -27,6 +27,7 @@ use FireflyIII\Generator\Chart\Basic\GeneratorInterface; use FireflyIII\Helpers\Chart\MetaPieChartInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Tag; +use FireflyIII\Repositories\Tag\OperationsRepositoryInterface; use FireflyIII\Support\CacheProperties; use FireflyIII\Support\Http\Controllers\AugumentData; use FireflyIII\Support\Http\Controllers\TransactionCalculation; @@ -42,8 +43,12 @@ class TagReportController extends Controller /** @var GeneratorInterface Chart generation methods. */ protected $generator; + /** @var OperationsRepositoryInterface */ + private $opsRepository; + /** * TagReportController constructor. + * * @codeCoverageIgnore */ public function __construct() @@ -51,6 +56,14 @@ class TagReportController extends Controller parent::__construct(); // create chart generator: $this->generator = app(GeneratorInterface::class); + + $this->middleware( + function ($request, $next) { + $this->opsRepository = app(OperationsRepositoryInterface::class); + + return $next($request); + } + ); } @@ -111,12 +124,7 @@ class TagReportController extends Controller return response()->json($data); } - /** - * Generate expense for tag grouped on budget. - * - * TODO this chart is not multi-currency aware. - * * @param Collection $accounts * @param Collection $tags * @param Carbon $start @@ -126,25 +134,32 @@ class TagReportController extends Controller */ public function budgetExpense(Collection $accounts, Collection $tags, Carbon $start, Carbon $end): JsonResponse { - /** @var MetaPieChartInterface $helper */ - $helper = app(MetaPieChartInterface::class); - $helper->setAccounts($accounts); - $helper->setTags($tags); - $helper->setStart($start); - $helper->setEnd($end); - $helper->setCollectOtherObjects(false); - $chartData = $helper->generate('expense', 'budget'); - $data = $this->generator->pieChart($chartData); + $result = []; + $spent = $this->opsRepository->listExpenses($start, $end, $accounts, $tags); + + // loop expenses. + foreach ($spent as $currency) { + /** @var array $tag */ + foreach ($currency['tags'] as $tag) { + foreach ($tag['transaction_journals'] as $journal) { + $objectName = $journal['budget_name'] ?? trans('firefly.no_budget'); + $title = sprintf('%s (%s)', $objectName, $currency['currency_name']); + $result[$title] = $result[$title] ?? [ + 'amount' => '0', + 'currency_symbol' => $currency['currency_symbol'], + ]; + $amount = app('steam')->positive($journal['amount']); + $result[$title]['amount'] = bcadd($result[$title]['amount'], $amount); + } + } + } + + $data = $this->generator->multiCurrencyPieChart($result); return response()->json($data); } - /** - * Generate expense for tag grouped on category. - * - * TODO this chart is not multi-currency aware. - * * @param Collection $accounts * @param Collection $tags * @param Carbon $start @@ -154,188 +169,371 @@ class TagReportController extends Controller */ public function categoryExpense(Collection $accounts, Collection $tags, Carbon $start, Carbon $end): JsonResponse { - /** @var MetaPieChartInterface $helper */ - $helper = app(MetaPieChartInterface::class); - $helper->setAccounts($accounts); - $helper->setTags($tags); - $helper->setStart($start); - $helper->setEnd($end); - $helper->setCollectOtherObjects(false); - $chartData = $helper->generate('expense', 'category'); - $data = $this->generator->pieChart($chartData); + $result = []; + $spent = $this->opsRepository->listExpenses($start, $end, $accounts, $tags); + + // loop expenses. + foreach ($spent as $currency) { + /** @var array $tag */ + foreach ($currency['tags'] as $tag) { + foreach ($tag['transaction_journals'] as $journal) { + $objectName = $journal['category_name'] ?? trans('firefly.no_category'); + $title = sprintf('%s (%s)', $objectName, $currency['currency_name']); + $result[$title] = $result[$title] ?? [ + 'amount' => '0', + 'currency_symbol' => $currency['currency_symbol'], + ]; + $amount = app('steam')->positive($journal['amount']); + $result[$title]['amount'] = bcadd($result[$title]['amount'], $amount); + } + } + } + + $data = $this->generator->multiCurrencyPieChart($result); return response()->json($data); } + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return JsonResponse + */ + public function categoryIncome(Collection $accounts, Collection $tags, Carbon $start, Carbon $end): JsonResponse + { + $result = []; + $spent = $this->opsRepository->listIncome($start, $end, $accounts, $tags); + + // loop expenses. + foreach ($spent as $currency) { + /** @var array $tag */ + foreach ($currency['tags'] as $tag) { + foreach ($tag['transaction_journals'] as $journal) { + $objectName = $journal['category_name'] ?? trans('firefly.no_category'); + $title = sprintf('%s (%s)', $objectName, $currency['currency_name']); + $result[$title] = $result[$title] ?? [ + 'amount' => '0', + 'currency_symbol' => $currency['currency_symbol'], + ]; + $amount = app('steam')->positive($journal['amount']); + $result[$title]['amount'] = bcadd($result[$title]['amount'], $amount); + } + } + } + + $data = $this->generator->multiCurrencyPieChart($result); + + return response()->json($data); + } + + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return JsonResponse + */ + public function destinationExpense(Collection $accounts, Collection $tags, Carbon $start, Carbon $end): JsonResponse + { + $result = []; + $spent = $this->opsRepository->listExpenses($start, $end, $accounts, $tags); + + // loop expenses. + foreach ($spent as $currency) { + /** @var array $tag */ + foreach ($currency['tags'] as $tag) { + foreach ($tag['transaction_journals'] as $journal) { + $objectName = $journal['destination_account_name'] ?? trans('firefly.empty'); + $title = sprintf('%s (%s)', $objectName, $currency['currency_name']); + $result[$title] = $result[$title] ?? [ + 'amount' => '0', + 'currency_symbol' => $currency['currency_symbol'], + ]; + $amount = app('steam')->positive($journal['amount']); + $result[$title]['amount'] = bcadd($result[$title]['amount'], $amount); + } + } + } + + $data = $this->generator->multiCurrencyPieChart($result); + + return response()->json($data); + } + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return JsonResponse + */ + public function destinationIncome(Collection $accounts, Collection $tags, Carbon $start, Carbon $end): JsonResponse + { + $result = []; + $spent = $this->opsRepository->listIncome($start, $end, $accounts, $tags); + + // loop expenses. + foreach ($spent as $currency) { + /** @var array $tag */ + foreach ($currency['tags'] as $tag) { + foreach ($tag['transaction_journals'] as $journal) { + $objectName = $journal['destination_account_name'] ?? trans('firefly.empty'); + $title = sprintf('%s (%s)', $objectName, $currency['currency_name']); + $result[$title] = $result[$title] ?? [ + 'amount' => '0', + 'currency_symbol' => $currency['currency_symbol'], + ]; + $amount = app('steam')->positive($journal['amount']); + $result[$title]['amount'] = bcadd($result[$title]['amount'], $amount); + } + } + } + + $data = $this->generator->multiCurrencyPieChart($result); + + return response()->json($data); + } /** * Generate main tag overview chart. * - * TODO this chart is not multi-currency aware. - * * @param Collection $accounts - * @param Collection $tags + * @param Tag $tag * @param Carbon $start * @param Carbon $end * * @return JsonResponse * */ - public function mainChart(Collection $accounts, Collection $tags, Carbon $start, Carbon $end): JsonResponse + public function mainChart(Collection $accounts, Tag $tag, Carbon $start, Carbon $end): JsonResponse { - $cache = new CacheProperties; - $cache->addProperty('chart.category.report.main'); - $cache->addProperty($accounts); - $cache->addProperty($tags); - $cache->addProperty($start); - $cache->addProperty($end); - if ($cache->has()) { - return response()->json($cache->get()); // @codeCoverageIgnore - } + $chartData = []; + $spent = $this->opsRepository->listExpenses($start, $end, $accounts, new Collection([$tag])); + $earned = $this->opsRepository->listIncome($start, $end, $accounts, new Collection([$tag])); + $format = app('navigation')->preferredCarbonLocalizedFormat($start, $end); - $format = app('navigation')->preferredCarbonLocalizedFormat($start, $end); - $function = app('navigation')->preferredEndOfPeriod($start, $end); - $chartData = []; - $currentStart = clone $start; + // loop expenses. + foreach ($spent as $currency) { + // add things to chart Data for each currency: + $spentKey = sprintf('%d-spent', $currency['currency_id']); + $chartData[$spentKey] = $chartData[$spentKey] ?? [ + 'label' => sprintf( + '%s (%s)', (string)trans('firefly.spent_in_specific_tag', ['tag' => $tag->tag]), $currency['currency_name'] + ), + 'type' => 'bar', + 'currency_symbol' => $currency['currency_symbol'], + 'currency_id' => $currency['currency_id'], + 'entries' => $this->makeEntries($start, $end), + ]; - // prep chart data: - foreach ($tags as $tag) { - $chartData[$tag->id . '-in'] = [ - 'label' => $tag->tag . ' (' . strtolower((string)trans('firefly.income')) . ')', - 'type' => 'bar', - 'yAxisID' => 'y-axis-0', - 'entries' => [], - ]; - $chartData[$tag->id . '-out'] = [ - 'label' => $tag->tag . ' (' . strtolower((string)trans('firefly.expenses')) . ')', - 'type' => 'bar', - 'yAxisID' => 'y-axis-0', - 'entries' => [], - ]; - // total in, total out: - $chartData[$tag->id . '-total-in'] = [ - 'label' => $tag->tag . ' (' . strtolower((string)trans('firefly.sum_of_income')) . ')', - 'type' => 'line', - 'fill' => false, - 'yAxisID' => 'y-axis-1', - 'entries' => [], - ]; - $chartData[$tag->id . '-total-out'] = [ - 'label' => $tag->tag . ' (' . strtolower((string)trans('firefly.sum_of_expenses')) . ')', - 'type' => 'line', - 'fill' => false, - 'yAxisID' => 'y-axis-1', - 'entries' => [], - ]; - } - $sumOfIncome = []; - $sumOfExpense = []; - - while ($currentStart < $end) { - $currentEnd = clone $currentStart; - $currentEnd = $currentEnd->$function(); - $expenses = $this->groupByTag($this->getExpensesForTags($accounts, $tags, $currentStart, $currentEnd)); - $income = $this->groupByTag($this->getIncomeForTags($accounts, $tags, $currentStart, $currentEnd)); - $label = $currentStart->formatLocalized($format); - - /** @var Tag $tag */ - foreach ($tags as $tag) { - $labelIn = $tag->id . '-in'; - $labelOut = $tag->id . '-out'; - $labelSumIn = $tag->id . '-total-in'; - $labelSumOut = $tag->id . '-total-out'; - $currentIncome = bcmul($income[$tag->id] ?? '0','-1'); - $currentExpense = $expenses[$tag->id] ?? '0'; - - // add to sum: - $sumOfIncome[$tag->id] = $sumOfIncome[$tag->id] ?? '0'; - $sumOfExpense[$tag->id] = $sumOfExpense[$tag->id] ?? '0'; - $sumOfIncome[$tag->id] = bcadd($sumOfIncome[$tag->id], $currentIncome); - $sumOfExpense[$tag->id] = bcadd($sumOfExpense[$tag->id], $currentExpense); - - // add to chart: - $chartData[$labelIn]['entries'][$label] = $currentIncome; - $chartData[$labelOut]['entries'][$label] = $currentExpense; - $chartData[$labelSumIn]['entries'][$label] = $sumOfIncome[$tag->id]; - $chartData[$labelSumOut]['entries'][$label] = $sumOfExpense[$tag->id]; + foreach ($currency['tags'] as $currentTag) { + foreach ($currentTag['transaction_journals'] as $journal) { + $key = $journal['date']->formatLocalized($format); + $amount = app('steam')->positive($journal['amount']); + $chartData[$spentKey]['entries'][$key] = $chartData[$spentKey]['entries'][$key] ?? '0'; + $chartData[$spentKey]['entries'][$key] = bcadd($chartData[$spentKey]['entries'][$key], $amount); + } } - /** @var Carbon $currentStart */ + } + + // loop income. + foreach ($earned as $currency) { + // add things to chart Data for each currency: + $spentKey = sprintf('%d-earned', $currency['currency_id']); + $chartData[$spentKey] = $chartData[$spentKey] ?? [ + 'label' => sprintf( + '%s (%s)', (string)trans('firefly.earned_in_specific_tag', ['tag' => $tag->tag]), $currency['currency_name'] + ), + 'type' => 'bar', + 'currency_symbol' => $currency['currency_symbol'], + 'currency_id' => $currency['currency_id'], + 'entries' => $this->makeEntries($start, $end), + ]; + + foreach ($currency['tags'] as $currentTag) { + foreach ($currentTag['transaction_journals'] as $journal) { + $key = $journal['date']->formatLocalized($format); + $amount = app('steam')->positive($journal['amount']); + $chartData[$spentKey]['entries'][$key] = $chartData[$spentKey]['entries'][$key] ?? '0'; + $chartData[$spentKey]['entries'][$key] = bcadd($chartData[$spentKey]['entries'][$key], $amount); + } + } + } + + $data = $this->generator->multiSet($chartData); + + return response()->json($data); + } + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return JsonResponse + */ + public function sourceExpense(Collection $accounts, Collection $tags, Carbon $start, Carbon $end): JsonResponse + { + $result = []; + $spent = $this->opsRepository->listExpenses($start, $end, $accounts, $tags); + + // loop expenses. + foreach ($spent as $currency) { + /** @var array $tag */ + foreach ($currency['tags'] as $tag) { + foreach ($tag['transaction_journals'] as $journal) { + $objectName = $journal['source_account_name'] ?? trans('firefly.empty'); + $title = sprintf('%s (%s)', $objectName, $currency['currency_name']); + $result[$title] = $result[$title] ?? [ + 'amount' => '0', + 'currency_symbol' => $currency['currency_symbol'], + ]; + $amount = app('steam')->positive($journal['amount']); + $result[$title]['amount'] = bcadd($result[$title]['amount'], $amount); + } + } + } + + $data = $this->generator->multiCurrencyPieChart($result); + + return response()->json($data); + } + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return JsonResponse + */ + public function sourceIncome(Collection $accounts, Collection $tags, Carbon $start, Carbon $end): JsonResponse + { + $result = []; + $earned = $this->opsRepository->listIncome($start, $end, $accounts, $tags); + + // loop expenses. + foreach ($earned as $currency) { + /** @var array $tag */ + foreach ($currency['tags'] as $tag) { + foreach ($tag['transaction_journals'] as $journal) { + $objectName = $journal['source_account_name'] ?? trans('firefly.empty'); + $title = sprintf('%s (%s)', $objectName, $currency['currency_name']); + $result[$title] = $result[$title] ?? [ + 'amount' => '0', + 'currency_symbol' => $currency['currency_symbol'], + ]; + $amount = app('steam')->positive($journal['amount']); + $result[$title]['amount'] = bcadd($result[$title]['amount'], $amount); + } + } + } + + $data = $this->generator->multiCurrencyPieChart($result); + + return response()->json($data); + } + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return JsonResponse + */ + public function tagExpense(Collection $accounts, Collection $tags, Carbon $start, Carbon $end): JsonResponse + { + $result = []; + $spent = $this->opsRepository->listExpenses($start, $end, $accounts, $tags); + + // loop expenses. + foreach ($spent as $currency) { + /** @var array $tag */ + foreach ($currency['tags'] as $tag) { + $title = sprintf('%s (%s)', $tag['name'], $currency['currency_name']); + $result[$title] = $result[$title] ?? [ + 'amount' => '0', + 'currency_symbol' => $currency['currency_symbol'], + ]; + foreach ($tag['transaction_journals'] as $journal) { + $amount = app('steam')->positive($journal['amount']); + $result[$title]['amount'] = bcadd($result[$title]['amount'], $amount); + } + } + } + $data = $this->generator->multiCurrencyPieChart($result); + + return response()->json($data); + } + + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return JsonResponse + */ + public function tagIncome(Collection $accounts, Collection $tags, Carbon $start, Carbon $end): JsonResponse + { + $result = []; + $earned = $this->opsRepository->listIncome($start, $end, $accounts, $tags); + + // loop expenses. + foreach ($earned as $currency) { + /** @var array $tag */ + foreach ($currency['tags'] as $tag) { + $title = sprintf('%s (%s)', $tag['name'], $currency['currency_name']); + $result[$title] = $result[$title] ?? [ + 'amount' => '0', + 'currency_symbol' => $currency['currency_symbol'], + ]; + foreach ($tag['transaction_journals'] as $journal) { + $amount = app('steam')->positive($journal['amount']); + $result[$title]['amount'] = bcadd($result[$title]['amount'], $amount); + } + } + } + $data = $this->generator->multiCurrencyPieChart($result); + + return response()->json($data); + + return response()->json($data); + } + + + /** + * TODO duplicate function + * + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + private function makeEntries(Carbon $start, Carbon $end): array + { + $return = []; + $format = app('navigation')->preferredCarbonLocalizedFormat($start, $end); + $preferredRange = app('navigation')->preferredRangeFormat($start, $end); + $currentStart = clone $start; + while ($currentStart <= $end) { + $currentEnd = app('navigation')->endOfPeriod($currentStart, $preferredRange); + $key = $currentStart->formatLocalized($format); + $return[$key] = '0'; $currentStart = clone $currentEnd; - $currentStart->addDay(); - $currentStart->startOfDay(); + $currentStart->addDay()->startOfDay(); + } - } - // remove all empty entries to prevent cluttering: - $newSet = []; - foreach ($chartData as $key => $entry) { - if (0 === !array_sum($entry['entries'])) { - $newSet[$key] = $chartData[$key]; // @codeCoverageIgnore - } - } - if (0 === count($newSet)) { - $newSet = $chartData; // @codeCoverageIgnore - } - $data = $this->generator->multiSet($newSet); - $cache->store($data); - - return response()->json($data); + return $return; } - - /** - * Show expense grouped by expense account. - * - * TODO this chart is not multi-currency aware. - * - * @param Collection $accounts - * @param Collection $tags - * @param Carbon $start - * @param Carbon $end - * @param string $others - * - * @return JsonResponse - */ - public function tagExpense(Collection $accounts, Collection $tags, Carbon $start, Carbon $end, string $others): JsonResponse - { - /** @var MetaPieChartInterface $helper */ - $helper = app(MetaPieChartInterface::class); - $helper->setAccounts($accounts); - $helper->setTags($tags); - $helper->setStart($start); - $helper->setEnd($end); - $helper->setCollectOtherObjects(1 === (int)$others); - $chartData = $helper->generate('expense', 'tag'); - $data = $this->generator->pieChart($chartData); - - return response()->json($data); - } - - - /** - * Show income grouped by tag. - * - * TODO this chart is not multi-currency aware. - * - * @param Collection $accounts - * @param Collection $tags - * @param Carbon $start - * @param Carbon $end - * @param string $others - * - * @return JsonResponse - */ - public function tagIncome(Collection $accounts, Collection $tags, Carbon $start, Carbon $end, string $others): JsonResponse - { - /** @var MetaPieChartInterface $helper */ - $helper = app(MetaPieChartInterface::class); - $helper->setAccounts($accounts); - $helper->setTags($tags); - $helper->setStart($start); - $helper->setEnd($end); - $helper->setCollectOtherObjects(1 === (int)$others); - $chartData = $helper->generate('income', 'tag'); - $data = $this->generator->pieChart($chartData); - - return response()->json($data); - } } diff --git a/app/Http/Controllers/Report/TagController.php b/app/Http/Controllers/Report/TagController.php new file mode 100644 index 0000000000..001ae55bb9 --- /dev/null +++ b/app/Http/Controllers/Report/TagController.php @@ -0,0 +1,573 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Http\Controllers\Report; + + +use Carbon\Carbon; +use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Models\Account; +use FireflyIII\Models\Tag; +use FireflyIII\Repositories\Tag\OperationsRepositoryInterface; +use Illuminate\Support\Collection; +use Log; +use Throwable; + +/** + * + * Class TagController + */ +class TagController extends Controller +{ + + + /** @var OperationsRepositoryInterface */ + private $opsRepository; + + /** + * ExpenseReportController constructor. + * + * @codeCoverageIgnore + */ + public function __construct() + { + parent::__construct(); + $this->middleware( + function ($request, $next) { + $this->opsRepository = app(OperationsRepositoryInterface::class); + + return $next($request); + } + ); + } + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function accountPerTag(Collection $accounts, Collection $tags, Carbon $start, Carbon $end) + { + $spent = $this->opsRepository->listExpenses($start, $end, $accounts, $tags); + $earned = $this->opsRepository->listIncome($start, $end, $accounts, $tags); + $report = []; + /** @var Account $account */ + foreach ($accounts as $account) { + $accountId = $account->id; + $report[$accountId] = $report[$accountId] ?? [ + 'name' => $account->name, + 'id' => $account->id, + 'iban' => $account->iban, + 'currencies' => [], + ]; + } + + // loop expenses. + foreach ($spent as $currency) { + $currencyId = $currency['currency_id']; + + /** @var array $tag */ + foreach ($currency['tags'] as $tag) { + foreach ($tag['transaction_journals'] as $journal) { + $sourceAccountId = $journal['source_account_id']; + $report[$sourceAccountId]['currencies'][$currencyId] = $report[$sourceAccountId]['currencies'][$currencyId] ?? [ + 'currency_id' => $currency['currency_id'], + 'currency_symbol' => $currency['currency_symbol'], + 'currency_name' => $currency['currency_name'], + 'currency_decimal_places' => $currency['currency_decimal_places'], + 'tags' => [], + ]; + + $report[$sourceAccountId]['currencies'][$currencyId]['tags'][$tag['id']] + = $report[$sourceAccountId]['currencies'][$currencyId]['tags'][$tag['id']] + ?? + [ + 'spent' => '0', + 'earned' => '0', + 'sum' => '0', + ]; + $report[$sourceAccountId]['currencies'][$currencyId]['tags'][$tag['id']]['spent'] = bcadd( + $report[$sourceAccountId]['currencies'][$currencyId]['tags'][$tag['id']]['spent'], $journal['amount'] + ); + $report[$sourceAccountId]['currencies'][$currencyId]['tags'][$tag['id']]['sum'] = bcadd( + $report[$sourceAccountId]['currencies'][$currencyId]['tags'][$tag['id']]['sum'], $journal['amount'] + ); + } + } + } + + + // loop income. + foreach ($earned as $currency) { + $currencyId = $currency['currency_id']; + + /** @var array $category */ + foreach ($currency['tags'] as $tag) { + foreach ($tag['transaction_journals'] as $journal) { + $destinationId = $journal['destination_account_id']; + $report[$destinationId]['currencies'][$currencyId] + = $report[$destinationId]['currencies'][$currencyId] + ?? [ + 'currency_id' => $currency['currency_id'], + 'currency_symbol' => $currency['currency_symbol'], + 'currency_name' => $currency['currency_name'], + 'currency_decimal_places' => $currency['currency_decimal_places'], + 'tags' => [], + ]; + + + $report[$destinationId]['currencies'][$currencyId]['tags'][$tag['id']] + = $report[$destinationId]['currencies'][$currencyId]['tags'][$tag['id']] + ?? + [ + 'spent' => '0', + 'earned' => '0', + 'sum' => '0', + ]; + $report[$destinationId]['currencies'][$currencyId]['tags'][$tag['id']]['earned'] = bcadd( + $report[$destinationId]['currencies'][$currencyId]['tags'][$tag['id']]['earned'], $journal['amount'] + ); + $report[$destinationId]['currencies'][$currencyId]['tags'][$tag['id']]['sum'] = bcadd( + $report[$destinationId]['currencies'][$currencyId]['tags'][$tag['id']]['sum'], $journal['amount'] + ); + } + } + } + + return view('reports.tag.partials.account-per-tag', compact('report', 'tags')); + } + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function accounts(Collection $accounts, Collection $tags, Carbon $start, Carbon $end) + { + $spent = $this->opsRepository->listExpenses($start, $end, $accounts, $tags); + $earned = $this->opsRepository->listIncome($start, $end, $accounts, $tags); + $report = []; + $sums = []; + /** @var Account $account */ + foreach ($accounts as $account) { + $accountId = $account->id; + $report[$accountId] = $report[$accountId] ?? [ + 'name' => $account->name, + 'id' => $account->id, + 'iban' => $account->iban, + 'currencies' => [], + ]; + } + + // loop expenses. + foreach ($spent as $currency) { + $currencyId = $currency['currency_id']; + $sums[$currencyId] = $sums[$currencyId] ?? [ + 'currency_id' => $currency['currency_id'], + 'currency_symbol' => $currency['currency_symbol'], + 'currency_name' => $currency['currency_name'], + 'currency_decimal_places' => $currency['currency_decimal_places'], + 'spent_sum' => '0', + 'earned_sum' => '0', + 'total_sum' => '0', + ]; + foreach ($currency['tags'] as $tag) { + foreach ($tag['transaction_journals'] as $journal) { + $sourceAccountId = $journal['source_account_id']; + $report[$sourceAccountId]['currencies'][$currencyId] = $report[$sourceAccountId]['currencies'][$currencyId] ?? [ + 'currency_id' => $currency['currency_id'], + 'currency_symbol' => $currency['currency_symbol'], + 'currency_name' => $currency['currency_name'], + 'currency_decimal_places' => $currency['currency_decimal_places'], + 'spent' => '0', + 'earned' => '0', + 'sum' => '0', + ]; + $report[$sourceAccountId]['currencies'][$currencyId]['spent'] = bcadd( + $report[$sourceAccountId]['currencies'][$currencyId]['spent'], $journal['amount'] + ); + $report[$sourceAccountId]['currencies'][$currencyId]['sum'] = bcadd( + $report[$sourceAccountId]['currencies'][$currencyId]['sum'], $journal['amount'] + ); + $sums[$currencyId]['spent_sum'] = bcadd($sums[$currencyId]['spent_sum'], $journal['amount']); + $sums[$currencyId]['total_sum'] = bcadd($sums[$currencyId]['total_sum'], $journal['amount']); + } + } + } + + // loop income. + foreach ($earned as $currency) { + $currencyId = $currency['currency_id']; + $sums[$currencyId] = $sums[$currencyId] ?? [ + 'currency_id' => $currency['currency_id'], + 'currency_symbol' => $currency['currency_symbol'], + 'currency_name' => $currency['currency_name'], + 'currency_decimal_places' => $currency['currency_decimal_places'], + 'spent_sum' => '0', + 'earned_sum' => '0', + 'total_sum' => '0', + ]; + foreach ($currency['tags'] as $tag) { + foreach ($tag['transaction_journals'] as $journal) { + $destinationAccountId = $journal['destination_account_id']; + $report[$destinationAccountId]['currencies'][$currencyId] = $report[$destinationAccountId]['currencies'][$currencyId] ?? [ + 'currency_id' => $currency['currency_id'], + 'currency_symbol' => $currency['currency_symbol'], + 'currency_name' => $currency['currency_name'], + 'currency_decimal_places' => $currency['currency_decimal_places'], + 'spent' => '0', + 'earned' => '0', + 'sum' => '0', + ]; + $report[$destinationAccountId]['currencies'][$currencyId]['earned'] = bcadd( + $report[$destinationAccountId]['currencies'][$currencyId]['earned'], $journal['amount'] + ); + $report[$destinationAccountId]['currencies'][$currencyId]['sum'] = bcadd( + $report[$destinationAccountId]['currencies'][$currencyId]['sum'], $journal['amount'] + ); + $sums[$currencyId]['earned_sum'] = bcadd($sums[$currencyId]['earned_sum'], $journal['amount']); + $sums[$currencyId]['total_sum'] = bcadd($sums[$currencyId]['total_sum'], $journal['amount']); + } + } + } + + return view('reports.tag.partials.accounts', compact('sums', 'report')); + } + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return array|string + */ + public function avgExpenses(Collection $accounts, Collection $tags, Carbon $start, Carbon $end) + { + $spent = $this->opsRepository->listExpenses($start, $end, $accounts, $tags); + $result = []; + foreach ($spent as $currency) { + $currencyId = $currency['currency_id']; + foreach ($currency['tags'] as $tag) { + foreach ($tag['transaction_journals'] as $journal) { + $destinationId = $journal['destination_account_id']; + $key = sprintf('%d-%d', $destinationId, $currency['currency_id']); + $result[$key] = $result[$key] ?? [ + 'transactions' => 0, + 'sum' => '0', + 'avg' => '0', + 'avg_float' => 0, + 'destination_account_name' => $journal['destination_account_name'], + 'destination_account_id' => $journal['destination_account_id'], + 'currency_id' => $currency['currency_id'], + 'currency_name' => $currency['currency_name'], + 'currency_symbol' => $currency['currency_symbol'], + 'currency_decimal_places' => $currency['currency_decimal_places'], + ]; + $result[$key]['transactions']++; + $result[$key]['sum'] = bcadd($journal['amount'], $result[$key]['sum']); + $result[$key]['avg'] = bcdiv($result[$key]['sum'], (string)$result[$key]['transactions']); + $result[$key]['avg_float'] = (float)$result[$key]['avg']; + } + } + } + // sort by amount_float + // sort temp array by amount. + $amounts = array_column($result, 'avg_float'); + array_multisort($amounts, SORT_ASC, $result); + + try { + $result = view('reports.tag.partials.avg-expenses', compact('result'))->render(); + // @codeCoverageIgnoreStart + } catch (Throwable $e) { + Log::debug(sprintf('Could not render reports.partials.budget-period: %s', $e->getMessage())); + $result = sprintf('Could not render view: %s', $e->getMessage()); + } + + return $result; + } + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return array|string + */ + public function avgIncome(Collection $accounts, Collection $tags, Carbon $start, Carbon $end) + { + $spent = $this->opsRepository->listIncome($start, $end, $accounts, $tags); + $result = []; + foreach ($spent as $currency) { + $currencyId = $currency['currency_id']; + foreach ($currency['tags'] as $tag) { + foreach ($tag['transaction_journals'] as $journal) { + $sourceId = $journal['source_account_id']; + $key = sprintf('%d-%d', $sourceId, $currency['currency_id']); + $result[$key] = $result[$key] ?? [ + 'transactions' => 0, + 'sum' => '0', + 'avg' => '0', + 'avg_float' => 0, + 'source_account_name' => $journal['source_account_name'], + 'source_account_id' => $journal['source_account_id'], + 'currency_id' => $currency['currency_id'], + 'currency_name' => $currency['currency_name'], + 'currency_symbol' => $currency['currency_symbol'], + 'currency_decimal_places' => $currency['currency_decimal_places'], + ]; + $result[$key]['transactions']++; + $result[$key]['sum'] = bcadd($journal['amount'], $result[$key]['sum']); + $result[$key]['avg'] = bcdiv($result[$key]['sum'], (string)$result[$key]['transactions']); + $result[$key]['avg_float'] = (float)$result[$key]['avg']; + } + } + } + // sort by amount_float + // sort temp array by amount. + $amounts = array_column($result, 'avg_float'); + array_multisort($amounts, SORT_DESC, $result); + + try { + $result = view('reports.tag.partials.avg-income', compact('result'))->render(); + // @codeCoverageIgnoreStart + } catch (Throwable $e) { + Log::debug(sprintf('Could not render reports.partials.budget-period: %s', $e->getMessage())); + $result = sprintf('Could not render view: %s', $e->getMessage()); + } + + return $result; + } + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function tags(Collection $accounts, Collection $tags, Carbon $start, Carbon $end) + { + $spent = $this->opsRepository->listExpenses($start, $end, $accounts, $tags); + $earned = $this->opsRepository->listIncome($start, $end, $accounts, $tags); + $sums = []; + $report = []; + /** @var Tag $tag */ + foreach ($tags as $tag) { + $tagId = $tag->id; + $report[$tagId] = $report[$tagId] ?? [ + 'name' => $tag->tag, + 'id' => $tag->id, + 'currencies' => [], + ]; + } + foreach ($spent as $currency) { + $currencyId = $currency['currency_id']; + $sums[$currencyId] = $sums[$currencyId] ?? [ + 'currency_id' => $currency['currency_id'], + 'currency_symbol' => $currency['currency_symbol'], + 'currency_name' => $currency['currency_name'], + 'currency_decimal_places' => $currency['currency_decimal_places'], + 'earned_sum' => '0', + 'spent_sum' => '0', + 'total_sum' => '0', + ]; + /** @var array $tag */ + foreach ($currency['tags'] as $tag) { + $tagId = $tag['id']; + + foreach ($tag['transaction_journals'] as $journal) { + // add currency info to report array: + $report[$tagId]['currencies'][$currencyId] = $report[$tagId]['currencies'][$currencyId] ?? [ + 'spent' => '0', + 'earned' => '0', + 'sum' => '0', + 'currency_id' => $currency['currency_id'], + 'currency_symbol' => $currency['currency_symbol'], + 'currency_name' => $currency['currency_name'], + 'currency_decimal_places' => $currency['currency_decimal_places'], + ]; + $report[$tagId]['currencies'][$currencyId]['spent'] = bcadd( + $report[$tagId]['currencies'][$currencyId]['spent'], $journal['amount'] + ); + $report[$tagId]['currencies'][$currencyId]['sum'] = bcadd( + $report[$tagId]['currencies'][$currencyId]['sum'], $journal['amount'] + ); + + $sums[$currencyId]['spent_sum'] = bcadd($sums[$currencyId]['spent_sum'], $journal['amount']); + $sums[$currencyId]['total_sum'] = bcadd($sums[$currencyId]['total_sum'], $journal['amount']); + } + } + } + + foreach ($earned as $currency) { + $currencyId = $currency['currency_id']; + $sums[$currencyId] = $sums[$currencyId] ?? [ + 'currency_id' => $currency['currency_id'], + 'currency_symbol' => $currency['currency_symbol'], + 'currency_name' => $currency['currency_name'], + 'currency_decimal_places' => $currency['currency_decimal_places'], + 'earned_sum' => '0', + 'spent_sum' => '0', + 'total_sum' => '0', + ]; + /** @var array $tag */ + foreach ($currency['tags'] as $tag) { + $tagId = $tag['id']; + + foreach ($tag['transaction_journals'] as $journal) { + // add currency info to report array: + $report[$tagId]['currencies'][$currencyId] = $report[$tagId]['currencies'][$currencyId] ?? [ + 'spent' => '0', + 'earned' => '0', + 'sum' => '0', + 'currency_id' => $currency['currency_id'], + 'currency_symbol' => $currency['currency_symbol'], + 'currency_name' => $currency['currency_name'], + 'currency_decimal_places' => $currency['currency_decimal_places'], + ]; + $report[$tagId]['currencies'][$currencyId]['earned'] = bcadd( + $report[$tagId]['currencies'][$currencyId]['earned'], $journal['amount'] + ); + $report[$tagId]['currencies'][$currencyId]['sum'] = bcadd( + $report[$tagId]['currencies'][$currencyId]['sum'], $journal['amount'] + ); + + $sums[$currencyId]['earned_sum'] = bcadd($sums[$currencyId]['earned_sum'], $journal['amount']); + $sums[$currencyId]['total_sum'] = bcadd($sums[$currencyId]['total_sum'], $journal['amount']); + } + } + } + + return view('reports.tag.partials.tags', compact('sums', 'report')); + } + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return array|string + */ + public function topExpenses(Collection $accounts, Collection $tags, Carbon $start, Carbon $end) + { + $spent = $this->opsRepository->listExpenses($start, $end, $accounts, $tags); + $result = []; + foreach ($spent as $currency) { + $currencyId = $currency['currency_id']; + foreach ($currency['tags'] as $tag) { + foreach ($tag['transaction_journals'] as $journal) { + $result[] = [ + 'description' => $journal['description'], + 'transaction_group_id' => $journal['transaction_group_id'], + 'amount_float' => (float)$journal['amount'], + 'amount' => $journal['amount'], + 'date' => $journal['date']->formatLocalized($this->monthAndDayFormat), + 'destination_account_name' => $journal['destination_account_name'], + 'destination_account_id' => $journal['destination_account_id'], + 'currency_id' => $currency['currency_id'], + 'currency_name' => $currency['currency_name'], + 'currency_symbol' => $currency['currency_symbol'], + 'currency_decimal_places' => $currency['currency_decimal_places'], + 'tag_id' => $tag['id'], + 'tag_name' => $tag['name'], + ]; + } + } + } + // sort by amount_float + // sort temp array by amount. + $amounts = array_column($result, 'amount_float'); + array_multisort($amounts, SORT_ASC, $result); + + try { + $result = view('reports.tag.partials.top-expenses', compact('result'))->render(); + // @codeCoverageIgnoreStart + } catch (Throwable $e) { + Log::debug(sprintf('Could not render reports.partials.budget-period: %s', $e->getMessage())); + $result = sprintf('Could not render view: %s', $e->getMessage()); + } + + return $result; + } + + /** + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return array|string + */ + public function topIncome(Collection $accounts, Collection $tags, Carbon $start, Carbon $end) + { + $spent = $this->opsRepository->listIncome($start, $end, $accounts, $tags); + $result = []; + foreach ($spent as $currency) { + $currencyId = $currency['currency_id']; + foreach ($currency['tags'] as $tag) { + foreach ($tag['transaction_journals'] as $journal) { + $result[] = [ + 'description' => $journal['description'], + 'transaction_group_id' => $journal['transaction_group_id'], + 'amount_float' => (float)$journal['amount'], + 'amount' => $journal['amount'], + 'date' => $journal['date']->formatLocalized($this->monthAndDayFormat), + 'source_account_name' => $journal['source_account_name'], + 'source_account_id' => $journal['source_account_id'], + 'currency_id' => $currency['currency_id'], + 'currency_name' => $currency['currency_name'], + 'currency_symbol' => $currency['currency_symbol'], + 'currency_decimal_places' => $currency['currency_decimal_places'], + 'tag_id' => $tag['id'], + 'tag_name' => $tag['name'], + ]; + } + } + } + // sort by amount_float + // sort temp array by amount. + $amounts = array_column($result, 'amount_float'); + array_multisort($amounts, SORT_DESC, $result); + + try { + $result = view('reports.tag.partials.top-income', compact('result'))->render(); + // @codeCoverageIgnoreStart + } catch (Throwable $e) { + Log::debug(sprintf('Could not render reports.partials.budget-period: %s', $e->getMessage())); + $result = sprintf('Could not render view: %s', $e->getMessage()); + } + + return $result; + } + +} \ No newline at end of file diff --git a/app/Providers/TagServiceProvider.php b/app/Providers/TagServiceProvider.php index 3b70f8b1b7..f023309ab8 100644 --- a/app/Providers/TagServiceProvider.php +++ b/app/Providers/TagServiceProvider.php @@ -22,6 +22,8 @@ declare(strict_types=1); namespace FireflyIII\Providers; +use FireflyIII\Repositories\Tag\OperationsRepository; +use FireflyIII\Repositories\Tag\OperationsRepositoryInterface; use FireflyIII\Repositories\Tag\TagRepository; use FireflyIII\Repositories\Tag\TagRepositoryInterface; use Illuminate\Foundation\Application; @@ -58,5 +60,19 @@ class TagServiceProvider extends ServiceProvider return $repository; } ); + + $this->app->bind( + OperationsRepositoryInterface::class, + function (Application $app) { + /** @var OperationsRepository $repository */ + $repository = app(OperationsRepository::class); + + if ($app->auth->check()) { + $repository->setUser(auth()->user()); + } + + return $repository; + } + ); } } diff --git a/app/Repositories/Tag/OperationsRepository.php b/app/Repositories/Tag/OperationsRepository.php new file mode 100644 index 0000000000..f5986bd75e --- /dev/null +++ b/app/Repositories/Tag/OperationsRepository.php @@ -0,0 +1,242 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Repositories\Tag; + +use Carbon\Carbon; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; +use FireflyIII\Models\TransactionType; +use FireflyIII\User; +use Illuminate\Support\Collection; + +/** + * + * Class OperationsRepository + */ +class OperationsRepository implements OperationsRepositoryInterface +{ + /** @var User */ + private $user; + + /** + * This method returns a list of all the withdrawal transaction journals (as arrays) set in that period + * which have the specified tag(s) set to them. It's grouped per currency, with as few details in the array + * as possible. Amounts are always negative. + * + * @param Carbon $start + * @param Carbon $end + * @param Collection|null $accounts + * @param Collection|null $tags + * + * @return array + */ + public function listExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $tags = null): array + { + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setUser($this->user)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL]); + if (null !== $accounts && $accounts->count() > 0) { + $collector->setAccounts($accounts); + } + if (null !== $tags && $tags->count() > 0) { + $collector->setTags($tags); + } + if (null === $tags || (null !== $tags && 0 === $tags->count())) { + $collector->setTags($this->getTags()); + } + $collector->withCategoryInformation()->withAccountInformation()->withBudgetInformation()->withTagInformation(); + $journals = $collector->getExtractedJournals(); + $array = []; + $listedJournals = []; + + + foreach ($journals as $journal) { + $currencyId = (int)$journal['currency_id']; + $array[$currencyId] = $array[$currencyId] ?? [ + 'tags' => [], + 'currency_id' => $currencyId, + 'currency_name' => $journal['currency_name'], + 'currency_symbol' => $journal['currency_symbol'], + 'currency_code' => $journal['currency_code'], + 'currency_decimal_places' => $journal['currency_decimal_places'], + ]; + + // may have multiple tags: + foreach ($journal['tags'] as $tag) { + $tagId = (int)$tag['id']; + $tagName = (string)$tag['name']; + $journalId = (int)$journal['transaction_journal_id']; + + if (in_array($journalId, $listedJournals, true)) { + continue; + } + $listedJournals[] = $journalId; + $array[$currencyId]['tags'][$tagId] = $array[$currencyId]['tags'][$tagId] ?? [ + 'id' => $tagId, + 'name' => $tagName, + 'transaction_journals' => [], + ]; + + $array[$currencyId]['tags'][$tagId]['transaction_journals'][$journalId] = [ + 'amount' => app('steam')->negative($journal['amount']), + 'date' => $journal['date'], + 'source_account_id' => $journal['source_account_id'], + 'budget_name' => $journal['budget_name'], + 'category_name' => $journal['category_name'], + 'source_account_name' => $journal['source_account_name'], + 'destination_account_id' => $journal['destination_account_id'], + 'destination_account_name' => $journal['destination_account_name'], + 'description' => $journal['description'], + 'transaction_group_id' => $journal['transaction_group_id'], + ]; + } + } + + return $array; + } + + /** + * This method returns a list of all the deposit transaction journals (as arrays) set in that period + * which have the specified tag(s) set to them. It's grouped per currency, with as few details in the array + * as possible. Amounts are always positive. + * + * @param Carbon $start + * @param Carbon $end + * @param Collection|null $accounts + * @param Collection|null $tags + * + * @return array + */ + public function listIncome(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $tags = null): array + { + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setUser($this->user)->setRange($start, $end)->setTypes([TransactionType::DEPOSIT]); + if (null !== $accounts && $accounts->count() > 0) { + $collector->setAccounts($accounts); + } + if (null !== $tags && $tags->count() > 0) { + $collector->setTags($tags); + } + if (null === $tags || (null !== $tags && 0 === $tags->count())) { + $collector->setTags($this->getTags()); + } + $collector->withCategoryInformation()->withAccountInformation()->withBudgetInformation()->withTagInformation(); + $journals = $collector->getExtractedJournals(); + $array = []; + $listedJournals = []; + + foreach ($journals as $journal) { + $currencyId = (int)$journal['currency_id']; + $array[$currencyId] = $array[$currencyId] ?? [ + 'tags' => [], + 'currency_id' => $currencyId, + 'currency_name' => $journal['currency_name'], + 'currency_symbol' => $journal['currency_symbol'], + 'currency_code' => $journal['currency_code'], + 'currency_decimal_places' => $journal['currency_decimal_places'], + ]; + + // may have multiple tags: + foreach ($journal['tags'] as $tag) { + $tagId = (int)$tag['id']; + $tagName = (string)$tag['name']; + $journalId = (int)$journal['transaction_journal_id']; + + if (in_array($journalId, $listedJournals, true)) { + continue; + } + $listedJournals[] = $journalId; + + $array[$currencyId]['tags'][$tagId] = $array[$currencyId]['tags'][$tagId] ?? [ + 'id' => $tagId, + 'name' => $tagName, + 'transaction_journals' => [], + ]; + $journalId = (int)$journal['transaction_journal_id']; + $array[$currencyId]['tags'][$tagId]['transaction_journals'][$journalId] = [ + 'amount' => app('steam')->positive($journal['amount']), + 'date' => $journal['date'], + 'source_account_id' => $journal['source_account_id'], + 'budget_name' => $journal['budget_name'], + 'source_account_name' => $journal['source_account_name'], + 'destination_account_id' => $journal['destination_account_id'], + 'destination_account_name' => $journal['destination_account_name'], + 'description' => $journal['description'], + 'transaction_group_id' => $journal['transaction_group_id'], + ]; + } + } + + return $array; + } + + /** + * @param User $user + */ + public function setUser(User $user): void + { + $this->user = $user; + } + + /** + * Sum of withdrawal journals in period for a set of tags, grouped per currency. Amounts are always negative. + * + * @param Carbon $start + * @param Carbon $end + * @param Collection|null $accounts + * @param Collection|null $tags + * + * @return array + */ + public function sumExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $tags = null): array + { + throw new FireflyException(sprintf('%s is not yet implemented.', __METHOD__)); + } + + /** + * Sum of income journals in period for a set of tags, grouped per currency. Amounts are always positive. + * + * @param Carbon $start + * @param Carbon $end + * @param Collection|null $accounts + * @param Collection|null $tags + * + * @return array + */ + public function sumIncome(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $tags = null): array + { + throw new FireflyException(sprintf('%s is not yet implemented.', __METHOD__)); + } + + /** + * @return Collection + */ + private function getTags(): Collection + { + $repository = app(TagRepositoryInterface::class); + + return $repository->get(); + } +} \ No newline at end of file diff --git a/app/Repositories/Tag/OperationsRepositoryInterface.php b/app/Repositories/Tag/OperationsRepositoryInterface.php new file mode 100644 index 0000000000..5bca16897d --- /dev/null +++ b/app/Repositories/Tag/OperationsRepositoryInterface.php @@ -0,0 +1,93 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Repositories\Tag; + +use Carbon\Carbon; +use FireflyIII\User; +use Illuminate\Support\Collection; + +/** + * Interface OperationsRepositoryInterface + * + * @package FireflyIII\Repositories\Tag + */ +interface OperationsRepositoryInterface +{ + /** + * This method returns a list of all the withdrawal transaction journals (as arrays) set in that period + * which have the specified tag(s) set to them. It's grouped per currency, with as few details in the array + * as possible. Amounts are always negative. + * + * @param Carbon $start + * @param Carbon $end + * @param Collection|null $accounts + * @param Collection|null $tags + * + * @return array + */ + public function listExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $tags = null): array; + + /** + * This method returns a list of all the deposit transaction journals (as arrays) set in that period + * which have the specified tag(s) set to them. It's grouped per currency, with as few details in the array + * as possible. Amounts are always positive. + * + * @param Carbon $start + * @param Carbon $end + * @param Collection|null $accounts + * @param Collection|null $tags + * + * @return array + */ + public function listIncome(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $tags = null): array; + + /** + * @param User $user + */ + public function setUser(User $user): void; + + /** + * Sum of withdrawal journals in period for a set of tags, grouped per currency. Amounts are always negative. + * + * @param Carbon $start + * @param Carbon $end + * @param Collection|null $accounts + * @param Collection|null $tags + * + * @return array + */ + public function sumExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $tags = null): array; + + /** + * Sum of income journals in period for a set of tags, grouped per currency. Amounts are always positive. + * + * @param Carbon $start + * @param Carbon $end + * @param Collection|null $accounts + * @param Collection|null $tags + * + * @return array + */ + public function sumIncome(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $tags = null): array; +} \ No newline at end of file diff --git a/public/v1/js/ff/reports/category/month.js b/public/v1/js/ff/reports/category/month.js index dd88a03c05..27ee0ec77e 100644 --- a/public/v1/js/ff/reports/category/month.js +++ b/public/v1/js/ff/reports/category/month.js @@ -19,30 +19,6 @@ */ $(function () { "use strict"; - drawChart(); - - - // $('#categories-in-pie-chart-checked').on('change', function () { - // redrawPieChart(categoryIncomeUri, 'categories-in-pie-chart'); - // }); - // - // $('#categories-out-pie-chart-checked').on('change', function () { - // redrawPieChart(categoryExpenseUri, 'categories-out-pie-chart'); - // }); - // - // $('#accounts-in-pie-chart-checked').on('change', function () { - // redrawPieChart(accountIncomeUri, 'accounts-in-pie-chart'); - // }); - // - // $('#accounts-out-pie-chart-checked').on('change', function () { - // redrawPieChart(accountExpenseUri, 'accounts-out-pie-chart'); - // }); - -}); - - -function drawChart() { - "use strict"; loadAjaxPartial('accountsHolder', accountsUri); loadAjaxPartial('categoriesHolder', categoriesUri); @@ -65,4 +41,5 @@ function drawChart() { loadAjaxPartial('avgExpensesHolder', avgExpensesUri); loadAjaxPartial('topIncomeHolder', topIncomeUri); loadAjaxPartial('avgIncomeHolder', avgIncomeUri); -} + +}); \ No newline at end of file diff --git a/public/v1/js/ff/reports/tag/month.js b/public/v1/js/ff/reports/tag/month.js index 01709c991b..9321df8294 100644 --- a/public/v1/js/ff/reports/tag/month.js +++ b/public/v1/js/ff/reports/tag/month.js @@ -18,61 +18,31 @@ * along with Firefly III. If not, see . */ -/** global: tagIncomeUri, tagExpenseUri, accountIncomeUri, accountExpenseUri, tagBudgetUri, tagCategoryUri, mainUri */ - $(function () { "use strict"; - drawChart(); + loadAjaxPartial('accountsHolder', accountsUri); + loadAjaxPartial('tagsHolder', tagsUri); + loadAjaxPartial('accountPerTagHolder', accountPerTagUri); - $('#tags-in-pie-chart-checked').on('change', function () { - redrawPieChart('tags-in-pie-chart', tagIncomeUri); + $.each($('.main_tag_canvas'), function (i, v) { + var canvas = $(v); + columnChart(canvas.data('url'), canvas.attr('id')); }); - $('#tags-out-pie-chart-checked').on('change', function () { - redrawPieChart('tags-out-pie-chart', tagExpenseUri); - }); + multiCurrencyPieChart(tagOutUri, 'tag-out-pie-chart'); + multiCurrencyPieChart(tagInUri, 'tag-in-pie-chart'); + multiCurrencyPieChart(categoryOutUri, 'category-out-pie-chart'); + multiCurrencyPieChart(categoryInUri, 'category-in-pie-chart'); + multiCurrencyPieChart(budgetsOutUri, 'budgets-out-pie-chart'); + multiCurrencyPieChart(sourceOutUri, 'source-out-pie-chart'); + multiCurrencyPieChart(sourceInUri, 'source-in-pie-chart'); + multiCurrencyPieChart(destOutUri, 'dest-out-pie-chart'); + multiCurrencyPieChart(destInUri, 'dest-in-pie-chart'); - $('#accounts-in-pie-chart-checked').on('change', function () { - redrawPieChart('accounts-in-pie-chart', accountIncomeUri); - }); - - $('#accounts-out-pie-chart-checked').on('change', function () { - redrawPieChart('accounts-out-pie-chart', accountExpenseUri); - }); - - // two extra charts: - pieChart(tagBudgetUri, 'budgets-out-pie-chart'); - pieChart(tagCategoryUri, 'categories-out-pie-chart'); + loadAjaxPartial('topExpensesHolder', topExpensesUri); + loadAjaxPartial('avgExpensesHolder', avgExpensesUri); + loadAjaxPartial('topIncomeHolder', topIncomeUri); + loadAjaxPartial('avgIncomeHolder', avgIncomeUri); }); - -function drawChart() { - "use strict"; - - // month view: - doubleYChart(mainUri, 'in-out-chart'); - - // draw pie chart of income, depending on "show other transactions too": - redrawPieChart('tags-in-pie-chart', tagIncomeUri); - redrawPieChart('tags-out-pie-chart', tagExpenseUri); - redrawPieChart('accounts-in-pie-chart', accountIncomeUri); - redrawPieChart('accounts-out-pie-chart', accountExpenseUri); - - -} - -function redrawPieChart(container, uri) { - "use strict"; - var checkbox = $('#' + container + '-checked'); - - var others = '0'; - // check if box is checked: - if (checkbox.prop('checked')) { - others = '1'; - } - uri = uri.replace('OTHERS', others); - - pieChart(uri, container); - -} diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 9098291d2d..1554fb0330 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -897,9 +897,15 @@ return [ 'income_per_destination_account' => 'Income per destination account', 'spent_in_specific_category' => 'Spent in category ":category"', 'earned_in_specific_category' => 'Earned in category ":category"', + 'spent_in_specific_tag' => 'Spent in tag ":tag"', + 'earned_in_specific_tag' => 'Earned in tag ":tag"', 'income_per_source_account' => 'Income per source account', 'average_spending_per_destination' => 'Average expense per destination account', 'average_earning_per_source' => 'Average earning per source account', + 'account_per_tag' => 'Account per tag', + 'tag_report_expenses_listed_once' => 'Expenses and income are never listed twice. If a transaction has multiple tags, it may only show up under one of its tags. This list may appear to be missing data, but the amounts will be correct.', + 'tag_report_chart_single_tag' => 'This chart applies to a single tag. If a transaction has multiple tags, what you see here may be reflected in the charts of other tags as well.', + 'tag' => 'Tag', 'no_budget_squared' => '(no budget)', 'perm-delete-many' => 'Deleting many items in one go can be very disruptive. Please be cautious. You can delete part of a split transaction from this page, so take care.', 'mass_deleted_transactions_success' => 'Deleted :amount transaction(s).', diff --git a/resources/views/v1/reports/tag/month.twig b/resources/views/v1/reports/tag/month.twig index a24b5f050a..0b426fdfd0 100644 --- a/resources/views/v1/reports/tag/month.twig +++ b/resources/views/v1/reports/tag/month.twig @@ -1,93 +1,114 @@ {% extends "./layout/default" %} {% block breadcrumbs %} - {{ Breadcrumbs.render(Route.getCurrentRoute.getName, accountIds, tagTags, start, end) }} + {#{{ Breadcrumbs.render(Route.getCurrentRoute.getName, accountIds, tagTags, start, end) }}#} {% endblock %} {% block content %}
-
+

{{ 'accounts'|_ }}

-
- - - - - - - - - - {% for account in accounts %} - - - {% if accountSummary[account.id] %} - - {% else %} - - {% endif %} - {% if accountSummary[account.id] %} - - {% else %} - - {% endif %} - - {% endfor %} - - - - - - - - -
{{ 'name'|_ }}{{ 'earned'|_ }}{{ 'spent'|_ }}
- {{ account.name }} - {{ accountSummary[account.id].earned|formatAmount }}{{ 0|formatAmount }}{{ accountSummary[account.id].spent|formatAmount }}{{ 0|formatAmount }}
{{ 'sum'|_ }}{{ accountSummary.sum.earned|formatAmount }}{{ accountSummary.sum.spent|formatAmount }}
+
+
+ {# loading indicator #} +
+
-
-
+
+
-

{{ 'income_per_tag'|_ }}

+

{{ 'tags'|_ }}

-
- - +
+
+ {# loading indicator #} +
+
-
+
+
+
+
+
+

{{ 'account_per_tag'|_ }}

+
+
+
+ {# loading indicator #} +
+ +
+
+
+
+
+

{{ 'expense_per_tag'|_ }}

- - +
+ +
-
+
+
+
+

{{ 'income_per_tag'|_ }}

+
+
+
+ +
+
+
+
+
+
+
+
+
+

{{ 'expense_per_category'|_ }}

+
+
+
+ +
+
+
+
+
+
+
+

{{ 'income_per_category'|_ }}

+
+
+
+ +
+
+
+
+

{{ 'expense_per_budget'|_ }}

- +
+ +
@@ -96,355 +117,128 @@
-

{{ 'tags'|_ }}

+

{{ 'expense_per_source_account'|_ }}

-
- - - - - - - - - - {% for tag in tags %} - - - {% if tagSummary[tag.id] %} - - {% else %} - - {% endif %} - {% if tagSummary[tag.id] %} - - {% else %} - - {% endif %} - - {% endfor %} - - - - - - - - -
{{ 'name'|_ }}{{ 'earned'|_ }}{{ 'spent'|_ }}
- {{ tag.tag }} - {{ tagSummary[tag.id].earned|formatAmount }}{{ 0|formatAmount }}{{ tagSummary[tag.id].spent|formatAmount }}{{ 0|formatAmount }}
{{ 'sum'|_ }}{{ tagSummary.sum.earned|formatAmount }}{{ tagSummary.sum.spent|formatAmount }}
+
+
+ +
-
+
-

{{ 'income_per_account'|_ }}

+

{{ 'income_per_source_account'|_ }}

- - -
-
-
-
-
-
-

{{ 'expense_per_account'|_ }}

-
-
- - -
-
-
- -
-
-
-

{{ 'expense_per_category'|_ }}

-
-
- -
-
-
- -
- -
-
-
-
-

{{ 'income_and_expenses'|_ }}

-
-
- +
+ +
- {% if averageExpenses|length > 0 %} -
-
-
-

{{ 'average_spending_per_account'|_ }}

-
-
- - - - - - - - - - - {% set totalCount = 0 %} - {% set totalSum = 0 %} - {% for row in averageExpenses %} - {% set totalCount = totalCount+ row.count %} - {% set totalSum = totalSum + row.sum %} - {% if loop.index > listLength %} - - {% else %} - - {% endif %} - - - - - - {% endfor %} - - - {% if averageExpenses|length > listLength %} - - - - {% endif %} - - - - - - -
{{ 'account'|_ }}{{ 'spent_average'|_ }}{{ 'total'|_ }}{{ 'transaction_count'|_ }}
- {{ row.name }} - - {{ row.average|formatAmount }} - - {{ row.sum|formatAmount }} - - {{ row.count }} -
- {{ trans('firefly.show_full_list',{number:incomeTopLength}) }} -
- {{ 'sum'|_ }} - {{ totalSum|formatAmount }}{{ totalCount }}
+
+
+
+

{{ 'expense_per_destination_account'|_ }}

+
+
+
+
- {% endif %} - {% if topExpenses|length > 0 %} -
+
+
+
+
+

{{ 'income_per_destination_account'|_ }}

+
+
+
+ +
+
+
+
+
-
+ {% for tag in tags %} +
+
+
-

{{ 'expenses'|_ }} ({{ trans('firefly.topX', {number: listLength}) }})

+

{{ 'income_and_expenses'|_ }} ({{ tag.tag }})

- - - - - - - - - - - {% set totalSum = 0 %} - {% for row in topExpenses %} - {% set totalSum = totalSum + row.transaction_amount %} - {% if loop.index > listLength %} - - {% else %} - - {% endif %} - - - - - - {% endfor %} - - - {% if topExpenses|length > listLength %} - - - - {% endif %} - - - - - -
{{ 'description'|_ }}{{ 'date'|_ }}{{ 'account'|_ }}{{ 'amount'|_ }}
- - {% if row.group_title|length > 0 %} - {{ row.group_title }} ({{ row.description }}) - {% else %} - {{ row.description }} - {% endif %} - - - {{ row.date.formatLocalized(monthAndDayFormat) }} - - - {{ row.destination_account_name }} - - - {{ formatAmountBySymbol(row.amount, row.currency_symbol, row.currency_decimal_places) }} -
- {{ trans('firefly.show_full_list',{number:incomeTopLength}) }} -
- {{ 'sum'|_ }} - {{ totalSum|formatAmount }}
+ +
+
- {% endif %} -
+
+ {% endfor %}
- {% if averageIncome|length > 0 %} -
-
-
-

{{ 'average_income_per_account'|_ }}

-
-
- - - - - - - - - - - {% set totalCount = 0 %} - {% set totalSum = 0 %} - {% for row in averageIncome %} - {% set totalCount = totalCount+ row.count %} - {% set totalSum = totalSum + row.sum %} - - - - - - - {% endfor %} - - - - - - -
{{ 'account'|_ }}{{ 'income_average'|_ }}{{ 'total'|_ }}{{ 'transaction_count'|_ }}
- {{ row.name }} - - {{ (row.average*-1)|formatAmount }} - - {{ (row.sum*-1)|formatAmount }} - - {{ row.count }} -
- {{ 'sum'|_ }} - {{ (totalSum*-1)|formatAmount }}{{ totalCount }}
-
-
-
- {% endif %}
- {% if topIncome|length > 0 %} -
-
-

{{ 'income'|_ }} ({{ trans('firefly.topX', {number: listLength}) }})

-
-
- - - - - - - - - - - {% set totalSum = 0 %} - {% for row in topIncome %} - {% set totalSum = totalSum + row.transaction_amount %} - {% if loop.index > listLength %} - - {% else %} - - {% endif %} - - - - - - - {% endfor %} - - - {% if topIncome.count > listLength %} - - - - {% endif %} - - - - - -
{{ 'description'|_ }}{{ 'date'|_ }}{{ 'account'|_ }}{{ 'amount'|_ }}
- - {% if row.transaction_description|length > 0 %} - {{ row.transaction_description }} ({{ row.description }}) - {% else %} - {{ row.description }} - {% endif %} - - - {{ row.date.formatLocalized(monthAndDayFormat) }} - - - {{ row.source_account_name }} - - - {{ (row.transaction_amount*-1)|formatAmount }} -
- {{ trans('firefly.show_full_list',{number:incomeTopLength}) }} -
- {{ 'sum'|_ }} - {{ (totalSum*-1)|formatAmount }}
-
+
+
+

{{ 'average_spending_per_destination'|_ }}

- {% endif %} +
+
+ {# loading indicator #} +
+ +
+
+
+
+
+
+

{{ 'average_earning_per_source'|_ }}

+
+
+
+ {# loading indicator #} +
+ +
+
+
+
+
+
+
+
+

{{ 'expenses'|_ }} ({{ trans('firefly.topX', {number: listLength}) }})

+
+
+
+ {# loading indicator #} +
+ +
+
+
+
+
+
+

{{ 'income'|_ }} ({{ trans('firefly.topX', {number: listLength}) }})

+
+
+
+ {# loading indicator #} +
+ +
+
@@ -456,26 +250,37 @@ + - - {% endblock %} diff --git a/resources/views/v1/reports/tag/partials/account-per-tag.twig b/resources/views/v1/reports/tag/partials/account-per-tag.twig new file mode 100644 index 0000000000..2df6c1c0ae --- /dev/null +++ b/resources/views/v1/reports/tag/partials/account-per-tag.twig @@ -0,0 +1,40 @@ + + + + + {% for tag in tags %} + + {% endfor %} + + + + {% for account in report %} + {% for currency in account.currencies %} + + + {% for tag in tags %} + + {% endfor %} + {% endfor %} + + + + + + +
{{ 'name'|_ }}{{ tag.tag }}
+ {{ account.name }} ({{ currency.currency_name }}) + + {% if currency.tags[tag.id] %} + + {% endfor %} +
+

+ {{ 'tag_report_expenses_listed_once'|_ }} +

+
\ No newline at end of file diff --git a/resources/views/v1/reports/tag/partials/accounts.twig b/resources/views/v1/reports/tag/partials/accounts.twig new file mode 100644 index 0000000000..5fd35138ef --- /dev/null +++ b/resources/views/v1/reports/tag/partials/accounts.twig @@ -0,0 +1,46 @@ + + + + + + + + + + + {% for account in report %} + {% for currency in account.currencies %} + + + + + + + {% endfor %} + {% endfor %} + + + {% for sum in sums %} + + + + + + + {% endfor %} + +
{{ 'name'|_ }}{{ 'spent'|_ }}{{ 'earned'|_ }}{{ 'sum'|_ }}
+ {{ account.name }} ({{ currency.currency_name }}) + + {{ formatAmountBySymbol(currency.spent, currency.currency_symbol, currency.currency_decimal_places) }} + + {{ formatAmountBySymbol(currency.earned, currency.currency_symbol, currency.currency_decimal_places) }} + + {{ formatAmountBySymbol(currency.sum, currency.currency_symbol, currency.currency_decimal_places) }} +
{{ 'sum'|_ }} ({{ sum.currency_name }}) + {{ formatAmountBySymbol(sum.spent_sum, sum.currency_symbol, sum.currency_decimal_places) }} + + {{ formatAmountBySymbol(sum.earned_sum, sum.currency_symbol, sum.currency_decimal_places) }} + + {{ formatAmountBySymbol(sum.total_sum, sum.currency_symbol, sum.currency_decimal_places) }} +
\ No newline at end of file diff --git a/resources/views/v1/reports/tag/partials/avg-expenses.twig b/resources/views/v1/reports/tag/partials/avg-expenses.twig new file mode 100644 index 0000000000..b34a2ecc63 --- /dev/null +++ b/resources/views/v1/reports/tag/partials/avg-expenses.twig @@ -0,0 +1,46 @@ + + + + + + + + + + + {% for row in result %} + {% if loop.index > listLength %} + + {% else %} + + {% endif %} + + + + + + + + + {% endfor %} + + + {% if result|length > listLength %} + + + + {% endif %} + +
{{ 'account'|_ }}{{ 'spent_average'|_ }}{{ 'total'|_ }}{{ 'transaction_count'|_ }}
+ + {{ row.destination_account_name }} + + + {{ formatAmountBySymbol(row.avg, row.currency_symbol, row.currency_decimal_places) }} + + {{ formatAmountBySymbol(row.sum, row.currency_symbol, row.currency_decimal_places) }} + + {{ row.transactions }} +
+ {{ trans('firefly.show_full_list',{number:incomeTopLength}) }} +
diff --git a/resources/views/v1/reports/tag/partials/avg-income.twig b/resources/views/v1/reports/tag/partials/avg-income.twig new file mode 100644 index 0000000000..b859f13a09 --- /dev/null +++ b/resources/views/v1/reports/tag/partials/avg-income.twig @@ -0,0 +1,46 @@ + + + + + + + + + + + {% for row in result %} + {% if loop.index > listLength %} + + {% else %} + + {% endif %} + + + + + + + + + {% endfor %} + + + {% if result|length > listLength %} + + + + {% endif %} + +
{{ 'account'|_ }}{{ 'spent_average'|_ }}{{ 'total'|_ }}{{ 'transaction_count'|_ }}
+ + {{ row.source_account_name }} + + + {{ formatAmountBySymbol(row.avg, row.currency_symbol, row.currency_decimal_places) }} + + {{ formatAmountBySymbol(row.sum, row.currency_symbol, row.currency_decimal_places) }} + + {{ row.transactions }} +
+ {{ trans('firefly.show_full_list',{number:incomeTopLength}) }} +
diff --git a/resources/views/v1/reports/tag/partials/tags.twig b/resources/views/v1/reports/tag/partials/tags.twig new file mode 100644 index 0000000000..fb12820f3d --- /dev/null +++ b/resources/views/v1/reports/tag/partials/tags.twig @@ -0,0 +1,64 @@ + + + + + + + + + + + {% for tag in report %} + {% if tag.currencies|length == 0 %} + + + + + + + {% endif %} + {% for currency in tag.currencies %} + + + + + + + {% endfor %} + {% endfor %} + + + {% for sum in sums %} + + + + + + + {% endfor %} + + + + +
{{ 'name'|_ }}{{ 'spent'|_ }}{{ 'earned'|_ }}{{ 'sum'|_ }}
+ {{ tag.name }} +
+ {{ tag.name }} ({{ currency.currency_name }}) + + {{ formatAmountBySymbol(currency.spent, currency.currency_symbol, currency.currency_decimal_places) }} + + {{ formatAmountBySymbol(currency.earned, currency.currency_symbol, currency.currency_decimal_places) }} + + {{ formatAmountBySymbol(currency.sum, currency.currency_symbol, currency.currency_decimal_places) }} +
{{ 'sum'|_ }} ({{ sum.currency_name }}) + {{ formatAmountBySymbol(sum.spent_sum, sum.currency_symbol, sum.currency_decimal_places) }} + + {{ formatAmountBySymbol(sum.earned_sum, sum.currency_symbol, sum.currency_decimal_places) }} + + {{ formatAmountBySymbol(sum.total_sum, sum.currency_symbol, sum.currency_decimal_places) }} +
+

+ {{ 'tag_report_expenses_listed_once'|_ }} +

+
+ diff --git a/resources/views/v1/reports/tag/partials/top-expenses.twig b/resources/views/v1/reports/tag/partials/top-expenses.twig new file mode 100644 index 0000000000..ef0bab8f92 --- /dev/null +++ b/resources/views/v1/reports/tag/partials/top-expenses.twig @@ -0,0 +1,54 @@ + + + + + + + + + + + + {% for row in result %} + {% if loop.index > listLength %} + + {% else %} + + {% endif %} + + + + + + + + + + {% endfor %} + + + {% if result|length > listLength %} + + + + {% endif %} + +
{{ 'description'|_ }}{{ 'date'|_ }}{{ 'account'|_ }}{{ 'tag'|_ }}{{ 'amount'|_ }}
+ + {{ row.description }} + + + {{ row.date }} + + + {{ row.destination_account_name }} + + + + {{ row.tag_name }} + + + {{ formatAmountBySymbol(row.amount, row.currency_symbol, row.currency_decimal_places) }} +
+ {{ trans('firefly.show_full_list',{number:incomeTopLength}) }} +
diff --git a/resources/views/v1/reports/tag/partials/top-income.twig b/resources/views/v1/reports/tag/partials/top-income.twig new file mode 100644 index 0000000000..f2c6e293e8 --- /dev/null +++ b/resources/views/v1/reports/tag/partials/top-income.twig @@ -0,0 +1,54 @@ + + + + + + + + + + + + {% for row in result %} + {% if loop.index > listLength %} + + {% else %} + + {% endif %} + + + + + + + + + + {% endfor %} + + + {% if result|length > listLength %} + + + + {% endif %} + +
{{ 'description'|_ }}{{ 'date'|_ }}{{ 'account'|_ }}{{ 'tag'|_ }}{{ 'amount'|_ }}
+ + {{ row.description }} + + + {{ row.date }} + + + {{ row.source_account_name }} + + + + {{ row.tag_name }} + + + {{ formatAmountBySymbol(row.amount, row.currency_symbol, row.currency_decimal_places) }} +
+ {{ trans('firefly.show_full_list',{number:incomeTopLength}) }} +
diff --git a/routes/web.php b/routes/web.php index a565962ead..01dab14946 100644 --- a/routes/web.php +++ b/routes/web.php @@ -423,40 +423,17 @@ Route::group( Route::group( ['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers\Chart', 'prefix' => 'chart/tag', 'as' => 'chart.tag.'], static function () { - // these charts are used in reports (tag reports): - Route::get( - 'tag/income/{accountList}/{tagList}/{start_date}/{end_date}', - ['uses' => 'TagReportController@tagIncome', 'as' => 'tag-income'] - ); - Route::get( - 'tag/expense/{accountList}/{tagList}/{start_date}/{end_date}', - ['uses' => 'TagReportController@tagExpense', 'as' => 'tag-expense'] - ); - Route::get( - 'account/income/{accountList}/{tagList}/{start_date}/{end_date}', - ['uses' => 'TagReportController@accountIncome', 'as' => 'account-income'] - ); - Route::get( - 'account/expense/{accountList}/{tagList}/{start_date}/{end_date}', - ['uses' => 'TagReportController@accountExpense', 'as' => 'account-expense'] - ); + Route::get('tag/expense/{accountList}/{tagList}/{start_date}/{end_date}', ['uses' => 'TagReportController@tagExpense', 'as' => 'tag-expense']); + Route::get('tag/income/{accountList}/{tagList}/{start_date}/{end_date}', ['uses' => 'TagReportController@tagIncome', 'as' => 'tag-income']); + Route::get('category/expense/{accountList}/{tagList}/{start_date}/{end_date}', ['uses' => 'TagReportController@categoryExpense', 'as' => 'category-expense']); + Route::get('category/income/{accountList}/{tagList}/{start_date}/{end_date}', ['uses' => 'TagReportController@categoryIncome', 'as' => 'category-income']); + Route::get('budget/expense/{accountList}/{tagList}/{start_date}/{end_date}', ['uses' => 'TagReportController@budgetExpense', 'as' => 'budget-expense']); + Route::get('source/expense/{accountList}/{tagList}/{start_date}/{end_date}', ['uses' => 'TagReportController@sourceExpense', 'as' => 'source-expense']); + Route::get('source/income/{accountList}/{tagList}/{start_date}/{end_date}', ['uses' => 'TagReportController@sourceIncome', 'as' => 'source-income']); + Route::get('dest/expense/{accountList}/{tagList}/{start_date}/{end_date}', ['uses' => 'tagReportController@destinationExpense', 'as' => 'dest-expense']); + Route::get('dest/income/{accountList}/{tagList}/{start_date}/{end_date}', ['uses' => 'TagReportController@destinationIncome', 'as' => 'dest-income']); - // new routes - Route::get( - 'budget/expense/{accountList}/{tagList}/{start_date}/{end_date}', - ['uses' => 'TagReportController@budgetExpense', 'as' => 'budget-expense'] - ); - Route::get( - 'category/expense/{accountList}/{tagList}/{start_date}/{end_date}', - ['uses' => 'TagReportController@categoryExpense', 'as' => 'category-expense'] - - ); - - - Route::get( - 'operations/{accountList}/{tagList}/{start_date}/{end_date}', - ['uses' => 'TagReportController@mainChart', 'as' => 'main'] - ); + Route::get('operations/{accountList}/{tag}/{start_date}/{end_date}', ['uses' => 'TagReportController@mainChart', 'as' => 'main']); } ); @@ -786,10 +763,7 @@ Route::group( Route::get('accounts/{accountList}/{categoryList}/{start_date}/{end_date}', ['uses' => 'CategoryController@accounts', 'as' => 'accounts']); Route::get('categories/{accountList}/{categoryList}/{start_date}/{end_date}', ['uses' => 'CategoryController@categories', 'as' => 'categories']); - Route::get( - 'account-per-category/{accountList}/{categoryList}/{start_date}/{end_date}', - ['uses' => 'CategoryController@accountPerCategory', 'as' => 'account-per-category'] - ); + Route::get('account-per-category/{accountList}/{categoryList}/{start_date}/{end_date}', ['uses' => 'CategoryController@accountPerCategory', 'as' => 'account-per-category']); Route::get('top-expenses/{accountList}/{categoryList}/{start_date}/{end_date}', ['uses' => 'CategoryController@topExpenses', 'as' => 'top-expenses']); Route::get('avg-expenses/{accountList}/{categoryList}/{start_date}/{end_date}', ['uses' => 'CategoryController@avgExpenses', 'as' => 'avg-expenses']); @@ -799,6 +773,25 @@ Route::group( } ); +/** + * Report Data TAG Controller + */ +Route::group( + ['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers\Report', 'prefix' => 'report-data/tag', + 'as' => 'report-data.tag.'], static function () { + + Route::get('accounts/{accountList}/{tagList}/{start_date}/{end_date}', ['uses' => 'TagController@accounts', 'as' => 'accounts']); + Route::get('tags/{accountList}/{tagList}/{start_date}/{end_date}', ['uses' => 'TagController@tags', 'as' => 'tags']); + Route::get('account-per-tag/{accountList}/{tagList}/{start_date}/{end_date}', ['uses' => 'TagController@accountPerTag', 'as' => 'account-per-tag']); + + Route::get('top-expenses/{accountList}/{tagList}/{start_date}/{end_date}', ['uses' => 'TagController@topExpenses', 'as' => 'top-expenses']); + Route::get('avg-expenses/{accountList}/{tagList}/{start_date}/{end_date}', ['uses' => 'TagController@avgExpenses', 'as' => 'avg-expenses']); + + Route::get('top-income/{accountList}/{tagList}/{start_date}/{end_date}', ['uses' => 'TagController@topIncome', 'as' => 'top-income']); + Route::get('avg-income/{accountList}/{tagList}/{start_date}/{end_date}', ['uses' => 'TagController@avgIncome', 'as' => 'avg-income']); +} +); + /** * Report Data Balance Controller */