projects/congarevenuecloud/elements/src/lib/product-configuration-summary/product-configuration-summary.component.ts
Product configuration summary component is used to show the hierarchical view of the selected configurations for a given cart/order line item in a modal window. The view is a tree like structure containing selected attributes and values within attribute groups, selected options/attributes within option groups, nested at different levels in the hierarchy.
import { ProductConfigurationSummaryModule } from '@congarevenuecloud/elements';
@NgModule({
imports: [ProductConfigurationSummaryModule, ...]
})
export class AppModule {}
// Basic Usage
```typescript
<apt-product-configuration-summary [product]="product"></apt-product-configuration-summary>
// All inputs and outputs.
```typescript
<apt-product-configuration-summary
[product]="product"
[relatedTo]="lineItem"
[changes]="lineItems"
[preload]="true"
[position]="'top'"
(onProductAdd)="closeModal()"
(onNavigate)="closeModal()">
</apt-product-configuration-summary>
OnChanges
OnDestroy
changeDetection | ChangeDetectionStrategy.OnPush |
selector | apt-product-configuration-summary |
styleUrls | ./product-configuration-summary.component.scss |
templateUrl | ./product-configuration-summary.component.html |
Inputs |
Outputs |
constructor(productOptionService: ProductOptionService, cartService: CartService, router: Router, exceptionService: ExceptionService, aobjectservice: AObjectService, cdr: ChangeDetectorRef, storefrontService: StorefrontService, cartItemService: CartItemService)
|
|||||||||||||||||||||||||||
Parameters :
|
cart |
Type : Cart
|
Instance of Cart |
changes |
Type : Array<CartItem>
|
List of cart items that this configuration changes. |
position |
Type : "left" | "right" | "middle"
|
Default value : 'middle'
|
Sets the position. |
preload |
Type : boolean
|
Default value : false
|
Will preload the data for the configuration component before showing to speed up initial render |
product |
Type : string | Product
|
Instance of Cart or Order line item or Product to represent the data displayed on this component. |
quantity |
Type : number
|
Default value : 1
|
Quantity selected of this product used for adding to cart. |
relatedTo |
Type : CartItem
|
Related cart item. |
showActionButtons |
Type : boolean
|
Default value : true
|
Boolean to show action buttons on configuration component. |
onNavigate |
Type : EventEmitter<void>
|
Event emitter for when user navigates from summary. |
onProductAdd |
Type : EventEmitter<void>
|
Event emitter for when a product is added to cart. |
<div bsModal #summaryModal="bs-modal" class="modal fade" [ngClass]="position" tabindex="-1" role="dialog"
aria-labelledby="dialog-sizes-name1">
<div class="modal-dialog modal-lg">
<div class="modal-content" *ngIf="product$ | async as product; else loading">
<!-- -->
<div class="p-0" *ngIf="position === 'middle'">
<div class="align-items-center d-flex justify-content-between flex-row-reverse">
<button type="button" class="close close-button pull-right" aria-label="Close" (click)="hide()">
<span aria-hidden="true">×</span>
</button>
</div>
</div>
<div class="modal-body bg-white">
<h6 class="modal-title pull-left font-weight-bold" *ngIf="position === 'middle'">
{{'COMMON.PRODUCT_CONFIGURATION' | translate}}
</h6>
<div class="d-md-flex d-lg-flex justify-content-between px-1 pt-3 d-none d-sm-none" *ngIf="position === 'middle'">
<div class="flex-grow-1" id="productName">
<h4>{{product?.Name}}</h4>
<div class="text-muted" [translate]="'PRODUCT_CONFIGURATION_SUMMARY.PRODUCT_ID'"
[translateParams]="{productCode: product?.ProductCode}" *ngIf="product.ProductCode"></div>
</div>
<div class="d-flex align-items-center">
<div class="px-4">
<div class="text-muted mb-2">
{{'COMMON.NET_PRICE' | translate}}
</div>
<h5 class="m-0">
<span *ngIf="changes?.length > 0; else relatedOrProductPrice">{{cartItem?.NetPrice | localCurrency | async}}</span>
<ng-template #relatedOrProductPrice>
<span *ngIf="relatedTo; else productPrice">{{relatedTo?.NetPrice | localCurrency | async}}</span>
<ng-template #productPrice>
<apt-price [record]="product"
type="list"></apt-price>
</ng-template>
</ng-template>
</h5>
</div>
<div class="d-flex align-items-center">
<div *ngIf="secondaryButton">
<button class="border-left border-secondary btn" [ngClass]="secondaryButton?.style"
[ladda]="changeConfigurationLoader" data-style="zoom-in"
(click)="secondaryButton.action(product)">{{secondaryButton.label | translate}}</button>
</div>
<div *ngIf="actionButton && hideActionBtn">
<button class="btn ml-2 btn-raised" [ngClass]="actionButton?.style"
(click)="actionButton.action(product)" [ladda]="addLoading" data-style="zoom-in">{{actionButton.label
| translate}}</button>
</div>
</div>
</div>
</div>
<!-- Mobile view support -->
<div class="justify-content-between pt-3 d-lg-none d-md-none d-sm-block d-block" *ngIf="position === 'middle'">
<h4>{{product?.Name}}</h4>
<div class="text-muted" [translate]="'PRODUCT_CONFIGURATION_SUMMARY.PRODUCT_ID'"
[translateParams]="{productCode: product?.ProductCode}" *ngIf="product.ProductCode"></div>
</div>
<!-- Mobile view support -->
<div class="d-sm-flex d-flex justify-content-between pt-3 d-lg-none d-md-none" *ngIf="position === 'middle'">
<div class="pr-4">
<div class="text-muted mb-1">
{{'COMMON.NET_PRICE' | translate}}
</div>
<h5 class="m-0">
<span *ngIf="changes?.length > 0; else relatedOrProductPrice">{{cartItem?.NetPrice | localCurrency | async}}</span>
<ng-template #relatedOrProductPrice>
<span *ngIf="relatedTo; else productPrice">{{relatedTo?.NetPrice | localCurrency | async}}</span>
<ng-template #productPrice>
<apt-price [record]="product"
type="list"></apt-price>
</ng-template>
</ng-template>
</h5>
</div>
<div *ngIf="secondaryButton" class="pt-1">
<button class="border-left border-secondary btn btn-sm" [ngClass]="secondaryButton?.style"
[ladda]="changeConfigurationLoader" data-style="zoom-in"
(click)="secondaryButton.action(product)">{{secondaryButton.label | translate}}</button>
</div>
<div *ngIf="actionButton && hideActionBtn" class="pt-1">
<button class="btn ml-2 btn-raised btn-sm" [ngClass]="actionButton?.style"
(click)="actionButton.action(product)" [ladda]="addLoading" data-style="zoom-in">{{actionButton.label
| translate}}</button>
</div>
</div>
<div class="p-0" *ngIf="position === 'right'">
<div class="align-items-center border-bottom border-secondary d-flex pb-3">
<h5 class="modal-title font-weight-normal">
<div>{{'COMMON.PRODUCT_CONFIGURATION' | translate}}</div>
</h5>
<div *ngIf="relatedTo">
<select class="form-control ml-3 form-control-sm" id="summary-filter" name="summaryFilter"
[(ngModel)]="filter" (ngModelChange)="setProduct()">
<option [value]="'items'">{{'COMMON.ALL' | translate}}</option>
<option [value]="'changes'">{{'COMMON.CHANGES_ONLY' | translate}}</option>
</select>
</div>
<button type="button" class="close ml-auto pt-1" aria-label="Close" (click)="hide()">
<span aria-hidden="true">×</span>
</button>
</div>
<div>
<div class="bg-light p-3 line-height-large">
<div class="d-flex justify-content-between">
<strong class="col-7 pl-0">{{product?.Name}}</strong>
<div class="col-2">{{'COMMON.QTY' | translate}}: {{product?._metadata?.item?.Quantity? product?._metadata?.item?.Quantity : 1}}</div>
<span>{{product?._metadata?.item?.BaseExtendedPrice | localCurrency | async}}</span>
</div>
<div class="d-flex justify-content-between">
<span>{{'COMMON.OPTION_TOTAL' | translate}}</span>
<span>{{product?._metadata?.item?.OptionPrice | localCurrency | async}}</span>
</div>
<div class="d-flex border-top border-gray justify-content-between pt-3">
<strong>{{'COMMON.NET_PRICE' | translate}}</strong>
<span>{{product?._metadata?.item?.NetPrice | localCurrency | async}}</span>
</div>
</div>
</div>
<h5 class="font-weight-normal mt-4">
{{'COMMON.ITEMIZED_OPTIONS' | translate}}
</h5>
</div>
<ng-container *ngIf="product?.OptionGroups?.length > 0 || product?.AttributeGroups?.length > 0; else empty">
<div>
<!-- Header -->
<!-- Main Accordion -->
<div class="accordion mt-3" [id]="uuid + product?.Id"
*ngIf="product?.AttributeGroups?.length > 0 || product?.OptionGroups?.length > 0">
<!-- Top Level Attributes -->
<ng-container
*ngFor="let group of product?.AttributeGroups; let x = index; let xf = first; trackBy: trackById">
<ng-container *ngIf="group?.AttributeGroup?.AttributeGroupMembers?.length > 0">
<div class="bg-light border-top border-secondary">
<button class="btn btn-link chevron" type="button" data-toggle="collapse"
[attr.data-target]="'#' + uuid + group.Id" [attr.aria-expanded]="xf">
<strong class="ml-2">{{group?.AttributeGroup?.Name}}</strong>
</button>
</div>
<div [id]="uuid + group.Id" class="collapse" [class.show]="xf"
[attr.data-parent]="'#' + uuid + product?.Id">
<ng-container *ngFor="let member of group?.AttributeGroup?.AttributeGroupMembers; let l = last">
<div class="pt-2 px-3" [class.border-bottom]="!l" [class.border-gray]="!l"
*ngIf="product.get('item')?.AttributeValue[member.Attribute.Name]">
<apt-output-field [record]="product?._metadata?.item?.AttributeValue"
[field]="member.Attribute.Name" [displayValue]="member.Attribute.Name" [editable]="false"
[label]="member.Attribute.DisplayName" labelClass="font-italic font-weight-normal"
valueClass="font-weight-bold">
</apt-output-field>
</div>
</ng-container>
</div>
</ng-container>
</ng-container>
<!-- Top Level Options-->
<div
*ngFor="let optionGroupMember of product.OptionGroups; let f = first; let i = index; trackBy: trackById">
<ng-container *ngIf="!optionGroupMember?.IsHidden">
<div class="bg-light border-top border-secondary">
<button class="btn btn-link chevron" type="button" data-toggle="collapse"
[attr.data-target]="'#' + uuid + optionGroupMember.Id"
[attr.aria-expanded]="i + product?.AttributeGroups?.length === 0">
<strong class="ml-2">{{optionGroupMember?.OptionGroup?.Label}}</strong>
</button>
</div>
<div class="collapse" [id]="uuid + optionGroupMember.Id" [attr.data-parent]="'#' + uuid + product?.Id"
[class.show]="i + product?.AttributeGroups?.length === 0">
<div class="card-body">
<div class="accordion" [id]="'child' + uuid + optionGroupMember.Id">
<div *ngFor="let productOptionGroup of optionGroupMember?.ChildOptionGroups; trackBy: trackById"
class="mb-3">
<ng-template *ngIf="!productOptionGroup?.IsHidden"
[ngTemplateOutlet]="productOptionGroupTemplate"
[ngTemplateOutletContext]="{productOptionGroup: productOptionGroup, parent: 'child' + uuid + optionGroupMember.Id}">
</ng-template>
</div>
<div *ngFor="let option of optionGroupMember?.Options; let f = first; trackBy: trackById"
class="mb-3">
<ng-template [ngTemplateOutlet]="optionTemplate" [ngTemplateOutletContext]="{option: option
,parent: 'child' + uuid + optionGroupMember.Id
,expanded: f
, cartItem: option?.ComponentProduct?._metadata?.item}">
</ng-template>
</div>
</div>
</div>
</div>
</ng-container>
</div>
<ng-template #productOptionGroupTemplate let-productOptionGroup="productOptionGroup" let-parent="parent">
<button class="btn btn-link p-0 minus text-capitalize text-dark" data-toggle="collapse"
[attr.data-target]="'#' + parent + productOptionGroup.Id" [attr.aria-expanded]="true">
<i>{{productOptionGroup?.OptionGroup?.Label}}</i>
</button>
<div [id]="parent + productOptionGroup.Id" class="collapse" [class.show]="true"
[attr.data-parent]="'#' + parent">
<div class="my-2 ml-3"
*ngFor="let component of productOptionGroup?.Options; let f = first; trackBy: trackById">
<ng-template [ngTemplateOutlet]="optionTemplate"
[ngTemplateOutletContext]="{option: component, parent: parent + productOptionGroup.Id, expanded: f, cartItem: component?.ComponentProduct?._metadata?.item}">
</ng-template>
</div>
</div>
</ng-template>
<!-- Nested option template -->
<ng-template #optionTemplate let-option="option" let-parent="parent" let-expanded="expanded"
let-cartItem="cartItem">
<div class="d-flex justify-content-between border-bottom border-gray"
*ngIf="cartItem?.AssetStatus !== 'Cancelled'">
<div class="d-flex align-items-center text-truncate mb-1">
<button class="btn btn-link p-0 minus text-capitalize text-dark text-truncate btn-sm"
data-toggle="collapse" [attr.data-target]="'#' + parent + option?.Id"
[attr.aria-expanded]="expanded"
*ngIf="option?.ComponentProduct?.OptionGroups?.length > 0 || option?.ComponentProduct?.AttributeGroups?.length > 0; else read">
{{option?.ComponentProduct?.Name}}
</button>
<ng-template #read>
<div class="text-dark text-truncate">
{{option?.ComponentProduct?.Name}}
</div>
</ng-template>
<ng-template [ngTemplateOutlet]="statusBadge" [ngTemplateOutletContext]="{cartItem: cartItem}"
*ngIf="relatedTo?.AssetLineItem?.Id || isOrderLineItem || isQuoteLineItem"></ng-template>
</div>
<div class="d-flex flex-nowrap justify-content-between width-fixed mx-2">
<ng-container *ngIf="cartItem && cartItem?.NetPrice; else productPrice">
<div>
{{'COMMON.QTY' | translate}}: {{cartItem?.Quantity}}
</div>
<div class="ml-auto">
<ng-container *ngIf="(isOrderLineItem || isQuoteLineItem); else itemPrice">
<!-- TO DO:NetPrice should come as object right now in QuoteLineItem it's not the same (CPQ-81002) -->
<div>{{ cartItem?.NetPrice?.Value || cartItem?.NetPrice | localCurrency | async }}</div>
</ng-container>
<ng-template #itemPrice>
<span *ngIf="!(isOrderLineItem || isQuoteLineItem)">{{cartItem?.NetPrice | localCurrency | async}}</span>
</ng-template>
</div>
</ng-container>
<ng-template #productPrice>
<div>
{{'COMMON.QTY' | translate}}: {{(option?.DefaultQuantity) ? (option?.DefaultQuantity) : 1}}
</div>
<div class="ml-auto">
<apt-price [record]="option?.ComponentProduct" [type]="'list'"></apt-price>
</div>
</ng-template>
</div>
</div>
<!-- Option Accordion -->
<div [id]="parent + option?.Id" [attr.data-parent]="'#' + parent"
class="collapse pl-4 configuration accordion" [class.show]="expanded">
<div
*ngFor="let nestedGroups of option?.ComponentProduct?.AttributeGroups; let aIndex = index; let aFirst = first; trackBy: trackById"
class="mt-2">
<button class="btn btn-link p-0 minus text-capitalize text-dark" data-toggle="collapse"
[attr.data-target]="'#' + 'ac' + option?.Id + nestedGroups.Id" [attr.aria-expanded]="true">
<i>{{nestedGroups?.AttributeGroup?.Name}}</i>
</button>
<div [id]="'ac' +option?.Id + nestedGroups.Id" class="collapse" [class.show]="true"
[attr.data-parent]="'#' + parent + option?.Id">
<div *ngFor="let member of nestedGroups?.AttributeGroup?.AttributeGroupMembers; let l = last">
<div class="pt-2 px-3 border-bottom border-gray"
*ngIf="cartItem?.AttributeValue && cartItem?.AttributeValue[member?.Attribute?.Name]">
<apt-output-field [record]="cartItem?.AttributeValue" [field]="member?.Attribute.Name"
[displayValue]="member?.Attribute?.Name" [editable]="false"
[label]="member?.Attribute?.DisplayName" labelClass="font-italic font-weight-normal"
valueClass="font-weight-bold">
</apt-output-field>
</div>
</div>
</div>
</div>
<div
*ngFor="let productOptionGroup of option.ComponentProduct?.OptionGroups; let oFirst = first; trackBy: trackById"
class="mt-3">
<ng-template *ngIf="!productOptionGroup?.IsHidden" [ngTemplateOutlet]="productOptionGroupTemplate"
[ngTemplateOutletContext]="{productOptionGroup: productOptionGroup, parent: parent + option.Id}">
</ng-template>
</div>
</div>
</ng-template>
</div>
</div>
</ng-container>
<ng-template #empty>
<div class="d-flex justify-content-center flex-column align-items-center py-5 my-5">
<i class="fa fa-cog fa-5x text-primary xl text-faded"></i>
<div class="mt-4">{{'CONFIGURATION.EMPTY' | translate}}</div>
</div>
</ng-template>
</div>
</div>
<ng-template #loading>
<div class="modal-content">
<div class="modal-body">
<div class="d-flex justify-content-center py-5">
<apt-dots></apt-dots>
</div>
</div>
</div>
</ng-template>
</div>
</div>
<ng-template #statusBadge let-cartItem="cartItem">
<ng-container [ngSwitch]="cartItem?.LineStatus">
<span class="badge ml-2 badge-danger py-1" *ngSwitchCase="'Cancelled'">
{{cartItem?.LineStatus}}
</span>
<span class="badge ml-2 badge-info py-1" *ngSwitchCase="'Upgraded'">
{{cartItem?.LineStatus}}
</span>
<span class="badge ml-2 badge-warning py-1" *ngSwitchCase="'Amended'">
{{cartItem?.LineStatus}}
</span>
<span class="badge ml-2 badge-success py-1" *ngSwitchCase="'New'">
{{cartItem?.LineStatus}}
</span>
<span class="badge ml-2 badge-warning py-1" *ngSwitchCase="'Renewed'">
{{cartItem?.LineStatus}}
</span>
<span class="badge ml-2 badge-light py-1" *ngSwitchCase="'Existing'">
{{cartItem?.LineStatus}}
</span>
</ng-container>
</ng-template>
./product-configuration-summary.component.scss
:host {
font-size: small;
}
.configuration {
font-size: small;
}
.line-height-large {
line-height: 1.8rem;
}
.modal {
&.right {
.modal-content {
height: 100vh;
}
&.show {
.modal-dialog {
right: -1px;
}
}
.modal-dialog {
position: absolute;
transition: right 300ms;
margin: -1px 0 0 0;
right: -20rem;
width: 40rem;
max-width: 100vw;
top: 0;
.modal-content {
height: 100vh;
}
}
}
}
.width-fixed {
min-width: 8rem;
max-width: 8rem;
}
.option-item {
padding-left: 1.5rem;
}