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
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.actions {
display: flex;
gap: 10px;
margin-bottom: 20px;
}

.confirmation-dialog {
border: 1px solid #ccc;
padding: 15px;
margin: 10px 0;
background-color: #f9f9f9;
border-radius: 5px;
}

.confirmation-actions {
display: flex;
gap: 10px;
margin-top: 10px;
}

.error {
color: red;
margin-top: 10px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,29 @@
@if (customer.hasValue() && customer.value(); as customer) {
<div>
<h2>Customer Details</h2>
<a routerLink="..">Back to Overview</a>
<div class="actions">
<a routerLink="..">Back to Overview</a>
<button type="button" (click)="toggleDeleteConfirmation()" [disabled]="isDeleting()">Delete Customer</button>
</div>

@if (showDeleteConfirmation()) {
<div class="confirmation-dialog">
<p>Are you sure you want to delete this customer?</p>
<div class="confirmation-actions">
<button type="button" (click)="deleteCustomer()" [disabled]="isDeleting()">
@if (isDeleting()) {
Deleting...
} @else {
Confirm Delete
}
</button>
<button type="button" (click)="toggleDeleteConfirmation()" [disabled]="isDeleting()">Cancel</button>
</div>
@if (deleteError()) {
<div class="error">{{ deleteError() }}</div>
}
</div>
}

<fieldset>
<legend>Personal Information</legend>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { expect, it } from 'vitest';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { render, screen } from '@testing-library/angular';
import { render, screen, waitFor } from '@testing-library/angular';
import userEvent from '@testing-library/user-event';
import { CustomerDetailsResponse } from '@sandbox-app/customer-management/models';
import CustomerDetailsComponent from './customer-details.component';
import { generateUuid } from '@sandbox-app/shared/functions';
import { ActivatedRoute, Router } from '@angular/router';

it('renders customer details when data is loaded', async () => {
const { mockRequest } = await setup();
Expand Down Expand Up @@ -174,14 +175,82 @@ it('displays error message when API request fails and can retry', async () => {
expect(screen.queryByText(/Customer Details/i)).toBeInTheDocument();
});

it('displays delete confirmation when delete button is clicked', async () => {
const { mockRequest, user } = await setup();

await mockRequest(customerDetails);

expect(screen.queryByText('Are you sure you want to delete this customer?')).not.toBeInTheDocument();

await user.click(screen.getByRole('button', { name: /delete customer/i }));

expect(screen.queryByText('Are you sure you want to delete this customer?')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /confirm delete/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
});

it('hides confirmation when cancel is clicked', async () => {
const { mockRequest, user } = await setup();

await mockRequest(customerDetails);

await user.click(screen.getByRole('button', { name: /delete customer/i }));
expect(screen.queryByText('Are you sure you want to delete this customer?')).toBeInTheDocument();

await user.click(screen.getByRole('button', { name: /cancel/i }));
expect(screen.queryByText('Are you sure you want to delete this customer?')).not.toBeInTheDocument();
});

it('deletes customer and navigates to overview when confirmed', async () => {
const { mockRequest, user, httpMock, router } = await setup();

const customerId = customerDetails.id;
await mockRequest(customerDetails);

await user.click(screen.getByRole('button', { name: /delete customer/i }));
await user.click(screen.getByRole('button', { name: /confirm delete/i }));

const deleteRequest = httpMock.expectOne(`/api/customers/${customerId}`);
expect(deleteRequest.request.method).toBe('DELETE');
deleteRequest.flush({});

await waitFor(() => {
expect(router.navigate).toHaveBeenCalledWith(['../'], { relativeTo: expect.anything() });
});
});

it('shows error message when deletion fails', async () => {
const { mockRequest, user, httpMock } = await setup();

await mockRequest(customerDetails);

await user.click(screen.getByRole('button', { name: /delete customer/i }));
await user.click(screen.getByRole('button', { name: /confirm delete/i }));

const deleteRequest = httpMock.expectOne(`/api/customers/${customerDetails.id}`);
deleteRequest.flush({ title: 'Deletion failed' }, { status: 500, statusText: 'Server Error' });

await waitFor(() => {
expect(screen.queryByText('Deletion failed')).toBeInTheDocument();
});
});

async function setup() {
const user = userEvent.setup();
const customerId = generateUuid();
const routerMock = { navigate: vi.fn() };
const activatedRouteMock = {};

const { fixture } = await render(CustomerDetailsComponent, {
inputs: {
customerId,
},
providers: [provideHttpClient(), provideHttpClientTesting()],
providers: [
provideHttpClient(),
provideHttpClientTesting(),
{ provide: Router, useValue: routerMock },
{ provide: ActivatedRoute, useValue: activatedRouteMock }
],
});
const httpMock = TestBed.inject(HttpTestingController);
return {
Expand All @@ -195,6 +264,8 @@ async function setup() {
return request;
},
user,
httpMock,
router: routerMock
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core';
import { RouterLink } from '@angular/router';
import { ChangeDetectionStrategy, Component, inject, input, signal } from '@angular/core';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { CustomersService } from '@sandbox-app/customer-management/customer-management.service';
import { stronglyTypedIdAttribute } from '@sandbox-app/shared/functions';
import { CustomerId } from '@sandbox-app/customer-management/models';
Expand All @@ -13,7 +13,33 @@ import { CustomerId } from '@sandbox-app/customer-management/models';
})
export default class CustomerDetailsComponent {
private readonly customersService = inject(CustomersService);
private readonly router = inject(Router);
private readonly route = inject(ActivatedRoute);

protected readonly customerId = input.required({ transform: stronglyTypedIdAttribute(CustomerId) });
protected readonly customer = this.customersService.getCustomerDetails(this.customerId);
protected readonly showDeleteConfirmation = signal(false);
protected readonly isDeleting = signal(false);
protected readonly deleteError = signal<string | null>(null);

protected toggleDeleteConfirmation(): void {
this.showDeleteConfirmation.set(!this.showDeleteConfirmation());
this.deleteError.set(null);
}

protected deleteCustomer(): void {
this.isDeleting.set(true);
this.deleteError.set(null);

this.customersService.deleteCustomer(this.customerId()).subscribe({
next: () => {
this.isDeleting.set(false);
this.router.navigate(['../'], { relativeTo: this.route });
},
error: (error) => {
this.isDeleting.set(false);
this.deleteError.set(error?.error?.title || 'An unexpected error occurred while deleting the customer.');
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,8 @@ export class CustomersService {
public createCustomer(customer: CreateCustomerCommand): Observable<CustomerId> {
return this.http.post<CustomerId>('/api/customers', customer);
}

public deleteCustomer(id: CustomerId): Observable<void> {
return this.http.delete<void>(`/api/customers/${id.toString()}`);
}
}
Loading