From 86f2461785723cc3c019783c74b785fbc165fcfd Mon Sep 17 00:00:00 2001 From: Algirdas Date: Sun, 5 Jul 2026 01:08:39 +0300 Subject: [PATCH] chore: ui updates --- backend/.env | 6 +- .../sidebar/actions/actions.component.html | 4 +- .../sidebar/actions/actions.component.ts | 14 ++- .../statement-review-dialog.component.html | 6 +- .../statement-review-dialog.component.scss | 4 + .../statement-review-dialog.component.ts | 111 ++++++++---------- .../header-sidebar-toggle.component.ts | 11 +- .../src/app/modules/main/main.component.ts | 2 +- .../main/services/statement-import.service.ts | 67 ++++++----- frontend/src/styles.scss | 23 ++++ frontend/src/styles/layout.scss | 1 + 11 files changed, 142 insertions(+), 107 deletions(-) diff --git a/backend/.env b/backend/.env index 68bbbc35..376b7719 100644 --- a/backend/.env +++ b/backend/.env @@ -16,8 +16,8 @@ ###> symfony/framework-bundle ### APP_ENV=prod -APP_SECRET={REGENERATE_SECRET} -APP_HASH={REGENERATE_SECRET} +APP_SECRET=e44d0c2bce708e5d00f41345fbb5fedc +APP_HASH=f591924c3c8932c236c89837d70f8f5c APP_VERSION=1.0.5 ###< symfony/framework-bundle ### @@ -49,7 +49,7 @@ FRONTEND_URL=http://localhost:18002 ###> lexik/jwt-authentication-bundle ### JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem -JWT_PASSPHRASE={REGENERATE_SECRET} +JWT_PASSPHRASE=5363aaa5301c9cb8cfbf136e8d8c7fd8 ###< lexik/jwt-authentication-bundle ### ###> symfony/messenger ### diff --git a/frontend/src/app/modules/main/components/sidebar/actions/actions.component.html b/frontend/src/app/modules/main/components/sidebar/actions/actions.component.html index e95f985c..582e6d0c 100644 --- a/frontend/src/app/modules/main/components/sidebar/actions/actions.component.html +++ b/frontend/src/app/modules/main/components/sidebar/actions/actions.component.html @@ -5,10 +5,10 @@ @if (user?.isAdmin) { } - @if (statementImportService.expenses.length) { + @if (hasPendingImport) { diff --git a/frontend/src/app/modules/main/components/sidebar/actions/actions.component.ts b/frontend/src/app/modules/main/components/sidebar/actions/actions.component.ts index 2a38b056..9037681a 100644 --- a/frontend/src/app/modules/main/components/sidebar/actions/actions.component.ts +++ b/frontend/src/app/modules/main/components/sidebar/actions/actions.component.ts @@ -19,11 +19,19 @@ export class ActionsComponent { @Input() public user: User; - protected statementImportService = inject(StatementImportService); protected readonly environment = environment; - private dialogService = inject(NbDialogService); - private router = inject(Router); + private readonly dialogService = inject(NbDialogService); + private readonly router = inject(Router); + private readonly statementImportService = inject(StatementImportService); + + protected get hasPendingImport(): boolean { + return this.statementImportService.draft().length > 0; + } + + public processImport(): void { + this.statementImportService.processImport(); + } public editCategories(): void { this.dialogService.open(CategoriesDialogComponent, { diff --git a/frontend/src/app/modules/main/dialogs/statement-review-dialog/statement-review-dialog.component.html b/frontend/src/app/modules/main/dialogs/statement-review-dialog/statement-review-dialog.component.html index 6b413c44..3c499f1a 100644 --- a/frontend/src/app/modules/main/dialogs/statement-review-dialog/statement-review-dialog.component.html +++ b/frontend/src/app/modules/main/dialogs/statement-review-dialog/statement-review-dialog.component.html @@ -13,12 +13,12 @@ - TRANSACTIONS - {{ expenses.length }} + TRANSACTIONS - {{ expensesCount }} {{ totalExpensesAmount | shortNumber }} @for (date of groupedDates; track date) { - +
{{ 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; }