Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -44,15 +45,13 @@ public interface CategoryRepository extends JpaRepository<Category, Long> {
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<CategorySummaryResponse> findTopCategoriesByTotalAmount(@Param("type") CategoryType type);
List<CategorySummaryResponse> findTopCategoriesByTotalAmount(
@Param("type") CategoryType type, @Param("transactionType") TransactionType transactionType);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -68,7 +69,13 @@ private BigDecimal calculateBalance(Category category, Map<Long, CategoryBalance

@ReadTransactional
public List<CategorySummaryResponse> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ <h2 class="header-2 text-center">{{ 'categories.noCategories' | translate }}</h2
[hideSingleSelectionIndicator]="true"
(change)="onTypeChanged($event.value)"
>
@for (type of categoryTypes | enumValue; track type) {
@for (type of listTypes; track type) {
<mat-button-toggle [value]="type" class="type-{{ type.toLowerCase() }}">
{{ codeForCategoryType(type) | translate | uppercase }}
</mat-button-toggle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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',
Expand All @@ -36,7 +35,6 @@ import { TransactionCreate } from '../../../data-model/modules/transaction/Trans
MatButtonToggleModule,
ReactiveFormsModule,
ButtonComponent,
EnumValuePipe,
TranslatePipe,
UpperCasePipe,
AnimatedSkeletonLoaderComponent,
Expand All @@ -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<void> {
const result = await this.dialog.openNonModal(CreateCategoryDialogComponent, undefined);
Expand All @@ -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<CategoryType, CategoryType.MIXED>): void {
this.categoryType.set(type);
this.categoryStore.toggleTopCategoriesType(type);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,14 @@ export class CategoryService {
);
}

public async listTopAll(): Promise<Record<CategoryType, CategorySummaryResponse[]>> {
const [expense, income, mixed] = await Promise.all([
public async listTopAll(): Promise<Record<Exclude<CategoryType, CategoryType.MIXED>, 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,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CategoryType, CategoryType.MIXED>;
}

const initialState: CategoryStoreData = {
Expand All @@ -24,7 +24,10 @@ export const CategoryStore = signalStore(
categoryResource: resource<CategoryGet[], undefined>({
loader: async () => await categoryService.list(),
}),
topCategoriesAllResource: resource<Record<CategoryType, CategorySummaryResponse[]>, undefined>({
topCategoriesAllResource: resource<
Record<Exclude<CategoryType, CategoryType.MIXED>, CategorySummaryResponse[]>,
undefined
>({
loader: async () => await categoryService.listTopAll(),
}),
};
Expand Down Expand Up @@ -62,7 +65,7 @@ export const CategoryStore = signalStore(
triggerReload();
},

toggleTopCategoriesType(type?: CategoryType): void {
toggleTopCategoriesType(type?: Exclude<CategoryType, CategoryType.MIXED>): void {
patchState(store, {
selectedTopCategoriesType: type ?? CategoryType.EXPENSE,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,13 @@ export class CreateTransactionDialogComponent extends DialogWithBaseComponent<
form = this.fb.group({
title: this.fb.control<string>('', [Validators.required, Validators.maxLength(255)]),
note: this.fb.control<string | undefined>(undefined, [Validators.maxLength(500)]),
date: this.fb.control<Date>(new Date(), [Validators.required]), // disable when recurring.isRecurring true
date: this.fb.control<Date>(new Date(), [Validators.required]),
amount: this.fb.control<number | null>(null, [Validators.required, Validators.min(1)]),
type: this.fb.control<TransactionType | null>(this.data?.type ?? TransactionType.EXPENSE, [
Validators.required,
]),
currency: this.fb.control<SupportedCurrency>(this.currencyService.baseCurrency(), [Validators.required]),
exchangeRate: this.fb.control<number | null>(null, [Validators.required, Validators.min(0.01)]), // disable when recurring.isRecurring true
exchangeRate: this.fb.control<number | null>(null, [Validators.required, Validators.min(0.0000000001)]),
category: this.fb.group({
category: this.fb.control<CategoryGet | null>(null, [Validators.required]),
searchText: this.fb.control<string>('', [Validators.maxLength(25)]),
Expand Down Expand Up @@ -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!,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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!,
Expand Down
Loading