mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
committed by
Nino Righi
parent
2c39ca05a9
commit
aee64d78e2
@@ -1,9 +1,10 @@
|
||||
import { BonusCardInfoDTO } from '@generated/swagger/crm-api';
|
||||
|
||||
export interface BonusCardInfo extends BonusCardInfoDTO {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
isActive: boolean;
|
||||
isPrimary: boolean;
|
||||
totalPoints: number;
|
||||
}
|
||||
import { BonusCardInfoDTO } from '@generated/swagger/crm-api';
|
||||
|
||||
export interface BonusCardInfo extends BonusCardInfoDTO {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
isActive: boolean;
|
||||
isPrimary: boolean;
|
||||
totalPoints: number;
|
||||
code: string;
|
||||
}
|
||||
|
||||
@@ -4,27 +4,39 @@ import { CrmSearchService } from '../services/crm-search.service';
|
||||
import { BonusCardInfo } from '../models';
|
||||
|
||||
/**
|
||||
* Resource for loading customer bonus cards (Kundenkarten).
|
||||
* Resource for loading customer bonus cards (Kundenkarten) using Angular's Resource API.
|
||||
*
|
||||
* Provides reactive loading of all bonus cards for a given customer ID.
|
||||
* Customer ID can be changed dynamically via `params()` method.
|
||||
* Provides reactive, automatic loading of all bonus cards for a given customer ID.
|
||||
* Uses Angular's `resource()` function for declarative data fetching with built-in
|
||||
* loading states, error handling, and automatic race condition prevention.
|
||||
*
|
||||
* **Note:** This resource should be provided at the component level,
|
||||
* not in root. Provide it in the `providers` array of the component
|
||||
* that needs scoped access to customer bonus cards.
|
||||
* **Features:**
|
||||
* - Reactive loading triggered by parameter changes
|
||||
* - Automatic request cancellation on parameter updates (race condition prevention)
|
||||
* - Built-in loading, error, and status states
|
||||
* - Customer ID can be changed dynamically via `params()` method
|
||||
* - Lazy loading: only fetches when customerId is provided
|
||||
*
|
||||
* **Lifecycle:**
|
||||
* - **Injectable:** Component-level only (not providedIn: 'root')
|
||||
* - **Scope:** Provide in component `providers` array for isolated state
|
||||
* - **Data Flow:** params → loader → resource.value → UI
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Component({
|
||||
* providers: [CustomerBonusCardsResource],
|
||||
* })
|
||||
* export class MyFeatureComponent {
|
||||
* #bonusCardsResource = inject(CustomerBonusCardsResource);
|
||||
* export class CustomerCardsComponent {
|
||||
* readonly #bonusCardsResource = inject(CustomerBonusCardsResource);
|
||||
*
|
||||
* cards = this.#bonusCardsResource.resource.value;
|
||||
* isLoading = this.#bonusCardsResource.resource.isLoading;
|
||||
* // Reactive signals exposed by resource
|
||||
* readonly cards = this.#bonusCardsResource.resource.value;
|
||||
* readonly isLoading = this.#bonusCardsResource.resource.isLoading;
|
||||
* readonly error = this.#bonusCardsResource.resource.error;
|
||||
*
|
||||
* loadCards(customerId: number) {
|
||||
* // Trigger load by updating params
|
||||
* loadCards(customerId: number): void {
|
||||
* this.#bonusCardsResource.params({ customerId });
|
||||
* }
|
||||
* }
|
||||
@@ -39,17 +51,40 @@ export class CustomerBonusCardsResource {
|
||||
|
||||
/**
|
||||
* Current customer ID being loaded.
|
||||
*
|
||||
* Read-only computed signal that exposes the current customer ID parameter.
|
||||
* Returns `undefined` if no customer ID has been set via `params()`.
|
||||
*
|
||||
* @returns Customer ID or undefined if not set
|
||||
*/
|
||||
readonly customerId = computed(() => this.#customerId());
|
||||
|
||||
/**
|
||||
* Resource that loads bonus cards based on current parameters.
|
||||
* Angular Resource API instance that manages bonus card loading.
|
||||
*
|
||||
* Exposes:
|
||||
* - `value()` - Array of bonus cards or undefined
|
||||
* - `isLoading()` - Loading state
|
||||
* - `error()` - Error state
|
||||
* - `status()` - Current status ('idle' | 'loading' | 'resolved' | 'error')
|
||||
* Automatically loads bonus cards when `customerId` parameter changes.
|
||||
* Provides reactive signals for data, loading state, errors, and status.
|
||||
*
|
||||
* **Exposed Signals:**
|
||||
* - `value()` - Array of bonus cards or undefined (when no data loaded)
|
||||
* - `isLoading()` - Boolean indicating if a request is in progress
|
||||
* - `error()` - Error object if load failed, undefined otherwise
|
||||
* - `status()` - Current lifecycle status: 'idle' | 'loading' | 'resolved' | 'error'
|
||||
*
|
||||
* **Behavior:**
|
||||
* - Lazy: Only loads when customerId is provided (not undefined)
|
||||
* - Reactive: Automatically reloads when params change
|
||||
* - Safe: Cancels previous requests to prevent race conditions
|
||||
* - Cacheable: Returns undefined as default value before first load
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Access reactive signals
|
||||
* const cards = this.resource.value(); // BonusCardInfo[] | undefined
|
||||
* const loading = this.resource.isLoading(); // boolean
|
||||
* const error = this.resource.error(); // Error | undefined
|
||||
* const status = this.resource.status(); // ResourceStatus
|
||||
* ```
|
||||
*/
|
||||
readonly resource = resource({
|
||||
params: computed(() => ({ customerId: this.#customerId() })),
|
||||
@@ -81,14 +116,85 @@ export class CustomerBonusCardsResource {
|
||||
defaultValue: undefined,
|
||||
});
|
||||
|
||||
constructor() {
|
||||
(window as any)['__customerBonusCardsResource'] = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update resource parameters to trigger a reload.
|
||||
* Updates resource parameters to trigger a reactive reload of bonus cards.
|
||||
*
|
||||
* Setting a new `customerId` will automatically cancel any in-flight request
|
||||
* and start a new load with the updated parameter. The resource's loader
|
||||
* function will be called with the new parameters.
|
||||
*
|
||||
* **Behavior:**
|
||||
* - Passing `customerId` triggers a load for that customer
|
||||
* - Passing `undefined` clears the data and returns resource to idle state
|
||||
* - Automatically cancels previous requests (race condition prevention)
|
||||
* - Updates are reactive: UI updates automatically via resource signals
|
||||
*
|
||||
* @param params - Parameters for loading bonus cards
|
||||
* @param params.customerId - Customer ID to load cards for (undefined clears data)
|
||||
* @param params.customerId - Customer ID to load cards for, or undefined to clear
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Load cards for customer 12345
|
||||
* bonusCardsResource.params({ customerId: 12345 });
|
||||
*
|
||||
* // Clear data and reset to idle
|
||||
* bonusCardsResource.params({ customerId: undefined });
|
||||
* ```
|
||||
*/
|
||||
params(params: { customerId?: number }): void {
|
||||
this.#logger.debug('Updating params', () => params);
|
||||
this.#customerId.set(params.customerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a random test card to the current card list.
|
||||
*
|
||||
* **For testing purposes only.** Creates a new card with random code
|
||||
* and copies customer details from the first existing card.
|
||||
*
|
||||
* **Testing the Card Stack Display:**
|
||||
* This method is useful for testing the card carousel stack behavior,
|
||||
* particularly verifying that newly added cards appear correctly in the stack
|
||||
* rather than mispositioned on the right side.
|
||||
*
|
||||
* **How to Test:**
|
||||
* 1. Open browser DevTools console (F12)
|
||||
* 2. Navigate to a customer with bonus cards
|
||||
* 3. Execute: `window.__customerBonusCardsResource.addRandomCardForTesting()`
|
||||
* 4. Verify the new card appears properly in the stack (not on the right)
|
||||
* 5. Repeat to test with multiple new cards
|
||||
*
|
||||
* **Expected Behavior:**
|
||||
* - New card should appear in correct stack position based on isPrimary/isActive
|
||||
* - Card should animate smoothly into position
|
||||
* - Stack should recalculate positions using ResizeObserver
|
||||
* - No visual glitches or misalignment
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // From browser console (development only):
|
||||
* window.__customerBonusCardsResource.addRandomCardForTesting();
|
||||
*
|
||||
* // From component code:
|
||||
* this.#bonusCardsResource.addRandomCardForTesting();
|
||||
* ```
|
||||
*/
|
||||
addRandomCardForTesting(): void {
|
||||
const currentCards = this.resource.value() ?? [];
|
||||
const newCard: BonusCardInfo = {
|
||||
code: `TEST-${Math.floor(Math.random() * 100000)}`,
|
||||
format: 'CARDCODE',
|
||||
isActive: true,
|
||||
isPrimary: false,
|
||||
totalPoints: currentCards[0].totalPoints,
|
||||
firstName: currentCards[0].firstName,
|
||||
lastName: currentCards[0].lastName,
|
||||
};
|
||||
this.#logger.debug('Adding random test card', () => ({ newCard }));
|
||||
this.resource.set([...currentCards, newCard]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
Directive,
|
||||
effect,
|
||||
ElementRef,
|
||||
inject,
|
||||
signal,
|
||||
@@ -11,6 +12,25 @@ export class CardStackContainerDirective implements AfterViewInit {
|
||||
readonly elementRef = inject(ElementRef<HTMLElement>);
|
||||
|
||||
readonly centerX = signal(0);
|
||||
private resizeObserver?: ResizeObserver;
|
||||
|
||||
constructor() {
|
||||
// Use ResizeObserver to detect layout changes (when cards added/removed)
|
||||
effect((onCleanup) => {
|
||||
const el = this.elementRef.nativeElement;
|
||||
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
// Recalculate center when container size changes
|
||||
this.centerX.set(this.getHorizontalCenter());
|
||||
});
|
||||
|
||||
this.resizeObserver.observe(el);
|
||||
|
||||
onCleanup(() => {
|
||||
this.resizeObserver?.disconnect();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.centerX.set(this.getHorizontalCenter());
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
AfterViewInit,
|
||||
computed,
|
||||
Directive,
|
||||
effect,
|
||||
ElementRef,
|
||||
inject,
|
||||
signal,
|
||||
@@ -26,7 +27,24 @@ export class CardStackDistanceDirective implements AfterViewInit {
|
||||
|
||||
centerX = signal(0);
|
||||
|
||||
constructor() {
|
||||
// Recalculate position when container center changes
|
||||
effect(() => {
|
||||
// React to container center changes
|
||||
const containerCenter = this.container.centerX();
|
||||
|
||||
// Recalculate this card's center position
|
||||
if (containerCenter !== 0) {
|
||||
// Use requestAnimationFrame to ensure DOM is stable
|
||||
requestAnimationFrame(() => {
|
||||
this.centerX.set(this.getHorizontalCenter());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
// Initial calculation
|
||||
this.centerX.set(this.getHorizontalCenter());
|
||||
}
|
||||
|
||||
|
||||
@@ -39,24 +39,29 @@ crm-customer-card {
|
||||
transform: translateX(var(--distance-to-center));
|
||||
}
|
||||
|
||||
.stacked crm-customer-card:nth-child(1) {
|
||||
/* Use data attributes instead of nth-child for dynamic positioning */
|
||||
.stacked crm-customer-card[data-stack-position='0'] {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.stacked crm-customer-card:nth-child(2) {
|
||||
.stacked crm-customer-card[data-stack-position='1'] {
|
||||
transform: translateX(calc(var(--distance-to-center) - 3.6rem))
|
||||
translateY(1rem) rotate(-2.538deg);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.stacked crm-customer-card:nth-child(3) {
|
||||
.stacked crm-customer-card[data-stack-position='2'] {
|
||||
transform: translateX(calc(var(--distance-to-center) + 3.6rem))
|
||||
translateY(1rem) rotate(2.538deg);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.stacked crm-customer-card:nth-child(n + 4) {
|
||||
/* Hide cards beyond the 3rd position */
|
||||
.stacked
|
||||
crm-customer-card:not([data-stack-position='0']):not([data-stack-position='1']):not([data-stack-position='2']) {
|
||||
box-shadow: none;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Respect user's motion preferences */
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
(cardLocked)="cardLocked.emit()"
|
||||
crmCardStackDistance
|
||||
[style.--card-index]="idx"
|
||||
[style.--stack-position]="idx"
|
||||
[attr.data-stack-position]="idx"
|
||||
/>
|
||||
}
|
||||
</ui-carousel>
|
||||
|
||||
@@ -52,15 +52,24 @@ export class CustomerCardsCarouselComponent {
|
||||
readonly cardLocked = output<void>();
|
||||
|
||||
/**
|
||||
* Cards sorted with blocked cards at the end.
|
||||
* Cards sorted by priority: isPrimary > isActive > code
|
||||
* Per Figma annotation: "gesperrte Karte immer nach hinten"
|
||||
*/
|
||||
readonly sortedCards = computed(() => {
|
||||
const cards = this.cards();
|
||||
return [...cards].sort((a, b) => {
|
||||
// Active cards first, blocked cards last
|
||||
if (a.isActive === b.isActive) return 0;
|
||||
return a.isActive ? -1 : 1;
|
||||
// 1. Primary cards first
|
||||
if (a.isPrimary !== b.isPrimary) {
|
||||
return a.isPrimary ? -1 : 1;
|
||||
}
|
||||
|
||||
// 2. Active cards before blocked cards
|
||||
if (a.isActive !== b.isActive) {
|
||||
return a.isActive ? -1 : 1;
|
||||
}
|
||||
|
||||
// 3. Sort by code for stable ordering
|
||||
return a.code.localeCompare(b.code);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user