Skip to content
Open
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
14 changes: 14 additions & 0 deletions frontend/Exence/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/Exence/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"jwt-decode": "^4.0.0",
"ng2-charts": "^8.0.0",
"ngx-cookie-service": "^21.1.0",
"ngx-infinite-scroll": "^21.0.0",
"or": "^0.2.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
title="Expenses"
[type]="transactionTypes.EXPENSE"
(dataChangedEvent)="onDataChanged()"
(onScroll)="onScroll($event, transactionTypes.EXPENSE)"
/>
<ex-data-table
class="col-12 col-lg-6"
Expand All @@ -96,6 +97,7 @@
title="Incomes"
[type]="transactionTypes.INCOME"
(dataChangedEvent)="onDataChanged()"
(onScroll)="onScroll($event, transactionTypes.INCOME)"
/>
</div>
} @else {
Expand All @@ -106,6 +108,7 @@
[data]="{ transactions, categories }"
title="Transactions"
(dataChangedEvent)="onDataChanged()"
(onScroll)="onScroll($event)"
/>
<ng-container [ngTemplateOutlet]="mobileButtons" />
</div>
Expand Down
37 changes: 30 additions & 7 deletions frontend/Exence/src/app/private/dashboard/dashboard.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export class DashboardComponent implements OnInit {
readonly display = inject(DisplaySizeService);
readonly navigation = inject(NavigationService);


transactionTypes = TransactionType;
dateIntervals = DateInterval;

Expand All @@ -65,23 +64,25 @@ export class DashboardComponent implements OnInit {
topCategories?: CategorySummaryResponse[];
topCategory?: CategorySummaryResponse;

loading = false;

async ngOnInit(): Promise<void> {
await this.initialize();
}

async initialize(): Promise<void> {
return Promise.all([
this.transactionService.list(),
this.getTransactions(),
this.getTransactions(0, TransactionType.INCOME),
this.getTransactions(0, TransactionType.EXPENSE),
this.categoryService.list(),
this.categoryService.listTop4(),
this.transactionService.incomes(),
this.transactionService.expenses(),
this.transactionService.totals(),
]).then(([transactions, categories, top4, incomes, expenses, totals]) => {
]).then(([transactions, incomes, expenses, categories, top4, totals]) => {
this.transactions = transactions;
this.categories = categories;
this.incomes = incomes;
this.expenses = expenses;
this.categories = categories;
this.totals = totals;
this.topCategories = top4;

Expand All @@ -90,7 +91,7 @@ export class DashboardComponent implements OnInit {
});
}

public async openCreateTransactionDialog(transactionType: TransactionType): Promise<void> {
async openCreateTransactionDialog(transactionType: TransactionType): Promise<void> {
const result = await this.dialog.openNonModal(
CreateTransactionDialogComponent, { type: transactionType }
);
Expand All @@ -102,4 +103,26 @@ export class DashboardComponent implements OnInit {
async onDataChanged(): Promise<void> {
await this.initialize();
}

async getTransactions(pageIndex = 0, type?: TransactionType): Promise<PagedResponse<Transaction>> {
switch (type) {
case TransactionType.INCOME:
return this.transactionService.listIncomes(pageIndex);
case TransactionType.EXPENSE:
return this.transactionService.listExpenses(pageIndex);
default:
return this.transactionService.list(pageIndex);
}
}

async onScroll(pageIndex: number, type?: TransactionType): Promise<void> {
if (!this.loading && !this.transactions.last) {
this.loading = true;
try {
this.transactions = await this.getTransactions(pageIndex, type);
} finally {
this.loading = false;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { inject, Injectable } from '@angular/core';
import { lastValueFrom } from 'rxjs';
import { lastValueFrom, tap } from 'rxjs';
import { PagedResponse } from '../../data-model/modules/common/PagedResponse';
import { RecurringTransactionsResponse } from '../../data-model/modules/transaction/RecurringTransactionsResponse';
import { Transaction } from '../../data-model/modules/transaction/Transaction';
import { TransactionTotalsResponse } from '../../data-model/modules/transaction/TransactionTotalsResponse';
import { HttpService } from '../../shared/http/http.service';
import { TransactionType } from '../../data-model/modules/transaction/TransactionType';

@Injectable({
providedIn: 'root',
Expand All @@ -13,40 +14,121 @@ export class TransactionService {
private readonly http = inject(HttpService);

private baseUrl = '/api/transactions';

private transactions?: PagedResponse<Transaction>;
private transactionsOutOfSync = false;

private incomes?: PagedResponse<Transaction>;
private incomesOutOfSync = false;

private expenses?: PagedResponse<Transaction>;
private expensesOutOfSync = false;

public get(id: number): Promise<Transaction> {
return lastValueFrom(this.http.get<Transaction>(`${this.baseUrl}/${id}`));
}

public list(): Promise<PagedResponse<Transaction>> {
return lastValueFrom(this.http.get<PagedResponse<Transaction>>(this.baseUrl));
public async list(pageIndex?: number): Promise<PagedResponse<Transaction>> {
if (this.transactions && !this.transactionsOutOfSync && pageIndex === this.transactions.page) {
return Promise.resolve(this.transactions);
}
await lastValueFrom(this.http.get<PagedResponse<Transaction>>(this.baseUrl, { page: (pageIndex ?? 0).toString() })
.pipe(tap(transactions => {
this.transactions = this.getDataToCache(this.transactions, transactions, this.transactionsOutOfSync, pageIndex);
this.transactionsOutOfSync = false;
})));
return this.transactions!;
}

public listRecurrings(): Promise<RecurringTransactionsResponse> {
return lastValueFrom(this.http.get<RecurringTransactionsResponse>(`${this.baseUrl}/recurring`));
}

public incomes(): Promise<PagedResponse<Transaction>> {
return lastValueFrom(this.http.get<PagedResponse<Transaction>>(`${this.baseUrl}/income`));
public async listIncomes(pageIndex?: number): Promise<PagedResponse<Transaction>> {
if (this.incomes && !this.incomesOutOfSync && pageIndex === this.incomes.page) {
return Promise.resolve(this.incomes);
}
await lastValueFrom(this.http.get<PagedResponse<Transaction>>(`${this.baseUrl}/income`, { page: (pageIndex ?? 0).toString() })
.pipe(tap(incomes => {
this.incomes = this.getDataToCache(this.incomes, incomes, this.incomesOutOfSync, pageIndex);
this.incomesOutOfSync = false;
}))
);
return this.incomes!;
}

public expenses(): Promise<PagedResponse<Transaction>> {
return lastValueFrom(this.http.get<PagedResponse<Transaction>>(`${this.baseUrl}/expense`));
public async listExpenses(pageIndex?: number): Promise<PagedResponse<Transaction>> {
if (this.expenses && !this.expensesOutOfSync && pageIndex === this.expenses.page) {
return Promise.resolve(this.expenses);
}
await lastValueFrom(this.http.get<PagedResponse<Transaction>>(`${this.baseUrl}/expense`, { page: (pageIndex ?? 0).toString() })
.pipe(tap(expenses => {
this.expenses = this.getDataToCache(this.expenses, expenses, this.expensesOutOfSync, pageIndex);
this.expensesOutOfSync = false;
}))
);
return this.expenses!;
}

public totals(): Promise<TransactionTotalsResponse> {
return lastValueFrom(this.http.get<TransactionTotalsResponse>(`${this.baseUrl}/totals`));
}

public create(request: Transaction): Promise<Transaction> {
return lastValueFrom(this.http.post<Transaction>(this.baseUrl, request));
return lastValueFrom(this.http.post<Transaction>(this.baseUrl, request).pipe(
tap(() => {
if (request.type === TransactionType.INCOME) {
this.incomesOutOfSync = true;
} else {
this.expensesOutOfSync = true;
}
this.transactionsOutOfSync = true;
})
));
}

public update(request: Transaction): Promise<Transaction> {
return lastValueFrom(this.http.put<Transaction>(`${this.baseUrl}/${request.id}`, request));
return lastValueFrom(this.http.put<Transaction>(`${this.baseUrl}/${request.id}`, request).pipe(
tap(() => {
if (request.type === TransactionType.INCOME) {
this.incomesOutOfSync = true;
} else {
this.expensesOutOfSync = true;
}
this.transactionsOutOfSync = true;
})
));
}

// somehow detect what type was removed
public delete(id: number, type?: TransactionType): Promise<void> {
return lastValueFrom(this.http.delete(`${this.baseUrl}/${id}`).pipe(
tap(() => {
if (type === TransactionType.INCOME) {
this.incomesOutOfSync = true;
} else if (type === TransactionType.EXPENSE) {
this.expensesOutOfSync = true;
}
this.transactionsOutOfSync = true;
})
));
}

public delete(id: number): Promise<void> {
return lastValueFrom(this.http.delete(`${this.baseUrl}/${id}`));
private getDataToCache(oldCached: PagedResponse<Transaction> | undefined, dataToAddToCache: PagedResponse<Transaction>, isOutOfSync: boolean, pageIndex?: number): PagedResponse<Transaction> {
// either content, or pageIndex changed
if (oldCached && !isOutOfSync && pageIndex !== oldCached.page) {
const clone: PagedResponse<Transaction> = { ...oldCached };
const content = [...clone.content.sort((a, b) => b.date.localeCompare(a.date)), ...dataToAddToCache.content];
const numberOfElements = clone.numberOfElements + dataToAddToCache.numberOfElements;

const result = {
...dataToAddToCache,
content,
numberOfElements
};
return result;
} else {
return dataToAddToCache;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@
matIcon="money_off"
class="flex-grow-1"
nonExpandable
paginationDisabled
title="Recurring Incomes"
[type]="transactionTypes.INCOME"
[isRecurring]="true"
Expand All @@ -84,7 +83,6 @@
matIcon="money_off"
class="flex-grow-1"
nonExpandable
paginationDisabled
title="Recurring Expenses"
[type]="transactionTypes.EXPENSE"
[isRecurring]="true"
Expand All @@ -99,7 +97,6 @@
matIcon="money_off"
class="flex-grow-1"
nonExpandable
paginationDisabled
title="Recurrings"
[isRecurring]="true"
(dataChangedEvent)="onDataChanged()"
Expand Down Expand Up @@ -135,6 +132,7 @@
[class.overflow-hidden]="display.isMd()"
title="Transactions"
(dataChangedEvent)="onDataChanged()"
(onScroll)="onScroll($event)"
/>
</div>
</div>
Expand Down Expand Up @@ -164,7 +162,6 @@
title="Categories"
type="category"
(dataChangedEvent)="onDataChanged()"
paginationDisabled
/>
</div>
</ng-template>
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export class TransactionsAndCategoriesComponent implements OnInit {
categories: Category[] = [];

selectedIndex = 0;
loading = false;

transactionTypes = TransactionType;

Expand All @@ -66,19 +67,34 @@ export class TransactionsAndCategoriesComponent implements OnInit {
});
}

public async openCreateTransactionDialog(): Promise<void> {
async openCreateTransactionDialog(): Promise<void> {
const result = await this.dialog.openNonModal(CreateTransactionDialogComponent, undefined);
if (!result) return;
await this.initialize(); // to trigger data refresh in all tables (e.g. if a recurring transaction was created)
}

public async openCreateCategoryDialog(): Promise<void> {
async openCreateCategoryDialog(): Promise<void> {
const result = await this.dialog.openNonModal(CreateCategoryDialogComponent, undefined);
if (!result) return;
this.categories = (await this.categoryService.list());
}

async getTransactions(pageIndex: number): Promise<PagedResponse<Transaction>> {
return this.transactionService.list(pageIndex);
}

async onDataChanged(): Promise<void> {
await this.initialize();
}

async onScroll(pageIndex: number): Promise<void> {
if (!this.loading && !this.transactions.last) {
this.loading = true;
try {
this.transactions = await this.getTransactions(pageIndex);
} finally {
this.loading = false;
}
}
}
}
Loading
Loading