diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 0aafedd..1047d2b 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -3,6 +3,7 @@ import { provideRouter, Route } from '@angular/router'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { authGuard } from './shared/util/guards'; +import { CurrencyPipe } from '@angular/common'; const routes: Route[] = [ { @@ -18,6 +19,7 @@ export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), importProvidersFrom(BrowserAnimationsModule), + CurrencyPipe, provideAnimationsAsync() ] }; diff --git a/src/app/buxx/data-access/recent-activity.store.ts b/src/app/buxx/data-access/recent-activity.store.ts index 49cd9b6..3228685 100644 --- a/src/app/buxx/data-access/recent-activity.store.ts +++ b/src/app/buxx/data-access/recent-activity.store.ts @@ -1,6 +1,7 @@ import { computed, Injectable, Signal, signal, WritableSignal } from '@angular/core'; -import { DeleteTransaction, RecentTransaction, Transaction } from '../../shared/model/buxx.model'; +import { RecentTransaction } from '../../shared/model/buxx.model'; import { Subject } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export type RecentActivityState = { data: RecentTransaction[]; @@ -9,8 +10,19 @@ export type RecentActivityState = { @Injectable() export class RecentActivityStore { - restore$: Subject = new Subject(); - delete$: Subject = new Subject(); private state: WritableSignal = signal({data: []}); readonly data: Signal = computed(() => this.state().data); + + reset$: Subject = new Subject(); + add$: Subject = new Subject(); + + constructor() { + this.reset$.pipe(takeUntilDestroyed()) + .subscribe(() => this.state.set({data: []})); + + this.add$.pipe(takeUntilDestroyed()) + .subscribe((transaction: RecentTransaction) => this.state.update(state => ({ + data: [...state.data, transaction] + }))); + } } \ No newline at end of file diff --git a/src/app/buxx/data-access/transaction.store.ts b/src/app/buxx/data-access/transaction.store.ts index 69e2613..17b7741 100644 --- a/src/app/buxx/data-access/transaction.store.ts +++ b/src/app/buxx/data-access/transaction.store.ts @@ -7,13 +7,14 @@ import { TRANSACTIONS, UpdateTransaction } from '../../shared/model/buxx.model'; -import { from, Subject, switchMap } from 'rxjs'; +import { from, map, Subject, switchMap } from 'rxjs'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { supabase } from '../../../supabase/supabase'; import { MatSnackBar } from '@angular/material/snack-bar'; import { AuthStore } from '../../shared/data-access/auth/auth.store'; import { PostgrestSingleResponse } from '@supabase/supabase-js'; import { buildFilter } from '../../shared/util/filter-builder'; +import { RecentActivityStore } from './recent-activity.store'; type BuxxState = { loaded: boolean; @@ -38,6 +39,7 @@ export class TransactionStore { private readonly snackBar = inject(MatSnackBar); private readonly authStore = inject(AuthStore); + private readonly recentActivityStore = inject(RecentActivityStore); private state: WritableSignal = signal(initialState); loaded: Signal = computed(() => this.state().loaded); @@ -78,15 +80,17 @@ export class TransactionStore { private handleSave(): void { this.save$.pipe( takeUntilDestroyed(), - switchMap((transaction: SaveTransaction) => from(supabase.from(TRANSACTIONS).insert(transaction))) - ).subscribe({ - next: () => { - this.snackBar.open('Transaction has been saved.', undefined, { duration: 3000 }); - }, - error: err => { - this.state.update(state => ({ ...state, error: err })); + switchMap((transaction: SaveTransaction) => from(supabase.from(TRANSACTIONS).insert(transaction).select())) + ).subscribe(response => { + if (response.error) { + this.openSnackBar('Unable to save transaction.'); + this.state.update(state => ({ ...state, error: response.error.message })); + } else { + this.openSnackBar('Transaction has been saved.'); + this.recentActivityStore.add$.next({ ...response.data[0], action: 'SAVED' }); + } } - }); + ); } private handleUpdate() { @@ -96,28 +100,51 @@ export class TransactionStore { .from(TRANSACTIONS) .update(transaction) .eq('id', transaction.id) - )) - ).subscribe({ - next: () => { - this.snackBar.open('Transaction has been updated.', undefined, { duration: 3000 }); - }, - error: err => { - this.state.update(state => ({ ...state, error: err })); + ).pipe(map(response => ({ response, transaction: transaction })))) + ).subscribe(({ response, transaction }) => { + if (response.error) { + this.openSnackBar('Unable to update transaction.'); + this.state.update(state => ({ ...state, error: response.error.message })); + } else { + this.snackBar.open('Transaction has been updated.'); + this.state.update(state => ({ + ...state, + data: { + ...state.data, + transactions: state.data.transactions.map(t => t.id === transaction.id ? transaction : t) + } + })); + } } - }); + ); } private handleDelete() { this.delete$.pipe( takeUntilDestroyed(), - switchMap((id: DeleteTransaction) => from(supabase.from(TRANSACTIONS).delete().eq('id', id))) - ).subscribe({ - next: () => { - this.snackBar.open('Transaction has been deleted.', undefined, { duration: 3000 }); - }, - error: err => { - this.state.update(state => ({ ...state, error: err })); + switchMap((id: DeleteTransaction) => from(supabase.from(TRANSACTIONS).delete().eq('id', id)) + .pipe(map(response => ({ response, id }))) + )).subscribe(({ response, id }) => { + if (response.error) { + this.openSnackBar('Unable to delete transaction.'); + this.state.update(state => ({ ...state, error: response.error.message })); + } else { + this.openSnackBar('Transaction has been deleted.'); + const transaction: Transaction = this.data().transactions.find(t => t.id === id)!; + this.recentActivityStore.add$.next({ ...transaction, action: 'DELETED' }); + this.state.update(state => ({ + ...state, + data: { + ...state.data, + transactions: state.data.transactions.filter(t => t.id != id) + } + })); + } } - }); + ); + } + + private openSnackBar(message: string) { + this.snackBar.open(message, undefined, { duration: 3000 }); } } \ No newline at end of file diff --git a/src/app/buxx/feature/buxx.component.html b/src/app/buxx/feature/buxx.component.html index 76439f1..f7b3269 100644 --- a/src/app/buxx/feature/buxx.component.html +++ b/src/app/buxx/feature/buxx.component.html @@ -2,7 +2,7 @@
- +
diff --git a/src/app/buxx/feature/buxx.component.ts b/src/app/buxx/feature/buxx.component.ts index 3b5c2a1..de13aa9 100644 --- a/src/app/buxx/feature/buxx.component.ts +++ b/src/app/buxx/feature/buxx.component.ts @@ -48,7 +48,8 @@ import { queryInitialState, QueryStore } from '../data-access/query.store'; import { environment } from '../../../environments/environment'; import { PositiveNumberOnlyDirective } from '../../shared/util/positive-number.directive'; import { MaskDateDirective } from '../../shared/util/date-mask.directive'; -import { RecentTransactionComponent } from '../ui/recent-transaction/recent-transaction.component'; +import { RecentActivityComponent } from '../ui/recent-activity/recent-activity.component'; +import { RecentActivityStore } from '../data-access/recent-activity.store'; export const filterValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { const date = control.get('date'); @@ -81,17 +82,18 @@ export const filterValidator: ValidatorFn = (control: AbstractControl): Validati SummaryComponent, PositiveNumberOnlyDirective, MaskDateDirective, - RecentTransactionComponent + RecentActivityComponent ], templateUrl: './buxx.component.html', styleUrl: './buxx.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, - providers: [TransactionStore, SummaryStore, SummaryService, QueryStore] + providers: [TransactionStore, SummaryStore, SummaryService, QueryStore, RecentActivityStore] }) export class BuxxComponent implements OnInit, OnDestroy { @ViewChild(MatPaginator) paginator!: MatPaginator; readonly summaryStore = inject(SummaryStore); + readonly recentActivityStore = inject(RecentActivityStore); private readonly fb = inject(FormBuilder); private readonly dialog = inject(MatDialog); private readonly transactionStore = inject(TransactionStore); diff --git a/src/app/buxx/ui/recent-transaction/recent-transaction.component.html b/src/app/buxx/ui/recent-activity/recent-activity.component.html similarity index 72% rename from src/app/buxx/ui/recent-transaction/recent-transaction.component.html rename to src/app/buxx/ui/recent-activity/recent-activity.component.html index b6f55bb..a68c9e3 100644 --- a/src/app/buxx/ui/recent-transaction/recent-transaction.component.html +++ b/src/app/buxx/ui/recent-activity/recent-activity.component.html @@ -1,4 +1,4 @@ -@if (transactions.length) { +@if (transactions().length) { @@ -9,7 +9,7 @@ - @for (transaction of transactions; track transaction.id) { + @for (transaction of transactions(); track transaction.id) { @@ -32,13 +32,6 @@ Rs. {{ transaction.amount }} {{ transaction.date | date: 'mediumDate' }} -
- @if (transaction.action === 'DELETED') { - - } @else if (transaction.action === 'SAVED') { - - } -

{{ transaction.details }}

} diff --git a/src/app/buxx/ui/recent-transaction/recent-transaction.component.scss b/src/app/buxx/ui/recent-activity/recent-activity.component.scss similarity index 100% rename from src/app/buxx/ui/recent-transaction/recent-transaction.component.scss rename to src/app/buxx/ui/recent-activity/recent-activity.component.scss diff --git a/src/app/buxx/ui/recent-transaction/recent-transaction.component.spec.ts b/src/app/buxx/ui/recent-activity/recent-activity.component.spec.ts similarity index 55% rename from src/app/buxx/ui/recent-transaction/recent-transaction.component.spec.ts rename to src/app/buxx/ui/recent-activity/recent-activity.component.spec.ts index cfc95ac..b4ba6ab 100644 --- a/src/app/buxx/ui/recent-transaction/recent-transaction.component.spec.ts +++ b/src/app/buxx/ui/recent-activity/recent-activity.component.spec.ts @@ -1,16 +1,16 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RecentTransactionComponent } from './recent-transaction.component'; +import { RecentActivityComponent } from './recent-activity.component'; describe('RecentTransactionComponent', () => { - let component: RecentTransactionComponent; - let fixture: ComponentFixture; + let component: RecentActivityComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [RecentTransactionComponent], + imports: [RecentActivityComponent], }).compileComponents(); - fixture = TestBed.createComponent(RecentTransactionComponent); + fixture = TestBed.createComponent(RecentActivityComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/buxx/ui/recent-activity/recent-activity.component.ts b/src/app/buxx/ui/recent-activity/recent-activity.component.ts new file mode 100644 index 0000000..4c5babd --- /dev/null +++ b/src/app/buxx/ui/recent-activity/recent-activity.component.ts @@ -0,0 +1,49 @@ +import { Component, input } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatIconModule } from '@angular/material/icon'; +import { RecentTransaction } from '../../../shared/model/buxx.model'; +import { MatChipsModule } from '@angular/material/chips'; +import { DatePipe } from '@angular/common'; +import { MatCardModule } from '@angular/material/card'; + +@Component({ + selector: 'as-recent-activity', + standalone: true, + imports: [ + MatButtonModule, + MatExpansionModule, + MatIconModule, + MatChipsModule, + DatePipe, + MatCardModule + ], + templateUrl: './recent-activity.component.html', + styleUrl: './recent-activity.component.scss', +}) +export class RecentActivityComponent { + + transactions = input.required(); + // transactions: RecentTransaction[] = [ + // { + // action: 'DELETED', + // date: (new Date()).toString(), + // id: 'sds', + // name: 'Test Name', + // isExpense: true, + // details: 'sdfd', + // amount: 222, + // userId: 'sdfsdf' + // }, + // { + // action: 'SAVED', + // date: (new Date()).toString(), + // id: 'sds', + // name: 'Test Name Again', + // isExpense: true, + // details: 'sdfd', + // amount: 222, + // userId: 'sdfsdf' + // } + // ]; +} diff --git a/src/app/buxx/ui/recent-transaction/recent-transaction.component.ts b/src/app/buxx/ui/recent-transaction/recent-transaction.component.ts deleted file mode 100644 index ddaaa0e..0000000 --- a/src/app/buxx/ui/recent-transaction/recent-transaction.component.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Component } from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; -import { MatExpansionModule } from '@angular/material/expansion'; -import { MatIconModule } from '@angular/material/icon'; -import { RecentTransaction } from '../../../shared/model/buxx.model'; -import { MatChipsModule } from '@angular/material/chips'; -import { DatePipe } from '@angular/common'; -import { MatCardModule } from '@angular/material/card'; - -@Component({ - selector: 'as-recent-transaction', - standalone: true, - imports: [ - MatButtonModule, - MatExpansionModule, - MatIconModule, - MatChipsModule, - DatePipe, - MatCardModule - ], - templateUrl: './recent-transaction.component.html', - styleUrl: './recent-transaction.component.scss', -}) -export class RecentTransactionComponent { - - transactions: RecentTransaction[] = [ - { - action: 'DELETED', - date: (new Date()).toString(), - id: 'sds', - name: 'Test Name', - isExpense: true, - details: 'sdfd', - amount: 222, - userId: 'sdfsdf' - }, - { - action: 'SAVED', - date: (new Date()).toString(), - id: 'sds', - name: 'Test Name Again', - isExpense: true, - details: 'sdfd', - amount: 222, - userId: 'sdfsdf' - } - ]; - - restore(transaction: RecentTransaction) { - - } - - delete(transaction: RecentTransaction): void { - - } -} diff --git a/src/app/buxx/ui/summary/summary.component.html b/src/app/buxx/ui/summary/summary.component.html index 94a1f68..5440ba6 100644 --- a/src/app/buxx/ui/summary/summary.component.html +++ b/src/app/buxx/ui/summary/summary.component.html @@ -1,7 +1,7 @@
- Rs. {{summary().balance}} + {{summary().balance | nprCurrency }} arrow_upward_altbalance @@ -10,7 +10,7 @@ - Rs. {{summary().income}} + {{summary().income | nprCurrency}} arrow_upward_altincome @@ -19,7 +19,7 @@ - Rs. {{summary().expense}} + {{summary().expense | nprCurrency}} arrow_downward_altexpense diff --git a/src/app/buxx/ui/summary/summary.component.ts b/src/app/buxx/ui/summary/summary.component.ts index 44cc0a9..8843219 100644 --- a/src/app/buxx/ui/summary/summary.component.ts +++ b/src/app/buxx/ui/summary/summary.component.ts @@ -2,11 +2,12 @@ import { Component, input } from '@angular/core'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { Summary } from '../../../shared/model/buxx.model'; +import { NprCurrencyPipe } from '../../../shared/util/npr-currency.pipe'; @Component({ selector: 'as-summary', standalone: true, - imports: [MatCardModule, MatIconModule], + imports: [MatCardModule, MatIconModule, NprCurrencyPipe], templateUrl: './summary.component.html', styleUrl: './summary.component.scss' }) diff --git a/src/app/shared/ui/add-new-dialog/feature/transaction-dialog.component.html b/src/app/shared/ui/add-new-dialog/feature/transaction-dialog.component.html index 3dad31d..2319e67 100644 --- a/src/app/shared/ui/add-new-dialog/feature/transaction-dialog.component.html +++ b/src/app/shared/ui/add-new-dialog/feature/transaction-dialog.component.html @@ -33,8 +33,8 @@

{{ data ? 'Update Transaction' : 'Add New Transaction' }} - - + diff --git a/src/app/shared/ui/add-new-dialog/feature/transaction-dialog.component.ts b/src/app/shared/ui/add-new-dialog/feature/transaction-dialog.component.ts index 5440cfd..9962d22 100644 --- a/src/app/shared/ui/add-new-dialog/feature/transaction-dialog.component.ts +++ b/src/app/shared/ui/add-new-dialog/feature/transaction-dialog.component.ts @@ -59,11 +59,11 @@ export class TransactionDialogComponent implements OnInit { this.transactionForm.patchValue({ name, amount, date, isExpense, details }); } - closDialog(isSave: boolean): void { - if (isSave) { - this.dialogRef.close(this.transactionForm.value); - } else { + closeDialog(isCancel: boolean): void { + if (isCancel) { this.dialogRef.close(); + } else { + this.dialogRef.close(this.data ? { ...this.transactionForm.value, id: this.data.id } : this.transactionForm.value); } } } \ No newline at end of file diff --git a/src/app/shared/util/filter-builder.ts b/src/app/shared/util/filter-builder.ts index 34c9ebd..46df040 100644 --- a/src/app/shared/util/filter-builder.ts +++ b/src/app/shared/util/filter-builder.ts @@ -3,7 +3,7 @@ import { format } from 'date-fns'; import { supabase } from '../../../supabase/supabase'; export const buildFilter = (query: Query, userId: string): BuxxFilterBuilder => { - const {criteria, paginate} = query; + const { criteria, paginate } = query; let filter: BuxxFilterBuilder = supabase .from(TRANSACTIONS) .select('id, name, details, date, isExpense, userId, amount', { count: 'exact' }); @@ -16,8 +16,9 @@ export const buildFilter = (query: Query, userId: string): BuxxFilterBuilder => if (criteria?.date) { buildDateRangeFilter(filter, criteria.date.start, criteria.date.end); } - filter = filter.eq('userId', userId); - filter = filter.range(paginate?.range.start!, paginate?.range.end!); + filter = filter.eq('userId', userId) + .range(paginate?.range.start!, paginate?.range.end!) + .order('amount', { ascending: false }); return filter; }; diff --git a/src/app/shared/util/npr-currency.pipe.ts b/src/app/shared/util/npr-currency.pipe.ts new file mode 100644 index 0000000..a11e186 --- /dev/null +++ b/src/app/shared/util/npr-currency.pipe.ts @@ -0,0 +1,15 @@ +import { inject, Pipe, PipeTransform } from '@angular/core'; +import { CurrencyPipe } from '@angular/common'; + +@Pipe({ + name: 'nprCurrency', + standalone: true +}) +export class NprCurrencyPipe implements PipeTransform { + + private readonly currencyPipe = inject(CurrencyPipe); + + transform(value: number): string { + return this.currencyPipe.transform(value, 'NPR', 'Rs. ', '1.0-3') ?? ''; + } +} \ No newline at end of file