From 1ff47441cefc131e8dec121bbce5eb635884081f Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 27 Sep 2025 16:07:03 +0200 Subject: [PATCH] Also add no budget and no category overview. --- .../Controllers/Budget/ShowController.php | 2 +- .../Category/NoCategoryController.php | 21 +- app/Models/PeriodStatistic.php | 6 + app/Models/UserGroup.php | 8 + app/Providers/FireflyServiceProvider.php | 13 +- .../PeriodStatisticRepository.php | 84 ++++-- .../PeriodStatisticRepositoryInterface.php | 2 + .../Http/Controllers/PeriodOverview.php | 246 ++++++++++-------- ..._09_25_175248_create_period_statistics.php | 5 + 9 files changed, 245 insertions(+), 142 deletions(-) diff --git a/app/Http/Controllers/Budget/ShowController.php b/app/Http/Controllers/Budget/ShowController.php index 36815436a4..ce344f13e8 100644 --- a/app/Http/Controllers/Budget/ShowController.php +++ b/app/Http/Controllers/Budget/ShowController.php @@ -92,7 +92,7 @@ class ShowController extends Controller // get first journal ever to set off the budget period overview. $first = $this->journalRepos->firstNull(); $firstDate = $first instanceof TransactionJournal ? $first->date : $start; - $periods = $this->getNoBudgetPeriodOverview($firstDate, $end); + $periods = $this->getNoModelPeriodOverview('budget', $firstDate, $end); $page = (int) $request->get('page'); $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; diff --git a/app/Http/Controllers/Category/NoCategoryController.php b/app/Http/Controllers/Category/NoCategoryController.php index be784e6aa6..4c12faeef3 100644 --- a/app/Http/Controllers/Category/NoCategoryController.php +++ b/app/Http/Controllers/Category/NoCategoryController.php @@ -35,6 +35,7 @@ use FireflyIII\Support\Http\Controllers\PeriodOverview; use Illuminate\Contracts\View\Factory; use Illuminate\Http\Request; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; use Illuminate\View\View; /** @@ -74,7 +75,7 @@ class NoCategoryController extends Controller */ public function show(Request $request, ?Carbon $start = null, ?Carbon $end = null) { - app('log')->debug('Start of noCategory()'); + Log::debug('Start of noCategory()'); $start ??= session('start'); $end ??= session('end'); @@ -82,14 +83,12 @@ class NoCategoryController extends Controller /** @var Carbon $end */ $page = (int) $request->get('page'); $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; - $subTitle = trans( - 'firefly.without_category_between', - ['start' => $start->isoFormat($this->monthAndDayFormat), 'end' => $end->isoFormat($this->monthAndDayFormat)] - ); - $periods = $this->getNoCategoryPeriodOverview($start); + $subTitle = trans('firefly.without_category_between', ['start' => $start->isoFormat($this->monthAndDayFormat), 'end' => $end->isoFormat($this->monthAndDayFormat)]); + $first = $this->journalRepos->firstNull()->date ?? clone $start; + $periods = $this->getNoModelPeriodOverview('category', $first, $end); - app('log')->debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d'))); - app('log')->debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d'))); + Log::debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d'))); + Log::debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d'))); /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); @@ -117,13 +116,13 @@ class NoCategoryController extends Controller $periods = new Collection(); $page = (int) $request->get('page'); $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; - app('log')->debug('Start of noCategory()'); + Log::debug('Start of noCategory()'); $subTitle = (string) trans('firefly.all_journals_without_category'); $first = $this->journalRepos->firstNull(); $start = $first instanceof TransactionJournal ? $first->date : new Carbon(); $end = today(config('app.timezone')); - app('log')->debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d'))); - app('log')->debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d'))); + Log::debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d'))); + Log::debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d'))); /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); diff --git a/app/Models/PeriodStatistic.php b/app/Models/PeriodStatistic.php index 9a167fafe0..c8878cfc9b 100644 --- a/app/Models/PeriodStatistic.php +++ b/app/Models/PeriodStatistic.php @@ -8,6 +8,7 @@ use FireflyIII\Casts\SeparateTimezoneCaster; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; class PeriodStatistic extends Model @@ -24,6 +25,11 @@ class PeriodStatistic extends Model ]; } + public function userGroup(): BelongsTo + { + return $this->belongsTo(UserGroup::class); + } + protected function count(): Attribute { return Attribute::make( diff --git a/app/Models/UserGroup.php b/app/Models/UserGroup.php index 0c142b69cb..e0ff63268a 100644 --- a/app/Models/UserGroup.php +++ b/app/Models/UserGroup.php @@ -76,6 +76,14 @@ class UserGroup extends Model return $this->hasMany(Account::class); } + /** + * Link to accounts. + */ + public function periodStatistics(): HasMany + { + return $this->hasMany(PeriodStatistic::class); + } + /** * Link to attachments. */ diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php index cff9567090..e900b72f9e 100644 --- a/app/Providers/FireflyServiceProvider.php +++ b/app/Providers/FireflyServiceProvider.php @@ -163,7 +163,6 @@ class FireflyServiceProvider extends ServiceProvider $this->app->bind(AttachmentHelperInterface::class, AttachmentHelper::class); $this->app->bind(ALERepositoryInterface::class, ALERepository::class); - $this->app->bind(PeriodStatisticRepositoryInterface::class, PeriodStatisticRepository::class); $this->app->bind( static function (Application $app): ObjectGroupRepositoryInterface { @@ -177,6 +176,18 @@ class FireflyServiceProvider extends ServiceProvider } ); + $this->app->bind( + static function (Application $app): PeriodStatisticRepositoryInterface { + /** @var PeriodStatisticRepository $repository */ + $repository = app(PeriodStatisticRepository::class); + if ($app->auth->check()) { // @phpstan-ignore-line (phpstan does not understand the reference to auth) + $repository->setUser(auth()->user()); + } + + return $repository; + } + ); + $this->app->bind( static function (Application $app): WebhookRepositoryInterface { /** @var WebhookRepository $repository */ diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php index 07523c5348..3606ddf0d8 100644 --- a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php @@ -25,37 +25,40 @@ namespace FireflyIII\Repositories\PeriodStatistic; use Carbon\Carbon; use FireflyIII\Models\PeriodStatistic; +use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface; +use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; -class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface +class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface, UserGroupInterface { + use UserGroupTrait; + public function findPeriodStatistics(Model $model, Carbon $start, Carbon $end, array $types): Collection { return $model->primaryPeriodStatistics() - ->where('start', $start) - ->where('end', $end) - ->whereIn('type', $types) - ->get() - ; + ->where('start', $start) + ->where('end', $end) + ->whereIn('type', $types) + ->get(); } public function findPeriodStatistic(Model $model, Carbon $start, Carbon $end, string $type): Collection { return $model->primaryPeriodStatistics() - ->where('start', $start) - ->where('end', $end) - ->where('type', $type) - ->get() - ; + ->where('start', $start) + ->where('end', $end) + ->where('type', $type) + ->get(); } public function saveStatistic(Model $model, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic { - $stat = new PeriodStatistic(); + $stat = new PeriodStatistic(); $stat->primaryStatable()->associate($model); $stat->transaction_currency_id = $currencyId; + $stat->user_group_id = $this->getUserGroup()->id; $stat->start = $start; $stat->start_tz = $start->format('e'); $stat->end = $end; @@ -66,16 +69,16 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface $stat->save(); Log::debug(sprintf( - 'Saved #%d [currency #%d, Model %s #%d, %s to %s, %d, %s] as new statistic.', - $stat->id, - $model::class, - $model->id, - $stat->transaction_currency_id, - $stat->start->toW3cString(), - $stat->end->toW3cString(), - $count, - $amount - )); + 'Saved #%d [currency #%d, Model %s #%d, %s to %s, %d, %s] as new statistic.', + $stat->id, + $model::class, + $model->id, + $stat->transaction_currency_id, + $stat->start->toW3cString(), + $stat->end->toW3cString(), + $count, + $amount + )); return $stat; } @@ -89,4 +92,41 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface { $model->primaryPeriodStatistics()->where('start', '<=', $date)->where('end', '>=', $date)->delete(); } + + #[\Override] + public function allInRangeForPrefix(string $prefix, Carbon $start, Carbon $end): Collection + { + return $this->userGroup->periodStatistics() + ->where('type', 'LIKE', sprintf('%s%%', $prefix)) + ->where('start', '>=', $start)->where('end', '<=', $end)->get(); + } + + #[\Override] + public function savePrefixedStatistic(string $prefix, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic + { + $stat = new PeriodStatistic(); + $stat->transaction_currency_id = $currencyId; + $stat->user_group_id = $this->getUserGroup()->id; + $stat->start = $start; + $stat->start_tz = $start->format('e'); + $stat->end = $end; + $stat->end_tz = $end->format('e'); + $stat->amount = $amount; + $stat->count = $count; + $stat->type = sprintf('%s_%s',$prefix, $type); + $stat->save(); + + Log::debug(sprintf( + 'Saved #%d [currency #%d, type "%s", %s to %s, %d, %s] as new statistic.', + $stat->id, + $stat->transaction_currency_id, + $stat->type, + $stat->start->toW3cString(), + $stat->end->toW3cString(), + $count, + $amount + )); + + return $stat; + } } diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php index 6e4f7cf422..fb464e9794 100644 --- a/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php @@ -35,8 +35,10 @@ interface PeriodStatisticRepositoryInterface public function findPeriodStatistic(Model $model, Carbon $start, Carbon $end, string $type): Collection; public function saveStatistic(Model $model, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic; + public function savePrefixedStatistic(string $prefix, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic; public function allInRangeForModel(Model $model, Carbon $start, Carbon $end): Collection; + public function allInRangeForPrefix(string $prefix, Carbon $start, Carbon $end): Collection; public function deleteStatisticsForModel(Model $model, Carbon $date): void; } diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index e5b020e772..edb51c7ee3 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -169,117 +169,129 @@ trait PeriodOverview * * @throws FireflyException */ - protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array + protected function getNoModelPeriodOverview(string $model, Carbon $start, Carbon $end): array { - $range = Navigation::getViewRange(true); - + Log::debug(sprintf('Now in getNoModelPeriodOverview(%s, %s %s)', $model, $start->format('Y-m-d'), $end->format('Y-m-d'))); + $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - $cache = new CacheProperties(); - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty($this->convertToPrimary); - $cache->addProperty('no-budget-period-entries'); - - if ($cache->has()) { - return $cache->get(); - } - /** @var array $dates */ - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; - - // get all expenses without a budget. - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $journals = $collector->getExtractedJournals(); + $dates = Navigation::blockPeriods($start, $end, $range); + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); + $entries = []; + $this->statistics = $this->periodStatisticRepo->allInRangeForPrefix(sprintf('no_%s', $model), $start, $end); + Log::debug(sprintf('Collected %d stats', $this->statistics->count())); foreach ($dates as $currentDate) { - $set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']); - $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); - $entries[] - = [ - 'title' => $title, - 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), - 'total_transactions' => count($set), - 'spent' => $this->groupByCurrency($set), - 'earned' => [], - 'transferred_away' => [], - 'transferred_in' => [], - ]; + $entries[] = $this->getSingleNoModelPeriodOverview($model, $currentDate['start'], $currentDate['end'], $currentDate['period']); } - $cache->store($entries); return $entries; } - /** - * TODO fix the date. - * - * Show period overview for no category view. - * - * @throws FireflyException - */ - protected function getNoCategoryPeriodOverview(Carbon $theDate): array + private function getSingleNoModelPeriodOverview(string $model, Carbon $start, Carbon $end, string $period): array { - Log::debug(sprintf('Now in getNoCategoryPeriodOverview(%s)', $theDate->format('Y-m-d'))); - $range = Navigation::getViewRange(true); - $first = $this->journalRepos->firstNull(); - $start = null === $first ? new Carbon() : $first->date; - $end = clone $theDate; - $end = Navigation::endOfPeriod($end, $range); + Log::debug(sprintf('getSingleNoModelPeriodOverview(%s, %s, %s, %s)', $model, $start->format('Y-m-d'), $end->format('Y-m-d'), $period)); + $statistics = $this->filterPrefixedStatistics($start, $end, sprintf('no_%s', $model)); + $title = Navigation::periodShow($end, $period); - Log::debug(sprintf('Start for getNoCategoryPeriodOverview() is %s', $start->format('Y-m-d'))); - Log::debug(sprintf('End for getNoCategoryPeriodOverview() is %s', $end->format('Y-m-d'))); + if (0 === $statistics->count()) { + Log::debug(sprintf('Found no statistics in period %s - %s, regenerating them.', $start->format('Y-m-d'), $end->format('Y-m-d'))); + switch ($model) { + default: + throw new FireflyException(sprintf('Cannot deal with model of type "%s"', $model)); + case 'budget': + // get all expenses without a budget. + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); + $spent = $collector->getExtractedJournals(); + $earned = []; + $transferred = []; + break; + case 'category': + // collect all expenses in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->withoutCategory(); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); + $earned = $collector->getExtractedJournals(); - // properties for cache - $dates = Navigation::blockPeriods($start, $end, $range); - $entries = []; + // collect all income in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->withoutCategory(); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); + $spent = $collector->getExtractedJournals(); - // collect all expenses in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->withoutCategory(); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]); - $earnedSet = $collector->getExtractedJournals(); - - // collect all income in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->withoutCategory(); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]); - $spentSet = $collector->getExtractedJournals(); - - // collect all transfers in this period: - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->withoutCategory(); - $collector->setRange($start, $end); - $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); - $transferSet = $collector->getExtractedJournals(); - - /** @var array $currentDate */ - foreach ($dates as $currentDate) { - $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']); - $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']); - $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']); - $title = Navigation::periodShow($currentDate['end'], $currentDate['period']); - $entries[] - = [ + // collect all transfers in this period: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->withoutCategory(); + $collector->setRange($start, $end); + $collector->setTypes([TransactionTypeEnum::TRANSFER->value]); + $transferred = $collector->getExtractedJournals(); + break; + } + $groupedSpent = $this->groupByCurrency($spent); + $groupedEarned = $this->groupByCurrency($earned); + $groupedTransferred = $this->groupByCurrency($transferred); + $entry + = [ 'title' => $title, - 'route' => route('categories.no-category', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), - 'total_transactions' => count($spent) + count($earned) + count($transferred), - 'spent' => $this->groupByCurrency($spent), - 'earned' => $this->groupByCurrency($earned), - 'transferred' => $this->groupByCurrency($transferred), + 'route' => route(sprintf('%s.no-%s', Str::plural($model), $model), [$start->format('Y-m-d'), $end->format('Y-m-d')]), + 'total_transactions' => count($spent), + 'spent' => $groupedSpent, + 'earned' => $groupedEarned, + 'transferred' => $groupedTransferred, ]; + $this->saveGroupedForPrefix(sprintf('no_%s', $model), $start, $end, 'spent', $groupedSpent); + $this->saveGroupedForPrefix(sprintf('no_%s', $model), $start, $end, 'earned', $groupedEarned); + $this->saveGroupedForPrefix(sprintf('no_%s', $model), $start, $end, 'transferred', $groupedTransferred); + return $entry; } - Log::debug('End of loops'); + Log::debug(sprintf('Found %d statistics in period %s - %s.', count($statistics), $start->format('Y-m-d'), $end->format('Y-m-d'))); - return $entries; + $entry + = [ + 'title' => $title, + 'route' => route(sprintf('%s.no-%s', Str::plural($model), $model), [$start->format('Y-m-d'), $end->format('Y-m-d')]), + 'total_transactions' => 0, + 'spent' => [], + 'earned' => [], + 'transferred' => [], + ]; + $grouped = []; + /** @var PeriodStatistic $statistic */ + foreach ($statistics as $statistic) { + $type = str_replace(sprintf('no_%s_', $model), '', $statistic->type); + $id = (int)$statistic->transaction_currency_id; + $currency = Amount::getTransactionCurrencyById($id); + $grouped[$type]['count'] ??= 0; + $grouped[$type][$id] = [ + 'amount' => (string)$statistic->amount, + 'count' => (int)$statistic->count, + 'currency_id' => $currency->id, + 'currency_name' => $currency->name, + 'currency_code' => $currency->code, + 'currency_symbol' => $currency->symbol, + 'currency_decimal_places' => $currency->decimal_places, + ]; + $grouped[$type]['count'] += (int)$statistic->count; + } + $types = ['spent', 'earned', 'transferred']; + foreach ($types as $type) { + if (array_key_exists($type, $grouped)) { + $entry['total_transactions'] += $grouped[$type]['count']; + unset($grouped[$type]['count']); + $entry[$type] = $grouped[$type]; + } + + } + return $entry; } protected function getSingleModelPeriod(Model $model, string $period, Carbon $start, Carbon $end): array @@ -303,30 +315,34 @@ trait PeriodOverview } - protected function filterStatistics(Carbon $start, Carbon $end, string $type): Collection + private function filterStatistics(Carbon $start, Carbon $end, string $type): Collection { + if (0 === $this->statistics->count()) { + Log::warning('Have no statistic to filter!'); + return new Collection; + } return $this->statistics->filter( function (PeriodStatistic $statistic) use ($start, $end, $type) { - if ( - !$statistic->end->equalTo($end) - && $statistic->end->format('Y-m-d H:i:s') === $end->format('Y-m-d H:i:s') - ) { - echo sprintf('End: "%s" vs "%s": %s', $statistic->end->toW3cString(), $end->toW3cString(), var_export($statistic->end->eq($end), true)); - var_dump($statistic->end); - var_dump($end); - - exit; - } - - return $statistic->start->eq($start) && $statistic->end->eq($end) && $statistic->type === $type; } ); } + private function filterPrefixedStatistics(Carbon $start, Carbon $end, string $prefix): Collection + { + if (0 === $this->statistics->count()) { + Log::warning('Have no statistic to filter!'); + return new Collection; + } + return $this->statistics->filter( + function (PeriodStatistic $statistic) use ($start, $end, $prefix) { + return $statistic->start->eq($start) && $statistic->end->eq($end) && str_starts_with($statistic->type, $prefix); + } + ); + } - protected function getSingleModelPeriodByType(Model $model, Carbon $start, Carbon $end, string $type): array + private function getSingleModelPeriodByType(Model $model, Carbon $start, Carbon $end, string $type): array { Log::debug(sprintf('Now in getSingleModelPeriodByType(%s #%d, %s %s, %s)', $model::class, $model->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type)); $statistics = $this->filterStatistics($start, $end, $type); @@ -497,7 +513,7 @@ trait PeriodOverview return $entries; } - protected function saveGroupedAsStatistics(Model $model, Carbon $start, Carbon $end, string $type, array $array): void + private function saveGroupedAsStatistics(Model $model, Carbon $start, Carbon $end, string $type, array $array): void { unset($array['count']); Log::debug(sprintf('saveGroupedAsStatistics(%s #%d, %s, %s, "%s", array(%d))', $model::class, $model->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type, count($array))); @@ -510,6 +526,19 @@ trait PeriodOverview } } + private function saveGroupedForPrefix(string $prefix, Carbon $start, Carbon $end, string $type, array $array): void + { + unset($array['count']); + Log::debug(sprintf('saveGroupedForPrefix("%s", %s, %s, "%s", array(%d))', $prefix, $start->format('Y-m-d'), $end->format('Y-m-d'), $type, count($array))); + foreach ($array as $entry) { + $this->periodStatisticRepo->savePrefixedStatistic($prefix, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']); + } + if (0 === count($array)) { + Log::debug('Save empty statistic.'); + $this->periodStatisticRepo->savePrefixedStatistic($prefix, $this->primaryCurrency->id, $start, $end, $type, 0, '0'); + } + } + /** * Filter a list of journals by a set of dates, and then group them by currency. */ @@ -584,6 +613,9 @@ trait PeriodOverview $return = [ 'count' => 0, ]; + if (0 === count($journals)) { + return $return; + } /** @var array $journal */ foreach ($journals as $journal) { diff --git a/database/migrations/2025_09_25_175248_create_period_statistics.php b/database/migrations/2025_09_25_175248_create_period_statistics.php index 0a5bf8d86b..0cda62b0ef 100644 --- a/database/migrations/2025_09_25_175248_create_period_statistics.php +++ b/database/migrations/2025_09_25_175248_create_period_statistics.php @@ -14,6 +14,10 @@ return new class extends Migration Schema::create('period_statistics', function (Blueprint $table) { $table->id(); $table->timestamps(); + + // reference to user group id. + $table->bigInteger('user_group_id', false, true); + $table->integer('primary_statable_id', false, true)->nullable(); $table->string('primary_statable_type', 255)->nullable(); @@ -33,6 +37,7 @@ return new class extends Migration $table->string('type',255); $table->integer('count', false, true)->default(0); $table->decimal('amount', 32, 12); + $table->foreign('user_group_id')->references('id')->on('user_groups')->onDelete('cascade'); }); }