diff --git a/backend/exence/src/main/java/com/exence/finance/modules/category/repository/CategoryRepository.java b/backend/exence/src/main/java/com/exence/finance/modules/category/repository/CategoryRepository.java index 45a3a549..cf163a05 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/category/repository/CategoryRepository.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/category/repository/CategoryRepository.java @@ -4,6 +4,7 @@ import com.exence.finance.modules.category.dto.CategoryType; import com.exence.finance.modules.category.dto.projection.CategoryBalanceSums; import com.exence.finance.modules.category.entity.Category; +import com.exence.finance.modules.transaction.dto.TransactionType; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -44,15 +45,13 @@ public interface CategoryRepository extends JpaRepository { c.id, c.name, CAST(c.icon AS string), c.color, COALESCE(SUM(t.baseCurrencyAmount), 0) ) FROM Category c - left JOIN c.transactions t - WHERE c.type = :type - AND ( - (:type = 'INCOME' AND t.type = 'INCOME') OR - (:type IN ('EXPENSE', 'MIXED') AND t.type = 'EXPENSE') - ) + JOIN c.transactions t + WHERE (c.type = :type OR CAST(c.type AS string) = 'MIXED') + AND t.type = :transactionType GROUP BY c.id, c.name, c.icon, c.color ORDER BY COALESCE(SUM(t.baseCurrencyAmount), 0) DESC LIMIT 4 """) - List findTopCategoriesByTotalAmount(@Param("type") CategoryType type); + List findTopCategoriesByTotalAmount( + @Param("type") CategoryType type, @Param("transactionType") TransactionType transactionType); } diff --git a/backend/exence/src/main/java/com/exence/finance/modules/category/service/impl/CategoryServiceImpl.java b/backend/exence/src/main/java/com/exence/finance/modules/category/service/impl/CategoryServiceImpl.java index ccac07cb..84b3e824 100644 --- a/backend/exence/src/main/java/com/exence/finance/modules/category/service/impl/CategoryServiceImpl.java +++ b/backend/exence/src/main/java/com/exence/finance/modules/category/service/impl/CategoryServiceImpl.java @@ -17,6 +17,7 @@ import com.exence.finance.modules.category.repository.CategoryRepository; import com.exence.finance.modules.category.service.CategoryService; import com.exence.finance.modules.statistics.event.MaterializedViewRefreshEvent; +import com.exence.finance.modules.transaction.dto.TransactionType; import java.math.BigDecimal; import java.util.List; import java.util.Map; @@ -68,7 +69,13 @@ private BigDecimal calculateBalance(Category category, Map getTopCategoriesByTotalAmount(CategoryFilter filter) { - return categoryRepository.findTopCategoriesByTotalAmount(filter.type()); + TransactionType transactionType = + switch (filter.type()) { + case INCOME -> TransactionType.INCOME; + case EXPENSE -> TransactionType.EXPENSE; + case MIXED -> throw new ExenceException(ErrorCode.ILLEGAL_ARGUMENT); + }; + return categoryRepository.findTopCategoriesByTotalAmount(filter.type(), transactionType); } @WriteTransactional diff --git a/frontend/Exence/src/app/private/dashboard/categories/categories.component.html b/frontend/Exence/src/app/private/dashboard/categories/categories.component.html index a34025e2..6fa81458 100644 --- a/frontend/Exence/src/app/private/dashboard/categories/categories.component.html +++ b/frontend/Exence/src/app/private/dashboard/categories/categories.component.html @@ -95,7 +95,7 @@

{{ 'categories.noCategories' | translate }}

- @for (type of categoryTypes | enumValue; track type) { + @for (type of listTypes; track type) { {{ codeForCategoryType(type) | translate | uppercase }} diff --git a/frontend/Exence/src/app/private/dashboard/categories/categories.component.ts b/frontend/Exence/src/app/private/dashboard/categories/categories.component.ts index 878b1213..a41a8f17 100644 --- a/frontend/Exence/src/app/private/dashboard/categories/categories.component.ts +++ b/frontend/Exence/src/app/private/dashboard/categories/categories.component.ts @@ -5,15 +5,16 @@ import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { CategoryGet } from '../../../data-model/modules/category/CategoryGet'; import { CategorySummaryResponse } from '../../../data-model/modules/category/CategorySummaryResponse'; import { CategoryType } from '../../../data-model/modules/category/CategoryType'; +import { TransactionCreate } from '../../../data-model/modules/transaction/TransactionCreate'; import { AnimatedSkeletonLoaderComponent } from '../../../shared/animated-skeleton-loader/animated-skeleton-loader.component'; import { BaseComponent } from '../../../shared/base-component/base.component'; import { ButtonComponent } from '../../../shared/button/button.component'; import { DialogService } from '../../../shared/dialog/dialog.service'; import { DisplaySizeService } from '../../../shared/display-size.service'; import { TranslationCode } from '../../../shared/i18n/translation-types'; -import { EnumValuePipe } from '../../../shared/pipes/enum-value.pipe'; import { TranslatePipe } from '../../../shared/pipes/translate.pipe'; import { CategoryStore } from '../../transactions-and-categories/category.store'; import { CreateCategoryDialogComponent } from '../../transactions-and-categories/create-category-dialog/create-category-dialog.component'; @@ -22,8 +23,6 @@ import { CreateTransactionDialogData, } from '../../transactions-and-categories/create-transaction-dialog/create-transaction-dialog.component'; import { TransactionStore } from '../../transactions-and-categories/transaction.store'; -import { CategoryGet } from '../../../data-model/modules/category/CategoryGet'; -import { TransactionCreate } from '../../../data-model/modules/transaction/TransactionCreate'; @Component({ selector: 'ex-categories', @@ -36,7 +35,6 @@ import { TransactionCreate } from '../../../data-model/modules/transaction/Trans MatButtonToggleModule, ReactiveFormsModule, ButtonComponent, - EnumValuePipe, TranslatePipe, UpperCasePipe, AnimatedSkeletonLoaderComponent, @@ -60,6 +58,7 @@ export class CategoriesComponent extends BaseComponent { hasTransactions = computed(() => !!this.topCategories().length); categoryTypes = CategoryType; + listTypes = Object.values(CategoryType).filter(t => t !== CategoryType.MIXED); async openCreateCategoryDialog(): Promise { const result = await this.dialog.openNonModal(CreateCategoryDialogComponent, undefined); @@ -78,12 +77,12 @@ export class CategoriesComponent extends BaseComponent { calcPercentage(amount: number): number { if (this.categoryType() === CategoryType.INCOME) { - return Math.floor((amount / this.totalIncome()) * 100); + return Math.round((amount / this.totalIncome()) * 10000) / 100; } - return Math.floor((amount / this.totalExpense()) * 100); + return Math.round((amount / this.totalExpense()) * 10000) / 100; } - onTypeChanged(type: CategoryType): void { + onTypeChanged(type: Exclude): void { this.categoryType.set(type); this.categoryStore.toggleTopCategoriesType(type); } diff --git a/frontend/Exence/src/app/private/transactions-and-categories/category.service.ts b/frontend/Exence/src/app/private/transactions-and-categories/category.service.ts index 47845a25..be04d998 100644 --- a/frontend/Exence/src/app/private/transactions-and-categories/category.service.ts +++ b/frontend/Exence/src/app/private/transactions-and-categories/category.service.ts @@ -31,16 +31,14 @@ export class CategoryService { ); } - public async listTopAll(): Promise> { - const [expense, income, mixed] = await Promise.all([ + public async listTopAll(): Promise, CategorySummaryResponse[]>> { + const [expense, income] = await Promise.all([ this.listTop({ type: CategoryType.EXPENSE }), this.listTop({ type: CategoryType.INCOME }), - this.listTop({ type: CategoryType.MIXED }), ]); return { [CategoryType.EXPENSE]: expense, [CategoryType.INCOME]: income, - [CategoryType.MIXED]: mixed, }; } diff --git a/frontend/Exence/src/app/private/transactions-and-categories/category.store.ts b/frontend/Exence/src/app/private/transactions-and-categories/category.store.ts index df27e578..a57fd6da 100644 --- a/frontend/Exence/src/app/private/transactions-and-categories/category.store.ts +++ b/frontend/Exence/src/app/private/transactions-and-categories/category.store.ts @@ -9,7 +9,7 @@ import { CategoryCreate } from '../../data-model/modules/category/CategoryCreate import { CategoryGet } from '../../data-model/modules/category/CategoryGet'; interface CategoryStoreData { - selectedTopCategoriesType: CategoryType; + selectedTopCategoriesType: Exclude; } const initialState: CategoryStoreData = { @@ -24,7 +24,10 @@ export const CategoryStore = signalStore( categoryResource: resource({ loader: async () => await categoryService.list(), }), - topCategoriesAllResource: resource, undefined>({ + topCategoriesAllResource: resource< + Record, CategorySummaryResponse[]>, + undefined + >({ loader: async () => await categoryService.listTopAll(), }), }; @@ -62,7 +65,7 @@ export const CategoryStore = signalStore( triggerReload(); }, - toggleTopCategoriesType(type?: CategoryType): void { + toggleTopCategoriesType(type?: Exclude): void { patchState(store, { selectedTopCategoriesType: type ?? CategoryType.EXPENSE, }); diff --git a/frontend/Exence/src/app/private/transactions-and-categories/create-transaction-dialog/create-transaction-dialog.component.ts b/frontend/Exence/src/app/private/transactions-and-categories/create-transaction-dialog/create-transaction-dialog.component.ts index 241b3663..d47f780b 100644 --- a/frontend/Exence/src/app/private/transactions-and-categories/create-transaction-dialog/create-transaction-dialog.component.ts +++ b/frontend/Exence/src/app/private/transactions-and-categories/create-transaction-dialog/create-transaction-dialog.component.ts @@ -110,13 +110,13 @@ export class CreateTransactionDialogComponent extends DialogWithBaseComponent< form = this.fb.group({ title: this.fb.control('', [Validators.required, Validators.maxLength(255)]), note: this.fb.control(undefined, [Validators.maxLength(500)]), - date: this.fb.control(new Date(), [Validators.required]), // disable when recurring.isRecurring true + date: this.fb.control(new Date(), [Validators.required]), amount: this.fb.control(null, [Validators.required, Validators.min(1)]), type: this.fb.control(this.data?.type ?? TransactionType.EXPENSE, [ Validators.required, ]), currency: this.fb.control(this.currencyService.baseCurrency(), [Validators.required]), - exchangeRate: this.fb.control(null, [Validators.required, Validators.min(0.01)]), // disable when recurring.isRecurring true + exchangeRate: this.fb.control(null, [Validators.required, Validators.min(0.0000000001)]), category: this.fb.group({ category: this.fb.control(null, [Validators.required]), searchText: this.fb.control('', [Validators.maxLength(25)]), @@ -294,7 +294,7 @@ export class CreateTransactionDialogComponent extends DialogWithBaseComponent< request = { title: formValue.title, note: formValue.note ?? '', - date: formValue.date.toISOString(), + date: format(formValue.date, 'yyyy-MM-dd'), amount: formValue.amount!, type: formValue.type!, categoryId: formValue.category.category!.id!, diff --git a/frontend/Exence/src/app/private/transactions-and-categories/edit-transaction-dialog/edit-transaction-dialog.component.ts b/frontend/Exence/src/app/private/transactions-and-categories/edit-transaction-dialog/edit-transaction-dialog.component.ts index 03ed97fb..871f56b6 100644 --- a/frontend/Exence/src/app/private/transactions-and-categories/edit-transaction-dialog/edit-transaction-dialog.component.ts +++ b/frontend/Exence/src/app/private/transactions-and-categories/edit-transaction-dialog/edit-transaction-dialog.component.ts @@ -171,7 +171,6 @@ export class EditTransactionDialogComponent extends DialogComponent< date: format(formValue.date, 'yyyy-MM-dd'), amount: formValue.amount!, type: formValue.type, - // recurring: formValue.recurring, categoryId: formValue.category.category!.id, currency: formValue.currency, exchangeRate: formValue.exchangeRate!,