mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 1969: Reward Shopping Cart Implementation with Navigation State Management and Shipping Address Integration
1. Reward Shopping Cart Implementation - New shopping cart with quantity control and availability checking - Responsive shopping cart item component with improved CSS styling - Shipping address integration in cart - Customer reward card and billing/shipping address components 2. Navigation State Management Library (@isa/core/navigation) - New library with type-safe navigation context service (373 lines) - Navigation state service (287 lines) for temporary state between routes - Comprehensive test coverage (668 + 227 lines of tests) - Documentation (792 lines in README.md) - Replaces query parameters for passing temporary navigation context 3. CRM Shipping Address Services - New ShippingAddressService with fetching and validation - CustomerShippingAddressResource and CustomerShippingAddressesResource - Zod schemas for data validation 4. Additional Improvements - Enhanced searchbox accessibility with ARIA support - Availability data access rework for better fetching/mapping - Storybook tooltip variant support - Vitest JUnit and Cobertura reporting configuration Related work items: #5382, #5383, #5384
This commit is contained in:
committed by
Nino Righi
parent
f15848d5c0
commit
596ae1da1b
@@ -4,3 +4,4 @@ export * from './country';
|
||||
export * from './customer-type';
|
||||
export * from './customer.model';
|
||||
export * from './payer';
|
||||
export * from './shipping-address.model';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ShippingAddressDTO } from '@generated/swagger/crm-api';
|
||||
|
||||
export type ShippingAddress = ShippingAddressDTO;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { effect, inject, Injectable, resource, signal } from '@angular/core';
|
||||
import { CrmTabMetadataService, ShippingAddressService } from '../services';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { ShippingAddress } from '../models';
|
||||
|
||||
@Injectable()
|
||||
export class CustomerShippingAddressResource {
|
||||
#shippingAddressService = inject(ShippingAddressService);
|
||||
|
||||
#params = signal<{
|
||||
shippingAddressId: number | undefined;
|
||||
}>({
|
||||
shippingAddressId: undefined,
|
||||
});
|
||||
|
||||
params(params: { shippingAddressId?: number }) {
|
||||
this.#params.update((p) => ({ ...p, ...params }));
|
||||
}
|
||||
|
||||
readonly resource = resource({
|
||||
params: () => this.#params(),
|
||||
loader: async ({ params, abortSignal }): Promise<ShippingAddress | undefined> => {
|
||||
if (!params.shippingAddressId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const res = await this.#shippingAddressService.fetchShippingAddress(
|
||||
{
|
||||
shippingAddressId: params.shippingAddressId,
|
||||
},
|
||||
abortSignal,
|
||||
);
|
||||
|
||||
return res.result as ShippingAddress;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SelectedCustomerShippingAddressResource extends CustomerShippingAddressResource {
|
||||
#tabId = inject(TabService).activatedTabId;
|
||||
#customerMetadata = inject(CrmTabMetadataService);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
effect(() => {
|
||||
const tabId = this.#tabId();
|
||||
const shippingAddressId = tabId
|
||||
? this.#customerMetadata.selectedShippingAddressId(tabId)
|
||||
: undefined;
|
||||
this.params({ shippingAddressId });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { effect, inject, Injectable, resource, signal } from '@angular/core';
|
||||
import { CrmTabMetadataService, ShippingAddressService } from '../services';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { ShippingAddress } from '../models';
|
||||
|
||||
@Injectable()
|
||||
export class CustomerShippingAddressesResource {
|
||||
#shippingAddressService = inject(ShippingAddressService);
|
||||
|
||||
#params = signal<{
|
||||
customerId: number | undefined;
|
||||
take?: number | null;
|
||||
skip?: number | null;
|
||||
}>({
|
||||
customerId: undefined,
|
||||
});
|
||||
|
||||
params(params: { customerId?: number; take?: number | null; skip?: number | null }) {
|
||||
this.#params.update((p) => ({ ...p, ...params }));
|
||||
}
|
||||
|
||||
readonly resource = resource({
|
||||
params: () => this.#params(),
|
||||
loader: async ({ params, abortSignal }): Promise<ShippingAddress[] | undefined> => {
|
||||
if (!params.customerId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const res = await this.#shippingAddressService.fetchCustomerShippingAddresses(
|
||||
{
|
||||
customerId: params.customerId,
|
||||
take: params.take,
|
||||
skip: params.skip,
|
||||
},
|
||||
abortSignal,
|
||||
);
|
||||
|
||||
return res.result as ShippingAddress[];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SelectedCustomerShippingAddressesResource extends CustomerShippingAddressesResource {
|
||||
#tabId = inject(TabService).activatedTabId;
|
||||
#customerMetadata = inject(CrmTabMetadataService);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
effect(() => {
|
||||
const tabId = this.#tabId();
|
||||
const customerId = tabId
|
||||
? this.#customerMetadata.selectedCustomerId(tabId)
|
||||
: undefined;
|
||||
this.params({ customerId });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './country.resource';
|
||||
export * from './customer-bonus-cards.resource';
|
||||
export * from './customer-shipping-address.resource';
|
||||
export * from './customer-shipping-addresses.resource';
|
||||
export * from './customer.resource';
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const FetchCustomerShippingAddressesSchema = z.object({
|
||||
customerId: z.number().int(),
|
||||
take: z.number().int().optional().nullable(),
|
||||
skip: z.number().int().optional().nullable(),
|
||||
});
|
||||
|
||||
export type FetchCustomerShippingAddresses = z.infer<typeof FetchCustomerShippingAddressesSchema>;
|
||||
export type FetchCustomerShippingAddressesInput = z.input<typeof FetchCustomerShippingAddressesSchema>;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const FetchShippingAddressSchema = z.object({
|
||||
shippingAddressId: z.number().int(),
|
||||
});
|
||||
|
||||
export type FetchShippingAddress = z.infer<typeof FetchShippingAddressSchema>;
|
||||
export type FetchShippingAddressInput = z.input<typeof FetchShippingAddressSchema>;
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from './fetch-customer-cards.schema';
|
||||
export * from './fetch-customer-shipping-addresses.schema';
|
||||
export * from './fetch-customer.schema';
|
||||
export * from './fetch-shipping-address.schema';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './country.service';
|
||||
export * from './crm-search.service';
|
||||
export * from './crm-tab-metadata.service';
|
||||
export * from './shipping-address.service';
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { ShippingAddressService as GeneratedShippingAddressService } from '@generated/swagger/crm-api';
|
||||
import {
|
||||
FetchCustomerShippingAddressesInput,
|
||||
FetchCustomerShippingAddressesSchema,
|
||||
FetchShippingAddressInput,
|
||||
FetchShippingAddressSchema,
|
||||
} from '../schemas';
|
||||
import {
|
||||
catchResponseArgsErrorPipe,
|
||||
ListResponseArgs,
|
||||
ResponseArgs,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { ShippingAddress } from '../models';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ShippingAddressService {
|
||||
#shippingAddressService = inject(GeneratedShippingAddressService);
|
||||
#logger = logger(() => ({
|
||||
service: 'ShippingAddressService',
|
||||
}));
|
||||
|
||||
async fetchCustomerShippingAddresses(
|
||||
params: FetchCustomerShippingAddressesInput,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<ListResponseArgs<ShippingAddress>> {
|
||||
this.#logger.info('Fetching customer shipping addresses from API');
|
||||
const { customerId, take, skip } =
|
||||
FetchCustomerShippingAddressesSchema.parse(params);
|
||||
|
||||
let req$ = this.#shippingAddressService
|
||||
.ShippingAddressGetShippingAddresses({ customerId, take, skip })
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
this.#logger.debug('Successfully fetched customer shipping addresses');
|
||||
return res as ListResponseArgs<ShippingAddress>;
|
||||
} catch (error) {
|
||||
this.#logger.error('Error fetching customer shipping addresses', error);
|
||||
return {
|
||||
result: [],
|
||||
totalCount: 0,
|
||||
} as unknown as ListResponseArgs<ShippingAddress>;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchShippingAddress(
|
||||
params: FetchShippingAddressInput,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<ResponseArgs<ShippingAddress>> {
|
||||
this.#logger.info('Fetching shipping address from API');
|
||||
const { shippingAddressId } = FetchShippingAddressSchema.parse(params);
|
||||
|
||||
let req$ = this.#shippingAddressService
|
||||
.ShippingAddressGetShippingaddress(shippingAddressId)
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
this.#logger.debug('Successfully fetched shipping address');
|
||||
return res as ResponseArgs<ShippingAddress>;
|
||||
} catch (error) {
|
||||
this.#logger.error('Error fetching shipping address', error);
|
||||
return undefined as unknown as ResponseArgs<ShippingAddress>;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user