Files
ISA-Frontend/apps/isa-app/src/domain/checkout/store/domain-checkout.reducer.ts
Lorenz Hilpert 68f50b911d Merged PR 1991: feat(navigation): implement title management and enhance tab system
 feat(navigation): implement title management and enhance tab system

This commit introduces a comprehensive title management system and extends
the tab functionality with subtitle support, improving navigation clarity
and user experience across the application.

Key changes:

Title Management System:
- Add @isa/common/title-management library with dual approach:
  - IsaTitleStrategy for route-based static titles
  - usePageTitle() for component-based dynamic titles
- Implement TitleRegistryService for nested component hierarchies
- Automatic ISA prefix addition and TabService integration
- Comprehensive test coverage (1,158 lines of tests)

Tab System Enhancement:
- Add subtitle field to tab schema for additional context
- Update TabService API (addTab, patchTab) to support subtitles
- Extend Zod schemas with subtitle validation
- Update documentation with usage examples

Routing Modernization:
- Consolidate route guards using ActivateProcessIdWithConfigKeyGuard
- Replace 4+ specific guards with generic config-key-based approach
- Add title attributes to 100+ routes across all modules
- Remove deprecated ProcessIdGuard in favor of ActivateProcessIdGuard

Code Cleanup:
- Remove deprecated preview component and related routes
- Clean up unused imports and exports
- Update TypeScript path aliases

Dependencies:
- Update package.json and package-lock.json
- Add @isa/common/title-management to tsconfig path mappings

Refs: #5351, #5418, #5419, #5420
2025-12-02 12:38:28 +00:00

312 lines
9.0 KiB
TypeScript

import { createReducer, on } from '@ngrx/store';
import {
initialCheckoutState,
storeCheckoutAdapter,
} from './domain-checkout.state';
import * as DomainCheckoutActions from './domain-checkout.actions';
import { Dictionary } from '@ngrx/entity';
import { CheckoutEntity } from './defs/checkout.entity';
import { isNullOrUndefined } from '@utils/common';
const _domainCheckoutReducer = createReducer(
initialCheckoutState,
on(
DomainCheckoutActions.setShoppingCart,
(s, { processId, shoppingCart }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
const addedShoppingCartItems =
shoppingCart?.items
?.filter(
(item) =>
!entity.shoppingCart?.items?.find((i) => i.id === item.id),
)
?.map((item) => item.data) ?? [];
entity.shoppingCart = shoppingCart;
entity.itemAvailabilityTimestamp = entity.itemAvailabilityTimestamp
? { ...entity.itemAvailabilityTimestamp }
: {};
const now = Date.now();
for (const shoppingCartItem of addedShoppingCartItems) {
if (shoppingCartItem.features?.orderType) {
entity.itemAvailabilityTimestamp[
`${shoppingCartItem.id}_${shoppingCartItem.features.orderType}`
] = now;
}
}
return storeCheckoutAdapter.setOne(entity, s);
},
),
on(
DomainCheckoutActions.setShoppingCartByShoppingCartId,
(s, { shoppingCartId, shoppingCart }) => {
let entity = getCheckoutEntityByShoppingCartId({
shoppingCartId,
entities: s.entities,
});
if (!entity) {
// No entity found for this shoppingCartId, cannot update
return s;
}
entity = { ...entity, shoppingCart };
return storeCheckoutAdapter.setOne(entity, s);
},
),
on(DomainCheckoutActions.setCheckout, (s, { processId, checkout }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
entity.checkout = checkout;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(
DomainCheckoutActions.setBuyerCommunicationDetails,
(s, { processId, email, mobile }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
const communicationDetails = { ...entity.buyer.communicationDetails };
communicationDetails.email = email || communicationDetails.email;
communicationDetails.mobile = mobile || communicationDetails.mobile;
entity.buyer = {
...entity.buyer,
communicationDetails,
};
return storeCheckoutAdapter.setOne(entity, s);
},
),
on(
DomainCheckoutActions.setNotificationChannels,
(s, { processId, notificationChannels }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
return storeCheckoutAdapter.setOne(
{ ...entity, notificationChannels },
s,
);
},
),
on(
DomainCheckoutActions.setCheckoutDestination,
(s, { processId, destination }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
entity.checkout = {
...entity.checkout,
destinations: entity.checkout.destinations.map((dest) => {
if (dest.id === destination.id) {
return { ...dest, ...destination };
}
return { ...dest };
}),
};
return storeCheckoutAdapter.setOne(entity, s);
},
),
on(
DomainCheckoutActions.setShippingAddress,
(s, { processId, shippingAddress }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
entity.shippingAddress = shippingAddress;
return storeCheckoutAdapter.setOne(entity, s);
},
),
on(DomainCheckoutActions.setBuyer, (s, { processId, buyer }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
entity.buyer = buyer;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.setPayer, (s, { processId, payer }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
entity.payer = payer;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(
DomainCheckoutActions.setSpecialComment,
(s, { processId, agentComment }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
entity.specialComment = agentComment;
return storeCheckoutAdapter.setOne(entity, s);
},
),
on(DomainCheckoutActions.removeCheckoutWithProcessId, (s, { processId }) => {
return storeCheckoutAdapter.removeOne(processId, s);
}),
on(DomainCheckoutActions.setOrders, (s, { orders }) => ({
...s,
orders: [...s.orders, ...orders],
})),
on(DomainCheckoutActions.updateOrderItem, (s, { item }) => {
const orders = [...s.orders];
const orderToUpdate = orders?.find((order) =>
order.items?.find((i) => i.id === item?.id),
);
const orderToUpdateIndex = orders?.indexOf(orderToUpdate);
const orderItemToUpdate = orderToUpdate?.items?.find(
(i) => i.id === item?.id,
);
const orderItemToUpdateIndex =
orderToUpdate?.items?.indexOf(orderItemToUpdate);
const items = [...(orderToUpdate?.items ?? [])];
items[orderItemToUpdateIndex] = item;
orders[orderToUpdateIndex] = {
...orderToUpdate,
items: [...items],
};
return { ...s, orders: [...orders] };
}),
on(DomainCheckoutActions.removeAllOrders, (s) => ({
...s,
orders: [],
})),
on(DomainCheckoutActions.setOlaError, (s, { processId, olaErrorIds }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
entity.olaErrorIds = olaErrorIds;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.setCustomer, (s, { processId, customer }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
entity.customer = customer;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistory,
(s, { processId, shoppingCartItemId, availability }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp
? { ...entity?.itemAvailabilityTimestamp }
: {};
const item = entity?.shoppingCart?.items?.find(
(i) => i.id === shoppingCartItemId,
)?.data;
if (!item?.features?.orderType) return s;
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] =
Date.now();
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
return storeCheckoutAdapter.setOne(entity, s);
},
),
on(
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistoryByShoppingCartId,
(s, { shoppingCartId, shoppingCartItemId, availability }) => {
const entity = getCheckoutEntityByShoppingCartId({
shoppingCartId,
entities: s.entities,
});
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp
? { ...entity?.itemAvailabilityTimestamp }
: {};
const item = entity?.shoppingCart?.items?.find(
(i) => i.id === shoppingCartItemId,
)?.data;
if (!item?.features?.orderType) return s;
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] =
Date.now();
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
return storeCheckoutAdapter.setOne(entity, s);
},
),
);
export function domainCheckoutReducer(state, action) {
return _domainCheckoutReducer(state, action);
}
function getOrCreateCheckoutEntity({
entities,
processId,
}: {
entities: Dictionary<CheckoutEntity>;
processId: number;
}): CheckoutEntity {
const entity = entities[processId];
if (isNullOrUndefined(entity)) {
return {
processId,
checkout: undefined,
shoppingCart: undefined,
shippingAddress: undefined,
orders: [],
payer: undefined,
buyer: undefined,
specialComment: '',
notificationChannels: 0,
olaErrorIds: [],
customer: undefined,
// availabilityHistory: [],
itemAvailabilityTimestamp: {},
};
}
return { ...entity };
}
function getCheckoutEntityByShoppingCartId({
entities,
shoppingCartId,
}: {
entities: Dictionary<CheckoutEntity>;
shoppingCartId: number;
}): CheckoutEntity {
return Object.values(entities).find(
(entity) => entity.shoppingCart?.id === shoppingCartId,
);
}