{{ date }}
@@ -60,7 +60,7 @@
aria-label="Cancel statement import"
nbTooltip="Cancel statement import"
(click)="cancelImport()">
-
+
- @if (statementImportService.expenses.length) {
+ @if (hasPendingImport) {
}
@@ -38,10 +38,13 @@ import { StatementImportService } from '../../services/statement-import.service'
imports: [NbButtonModule, NbIconModule, NbBadgeModule],
})
export class HeaderSidebarToggleComponent {
- protected readonly statementImportService = inject(StatementImportService);
-
+ private readonly statementImportService = inject(StatementImportService);
private readonly sidebarService = inject(NbSidebarService);
+ protected get hasPendingImport(): boolean {
+ return this.statementImportService.draft().length > 0;
+ }
+
public toggleSidebar(): void {
this.sidebarService.toggle(false, SIDEBAR_TAG);
}
diff --git a/frontend/src/app/modules/main/main.component.ts b/frontend/src/app/modules/main/main.component.ts
index c9b299e4..95dd70c4 100644
--- a/frontend/src/app/modules/main/main.component.ts
+++ b/frontend/src/app/modules/main/main.component.ts
@@ -132,7 +132,7 @@ export class MainComponent implements OnInit {
this.bindResolvedRouteData();
this.bindVisibleDateQueryParam();
- if (this.statementImportService.expenses.length) {
+ if (this.statementImportService.draft().length) {
this.statementImportService.processImport();
}
}
diff --git a/frontend/src/app/modules/main/services/statement-import.service.ts b/frontend/src/app/modules/main/services/statement-import.service.ts
index 2bdbdea5..2975ec04 100644
--- a/frontend/src/app/modules/main/services/statement-import.service.ts
+++ b/frontend/src/app/modules/main/services/statement-import.service.ts
@@ -1,4 +1,4 @@
-import { inject, Injectable } from '@angular/core';
+import { inject, Injectable, signal } from '@angular/core';
import { injectMutation } from '@tanstack/angular-query-experimental';
import { NbDialogService, NbToastrService } from '@nebular/theme';
import { plainToInstance } from 'class-transformer';
@@ -22,7 +22,8 @@ const TOASTR_TITLE = 'Bank statement import';
// eslint-disable-next-line @angular-eslint/use-injectable-provided-in -- Intentionally scoped through MainModule providers.
@Injectable()
export class StatementImportService {
- public expenses: Expense[] = [];
+ // Reactive draft — single source of truth for the review dialog
+ public readonly draft = signal([]);
private readonly dialogService = inject(NbDialogService);
private readonly mainService = inject(MainService);
@@ -68,9 +69,7 @@ export class StatementImportService {
return;
}
- this.updateImportStorage(response.expenses);
- this.reloadImportStorage();
-
+ this.setDraft(response.expenses);
this.processImport();
},
onSettled: () => this.mainService.isApplicationBusy.next(false),
@@ -82,41 +81,39 @@ export class StatementImportService {
fileInput.click();
}
- public updateImportStorage(expenses: Expense[]): void {
- localStorage.setItem(IMPORT_KEY, JSON.stringify(expenses));
- localStorage.setItem(IMPORT_COUNT_KEY, expenses.length.toString());
+ public removeFromDraft(expense: Expense): void {
+ const key = this.expenseKey(expense);
+ this.setDraft(this.draft().filter(item => this.expenseKey(item) !== key));
+ }
+
+ public clearImportStorage(): void {
+ localStorage.removeItem(IMPORT_KEY);
+ localStorage.removeItem(IMPORT_COUNT_KEY);
+ this.draft.set([]);
}
public reloadImportStorage(): void {
- const localExpenses = localStorage.getItem(IMPORT_KEY);
- if (localExpenses === null) {
+ const raw = localStorage.getItem(IMPORT_KEY);
+
+ if (raw === null) {
return;
}
try {
- const expenses = JSON.parse(localExpenses);
- this.expenses = Array.isArray(expenses)
- ? expenses.map((expense: Expense) => plainToInstance(Expense, expense))
- : [];
+ const parsed = JSON.parse(raw);
+ const expenses = Array.isArray(parsed) ? parsed.map((e: Expense) => plainToInstance(Expense, e)) : [];
+
+ this.draft.set(expenses);
} catch {
this.clearImportStorage();
}
}
- public clearImportStorage(): void {
- localStorage.removeItem(IMPORT_KEY);
- localStorage.removeItem(IMPORT_COUNT_KEY);
- this.expenses = [];
- }
-
public processImport(): void {
this.dialogService
.open(StatementReviewDialogComponent, {
context: {
- expenses: this.expenses,
- onImportChange: (expenses: Expense[]) => {
- this.updateImportStorage(expenses);
- },
+ importService: this,
},
})
.onClose.subscribe((result: { action: string; calendarRefreshNeeded: boolean }) => {
@@ -127,7 +124,7 @@ export class StatementImportService {
switch (result.action) {
case DIALOG_ACTION_IMPORT:
this.mainService.isApplicationBusy.next(true);
- this.expenseImportMutation.mutate(this.expenses, {
+ this.expenseImportMutation.mutate(this.draft(), {
onSuccess: () => {
this.toastrService.success(
'Transactions are being imported, please be patient',
@@ -143,17 +140,27 @@ export class StatementImportService {
this.clearImportStorage();
break;
case DIALOG_ACTION_CLOSE:
- if (!this.expenses.length) {
+ if (!this.draft().length) {
this.clearImportStorage();
- } else {
- this.reloadImportStorage();
}
+
break;
}
});
}
- public importStorage(): void {
- this.expenseImportMutation.mutate(this.expenses);
+ private setDraft(expenses: Expense[]): void {
+ this.draft.set(expenses);
+ localStorage.setItem(IMPORT_KEY, JSON.stringify(expenses));
+ localStorage.setItem(IMPORT_COUNT_KEY, String(expenses.length));
+ }
+
+ private expenseKey(expense: Expense): string {
+ return [
+ expense.id ?? 'null',
+ expense.label ?? '',
+ expense.amount ?? 0,
+ expense.createdAt ? new Date(expense.createdAt).toISOString() : '',
+ ].join('|');
}
}
diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss
index 96399c6f..8a2cbde9 100644
--- a/frontend/src/styles.scss
+++ b/frontend/src/styles.scss
@@ -9,6 +9,29 @@
@include nb-install() {
@include nb-theme-global();
+
+ * {
+ scrollbar-width: thin;
+ scrollbar-color: nb-theme(border-basic-color-4) transparent;
+
+ &::-webkit-scrollbar {
+ width: 4px;
+ height: 4px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: nb-theme(border-basic-color-4);
+ border-radius: 4px;
+ }
+
+ &::-webkit-scrollbar-thumb:hover {
+ background: nb-theme(border-basic-color-5);
+ }
+ }
}
body {
diff --git a/frontend/src/styles/layout.scss b/frontend/src/styles/layout.scss
index 14f63c7b..382cfc4f 100644
--- a/frontend/src/styles/layout.scss
+++ b/frontend/src/styles/layout.scss
@@ -1,5 +1,6 @@
@use 'bootstrap/scss/bootstrap-utilities' as *;
+
nb-sidebar > .main-container > .scrollable {
padding: 0 !important;
}