Skip to content

Commit 40f85e2

Browse files
committed
feat: add functionality to delete all imported finance records with confirmation dialog
1 parent a03fe49 commit 40f85e2

File tree

11 files changed

+147
-4
lines changed

11 files changed

+147
-4
lines changed

PhantomDave.BankTracking.Api/Services/FinanceRecordService.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,4 +356,20 @@ public async Task<MonthlyComparisonType> GetMonthlyComparisonAsync(
356356
LowestSpendingMonth = lowestSpending
357357
};
358358
}
359+
360+
public async Task<int> DeleteAllImportedFinanceRecordForAccountAsync(int accountId)
361+
{
362+
var records = await _unitOfWork.FinanceRecords.Query()
363+
.Where(r => r.AccountId == accountId && r.Imported)
364+
.ToListAsync();
365+
366+
var count = records.Count;
367+
if (count == 0)
368+
{
369+
return 0;
370+
}
371+
372+
await _unitOfWork.FinanceRecords.DeleteRangeAsync(records);
373+
return await _unitOfWork.SaveChangesAsync();
374+
}
359375
}

PhantomDave.BankTracking.Api/Types/Mutations/FinanceRecordMutation.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,21 @@ public async Task<bool> DeleteFinanceRecord(
9999
}
100100
return deletedRecord;
101101
}
102+
103+
[Authorize]
104+
public async Task<bool> DeleteImportedFinanceRecord(
105+
[Service] FinanceRecordService financeRecordService,
106+
[Service] IHttpContextAccessor httpContextAccessor)
107+
{
108+
var deletedRecord = await financeRecordService.DeleteAllImportedFinanceRecordForAccountAsync(httpContextAccessor.GetAccountIdFromContext());
109+
if (deletedRecord == 0)
110+
{
111+
throw new GraphQLException(
112+
ErrorBuilder.New()
113+
.SetMessage("Failed to delete finance record. Please check the provided data.")
114+
.SetCode("BAD_USER_INPUT")
115+
.Build());
116+
}
117+
return deletedRecord > 0;
118+
}
102119
}

PhantomDave.BankTracking.Data/Repositories/IRepository.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public interface IRepository<T> where T : class
1111
Task<IEnumerable<T>> AddRangeAsync(IEnumerable<T> entity);
1212
Task<T> UpdateAsync(T entity);
1313
Task<bool> DeleteAsync(object id);
14+
Task<int> DeleteRangeAsync(IEnumerable<T> entities);
1415
Task<T?> GetSingleOrDefaultAsync(Expression<Func<T, bool>> predicate);
1516
Task SaveAsync();
1617
IQueryable<T> Query();
@@ -70,6 +71,12 @@ public async Task<bool> DeleteAsync(object id)
7071
return true;
7172
}
7273

74+
public async Task<int> DeleteRangeAsync(IEnumerable<T> entities)
75+
{
76+
_dbSet.RemoveRange(entities);
77+
return entities.Count();
78+
}
79+
7380
public async Task SaveAsync()
7481
{
7582
await _context.SaveChangesAsync();

frontend/src/app/components/settings-component/settings-component.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,11 @@
4444
color: var(--mat-sys-on-surface-variant, rgba(0, 0, 0, 0.6));
4545
margin: 16px 0 0;
4646
}
47+
48+
.danger-button {
49+
margin-top: 16px;
50+
}
51+
52+
.danger-button mat-icon {
53+
margin-right: 8px;
54+
}

frontend/src/app/components/settings-component/settings-component.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@
7575
</button>
7676
</div>
7777
</form>
78+
79+
<mat-divider></mat-divider>
80+
81+
<button mat-flat-button color="warn" (click)="onDeleteAllImportedData()" class="danger-button">
82+
<mat-icon>warning</mat-icon>
83+
Delete all imported data
84+
</button>
7885
} @else {
7986
<p class="empty-state" role="status">We could not load your account information.</p>
8087
}

frontend/src/app/components/settings-component/settings-component.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@ import { ChangeDetectionStrategy, Component, effect, inject, OnInit } from '@ang
22
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
33
import { MatButtonModule } from '@angular/material/button';
44
import { MatCardModule } from '@angular/material/card';
5+
import { MatDividerModule } from '@angular/material/divider';
56
import { MatFormFieldModule } from '@angular/material/form-field';
7+
import { MatIconModule } from '@angular/material/icon';
68
import { MatInputModule } from '@angular/material/input';
79
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
810

911
import { AccountService } from '../../models/account/account-service';
12+
import { MatDialog } from '@angular/material/dialog';
13+
import { ConfirmationDialogComponentComponent } from '../ui-library/confirmation-dialog-component/confirmation-dialog-component.component';
14+
import { FinanceRecordService } from '../../models/finance-record/finance-record-service';
1015

1116
@Component({
1217
selector: 'app-settings-component',
@@ -17,14 +22,18 @@ import { AccountService } from '../../models/account/account-service';
1722
MatInputModule,
1823
MatButtonModule,
1924
MatProgressSpinnerModule,
25+
MatDividerModule,
26+
MatIconModule,
2027
],
2128
templateUrl: './settings-component.html',
2229
styleUrl: './settings-component.css',
2330
changeDetection: ChangeDetectionStrategy.OnPush,
2431
})
2532
export class SettingsComponent implements OnInit {
2633
private readonly accountService = inject(AccountService);
34+
private readonly financeRecordService = inject(FinanceRecordService);
2735
private readonly formBuilder = inject(FormBuilder);
36+
private readonly dialog = inject(MatDialog);
2837

2938
protected readonly account = this.accountService.selectedAccount;
3039
protected readonly loading = this.accountService.loading;
@@ -88,4 +97,22 @@ export class SettingsComponent implements OnInit {
8897

8998
await this.accountService.updateAccount(email!, balance ?? 0);
9099
}
100+
101+
protected async onDeleteAllImportedData() {
102+
const dialogRef = this.dialog.open(ConfirmationDialogComponentComponent, {
103+
data: {
104+
title: 'Delete all imported data',
105+
content: 'Are you sure you want to delete all imported data? This action cannot be undone.',
106+
action: [
107+
{
108+
text: 'Delete',
109+
action: async () => {
110+
await this.financeRecordService.deleteAllImportedData();
111+
dialogRef.close();
112+
},
113+
},
114+
],
115+
},
116+
});
117+
}
91118
}

frontend/src/app/components/ui-library/confirmation-dialog-component/confirmation-dialog-component.component.css

Whitespace-only changes.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<h2 mat-dialog-title>{{ dataModel().title }}</h2>
2+
<mat-dialog-content>
3+
<div [innerHTML]="dataModel().content"></div>
4+
@if (dataModel().field) {
5+
<div [innerHTML]="dataModel().field"></div>
6+
}
7+
</mat-dialog-content>
8+
<mat-dialog-actions>
9+
<button mat-flat-button color="primary" mat-dialog-close cdkFocusInitial>{{dataModel().defaultButtonText ?? 'Ok'}}</button>
10+
@if (dataModel().action) {
11+
@for (action of dataModel().action; track $index) {
12+
<button mat-flat-button color="primary" (click)="action.action()">{{ action.text }}</button>
13+
}
14+
}
15+
</mat-dialog-actions>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Component, inject, model } from '@angular/core';
2+
import { MatButtonModule } from '@angular/material/button';
3+
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
4+
5+
export interface ConfirmationDialogModel {
6+
title: string;
7+
content: string;
8+
field?: string;
9+
defaultButtonText?: string;
10+
action?: { text: string; action: () => void }[];
11+
}
12+
@Component({
13+
selector: 'app-confirmation-dialog-component',
14+
templateUrl: './confirmation-dialog-component.component.html',
15+
styleUrls: ['./confirmation-dialog-component.component.css'],
16+
imports: [MatDialogModule, MatButtonModule],
17+
})
18+
export class ConfirmationDialogComponentComponent {
19+
readonly dialogRef = inject(MatDialogRef<ConfirmationDialogComponentComponent>);
20+
readonly data = inject<ConfirmationDialogModel>(MAT_DIALOG_DATA);
21+
readonly dataModel = model(this.data);
22+
23+
closeDialog() {
24+
this.dialogRef.close();
25+
}
26+
}

frontend/src/app/models/finance-record/finance-record-service.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import { inject, Injectable, Signal, signal } from '@angular/core';
2-
import { firstValueFrom, tap } from 'rxjs';
31
import {
42
CreateFinanceRecordGQL,
53
DeleteFinanceRecordGQL,
4+
DeleteImportedFinanceRecordGQL,
65
GetFinanceRecordsGQL,
76
GetMonthlyComparisonGQL,
87
GetMonthlyComparisonQuery,
98
RecurrenceFrequency,
10-
UpdateFinanceRecordGQL,
9+
UpdateFinanceRecordGQL
1110
} from '../../../generated/graphql';
12-
import { FinanceRecord } from './finance-record';
11+
import {inject, Injectable, Signal, signal} from '@angular/core';
12+
import {firstValueFrom, tap} from 'rxjs';
13+
import {FinanceRecord} from './finance-record';
1314

1415
@Injectable({
1516
providedIn: 'root',
@@ -20,6 +21,7 @@ export class FinanceRecordService {
2021
private readonly updateFinanceRecordGQL = inject(UpdateFinanceRecordGQL);
2122
private readonly deleteFinanceRecordGQL = inject(DeleteFinanceRecordGQL);
2223
private readonly getMonthlyComparisonGQL = inject(GetMonthlyComparisonGQL);
24+
private readonly deleteImportedFinanceRecordGQL = inject(DeleteImportedFinanceRecordGQL);
2325

2426
private readonly _monthlyComparison = signal<
2527
GetMonthlyComparisonQuery['monthlyComparison'] | null
@@ -220,6 +222,21 @@ export class FinanceRecordService {
220222
};
221223
}
222224

225+
async deleteAllImportedData(): Promise<void> {
226+
this._loading.set(true);
227+
this._error.set(null);
228+
229+
try {
230+
await firstValueFrom(
231+
this.deleteImportedFinanceRecordGQL.mutate({ refetchQueries: ['getFinanceRecords'] }),
232+
);
233+
} catch (error) {
234+
this._error.set(`Failed to delete imported finance records: ${error}`);
235+
} finally {
236+
this._loading.set(false);
237+
}
238+
}
239+
223240
async getFinanceRecords(): Promise<void> {
224241
this._loading.set(true);
225242
this._error.set(null);

0 commit comments

Comments
 (0)