feat: enhance checkout process with purchase editing and status handling
- Implemented purchase editing functionality in CheckoutPageComponent, allowing users to modify items in their purchase. - Added a new guard (checkoutPendingPurchaseGuard) to prevent navigation away from the checkout page while a purchase is in progress. - Updated the login page to handle return URLs after authentication. - Enhanced product detail page to support direct purchases with a new buyNow method. - Introduced new UI elements and logic to handle purchase status, including expired and rejected states in PurchaseStatusPageComponent. - Improved cart component to allow editing of item quantities with a toggle button. - Added quantity selector enhancements to disable controls when necessary. - Updated tests to cover new functionalities and ensure proper behavior of components.
This commit is contained in:
@@ -5,16 +5,30 @@
|
||||
<header class="d-flex align-items-center p-3 justify-content-between bg-transparent cart-header">
|
||||
<h2 class="m-0 text-uppercase fw-bold cart-title">{{ title() }}</h2>
|
||||
|
||||
@if (showClose()) {
|
||||
<button
|
||||
class="btn btn-link p-0 border-0 d-inline-flex align-items-center justify-content-center cart-close-btn"
|
||||
type="button"
|
||||
aria-label="Cerrar carrito"
|
||||
(click)="closed.emit()"
|
||||
>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
}
|
||||
<div class="d-flex align-items-center cart-header-actions">
|
||||
@if (!readonly() && allowEditing() && items().length > 0) {
|
||||
<button
|
||||
class="btn btn-link p-0 border-0 cart-edit-btn"
|
||||
type="button"
|
||||
[attr.aria-pressed]="editing()"
|
||||
[disabled]="editingDisabled()"
|
||||
(click)="toggleEditing()"
|
||||
>
|
||||
{{ editing() ? 'Listo' : 'Modificar' }}
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (showClose()) {
|
||||
<button
|
||||
class="btn btn-link p-0 border-0 d-inline-flex align-items-center justify-content-center cart-close-btn"
|
||||
type="button"
|
||||
aria-label="Cerrar carrito"
|
||||
(click)="closed.emit()"
|
||||
>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="d-grid w-100 flex-grow-1 overflow-y-auto cart-items-container">
|
||||
@@ -31,6 +45,9 @@
|
||||
[discountPercentage]="item.discountPercentage"
|
||||
[attributes]="item.attributes"
|
||||
[quantity]="getItemQuantity(item)"
|
||||
[readonly]="readonly()"
|
||||
[quantityDisabled]="editingDisabled() || (allowEditing() && !editing())"
|
||||
[showRemove]="allowRemove()"
|
||||
(quantityChange)="onItemQuantityChange(idx, $event)"
|
||||
(remove)="onItemRemove(idx)"
|
||||
/>
|
||||
|
||||
@@ -21,6 +21,17 @@
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.cart-header-actions {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.cart-edit-btn {
|
||||
color: var(--bs-primary);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cart-items-container {
|
||||
min-height: 0;
|
||||
gap: 0;
|
||||
|
||||
@@ -306,4 +306,105 @@ describe('CartComponent', () => {
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('enables quantity editing only while Modificar mode is active', async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CartComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: CartService,
|
||||
useValue: {
|
||||
cart: signal(null).asReadonly(),
|
||||
updateItemQuantity: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
},
|
||||
},
|
||||
{ provide: ModalService, useValue: {} },
|
||||
{
|
||||
provide: ToastService,
|
||||
useValue: { success: vi.fn(), info: vi.fn(), danger: vi.fn() },
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(CartComponent);
|
||||
fixture.componentRef.setInput('items', [
|
||||
{
|
||||
cartItemId: 10,
|
||||
imageUrl: null,
|
||||
product: 'Producto de prueba',
|
||||
originalPrice: null,
|
||||
discountedPrice: 1000,
|
||||
discountPercentage: null,
|
||||
attributes: [],
|
||||
quantity: 1,
|
||||
},
|
||||
]);
|
||||
fixture.componentRef.setInput('allowEditing', true);
|
||||
const editingChange = vi.fn();
|
||||
fixture.componentInstance.editing.subscribe(editingChange);
|
||||
fixture.detectChanges();
|
||||
|
||||
const editButton = fixture.debugElement.query(By.css('.cart-edit-btn'));
|
||||
expect(editButton.nativeElement.textContent.trim()).toBe('Modificar');
|
||||
expect(
|
||||
fixture.debugElement.query(By.css('app-cart-item')).componentInstance.quantityDisabled(),
|
||||
).toBe(true);
|
||||
|
||||
editButton.nativeElement.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(editButton.nativeElement.textContent.trim()).toBe('Listo');
|
||||
expect(
|
||||
fixture.debugElement.query(By.css('app-cart-item')).componentInstance.quantityDisabled(),
|
||||
).toBe(false);
|
||||
expect(editingChange).toHaveBeenCalledWith(true);
|
||||
|
||||
editButton.nativeElement.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(editButton.nativeElement.textContent.trim()).toBe('Modificar');
|
||||
expect(editingChange).toHaveBeenLastCalledWith(false);
|
||||
});
|
||||
|
||||
it('allows editing directly when the optional Modificar toggle is disabled', async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CartComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: CartService,
|
||||
useValue: {
|
||||
cart: signal(null).asReadonly(),
|
||||
updateItemQuantity: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
},
|
||||
},
|
||||
{ provide: ModalService, useValue: {} },
|
||||
{
|
||||
provide: ToastService,
|
||||
useValue: { success: vi.fn(), info: vi.fn(), danger: vi.fn() },
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(CartComponent);
|
||||
fixture.componentRef.setInput('items', [
|
||||
{
|
||||
cartItemId: 10,
|
||||
imageUrl: null,
|
||||
product: 'Producto de prueba',
|
||||
originalPrice: null,
|
||||
discountedPrice: 1000,
|
||||
discountPercentage: null,
|
||||
attributes: [],
|
||||
quantity: 1,
|
||||
},
|
||||
]);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.debugElement.query(By.css('.cart-edit-btn'))).toBeNull();
|
||||
expect(
|
||||
fixture.debugElement.query(By.css('app-cart-item')).componentInstance.quantityDisabled(),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
model,
|
||||
output,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
@@ -48,11 +49,21 @@ export class CartComponent {
|
||||
readonly discount = input<number>(0);
|
||||
readonly total = input<number>(0);
|
||||
readonly backgroundColor = input<string>('#ffffff');
|
||||
readonly readonly = input<boolean>(false);
|
||||
readonly allowEditing = input<boolean>(false);
|
||||
readonly allowRemove = input<boolean>(true);
|
||||
readonly persistQuantityChanges = input<boolean>(true);
|
||||
readonly editingDisabled = input<boolean>(false);
|
||||
readonly editing = model<boolean>(false);
|
||||
|
||||
readonly closed = output<void>();
|
||||
readonly itemQuantityChange = output<{
|
||||
item: CartItemMock;
|
||||
index: number;
|
||||
quantity: number;
|
||||
}>();
|
||||
|
||||
protected readonly quantityOverrides = signal<Record<number, number>>({});
|
||||
|
||||
constructor() {
|
||||
this.quantityUpdates$
|
||||
.pipe(
|
||||
@@ -103,6 +114,20 @@ export class CartComponent {
|
||||
|
||||
protected onItemQuantityChange(index: number, newQuantity: number): void {
|
||||
const mockItem = this.items()[index];
|
||||
if (!mockItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.itemQuantityChange.emit({
|
||||
item: mockItem,
|
||||
index,
|
||||
quantity: newQuantity,
|
||||
});
|
||||
|
||||
if (!this.persistQuantityChanges()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cartItemId = mockItem?.cartItemId;
|
||||
if (cartItemId) {
|
||||
this.quantityOverrides.update((overrides) => ({
|
||||
@@ -153,6 +178,15 @@ export class CartComponent {
|
||||
protected readonly formattedDiscount = computed(() => this.formatCurrency(this.discount()));
|
||||
protected readonly formattedTotal = computed(() => this.formatCurrency(this.total()));
|
||||
|
||||
protected toggleEditing(): void {
|
||||
if (this.editingDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editing = !this.editing();
|
||||
this.editing.set(editing);
|
||||
}
|
||||
|
||||
private formatCurrency(value: number): string {
|
||||
const rounded = Math.round(value);
|
||||
const parts = rounded.toString().split('.');
|
||||
|
||||
Reference in New Issue
Block a user