projects/apttus/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 '@apttus/elements';
@NgModule({
imports: [ProductConfigurationSummaryModule, ...]
})
export class AppModule {}
// Basic Usage
<apt-product-configuration-summary [product]="product"></apt-product-configuration-summary>
// All inputs and outputs.
<apt-product-configuration-summary
[product]="product"
[relatedTo]="lineItem"
[changes]="lineItems"
[preload]="true"
[position]="'top'"
(onProductAdd)="closeModal()"
(onNavigate)="closeModal()">
</apt-product-configuration-summary>
| 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, cacheService: CacheService, ngZone: NgZone, exceptionService: ExceptionService)
|
|||||||||||||||||||||
|
Parameters :
|
| 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 | BundleProduct
|
|
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. |
| 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 overflow-auto" *ngIf="product$ | async as product; else loading">
<!-- -->
<div class="p-0 mx-4 mt-3" *ngIf="position === 'middle'">
<div class="align-items-center border-bottom border-secondary d-flex justify-content-between pb-3">
<h5 class="modal-title font-weight-normal">
{{'COMMON.PRODUCT_CONFIGURATION' | translate}}
</h5>
<button type="button" class="close" aria-label="Close" (click)="hide()">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="bg-light d-flex justify-content-between p-3">
<div class="flex-grow-1">
<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="border-left border-secondary d-flex align-items-center">
<div class="px-4">
<div class="text-muted mb-2">
{{'COMMON.NET_PRICE' | translate}}
</div>
<h5 class="m-0">
<apt-price *ngIf="changes?.length > 0; else relatedOrProductPrice"
[record]="changes"
[bundle]="true"
[quantity]="quantity"
type="net"></apt-price>
<ng-template #relatedOrProductPrice>
<apt-price *ngIf="relatedTo; else productPrice"
[record]="relatedTo"
[quantity]="quantity"
[bundle]="true"
type="net"></apt-price>
<ng-template #productPrice>
<apt-price [record]="product"
[quantity]="quantity"
[bundle]="true"
type="net"></apt-price>
</ng-template>
</ng-template>
</h5>
</div>
<div class="d-flex align-items-center">
<div *ngIf="secondaryButton">
<button class="btn" [ngClass]="secondaryButton?.style"
(click)="secondaryButton.action(product)">{{secondaryButton.label | translate}}</button>
</div>
<div *ngIf="actionButton">
<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>
</div>
<div class="p-0 mx-4 mt-3" *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" aria-label="Close" (click)="hide()">
<span aria-hidden="true">×</span>
</button>
</div>
<div>
<div class="bg-light p-3 line-height-large">
<div class="row">
<strong class="col-7">{{product?.Name}}</strong>
<div class="col-2">{{'COMMON.QTY' | translate}}: 1</div>
<apt-price class="col-3 text-right" [record]="product?._metadata?.item" [type]="'net'"></apt-price>
</div>
<div class="d-flex justify-content-between">
<span>{{'COMMON.OPTION_TOTAL' | translate}}</span>
<apt-price [record]="optionItems" [type]="'net'"></apt-price>
</div>
<div class="d-flex border-top border-gray justify-content-between pt-3">
<strong>{{'COMMON.NET_PRICE' | translate}}</strong>
<apt-price [record]="cartItems" [type]="'net'"></apt-price>
</div>
</div>
</div>
<h5 class="font-weight-normal mt-4">
{{'COMMON.ITEMIZED_OPTIONS' | translate}}
</h5>
</div>
<div class="modal-body px-0 pt-0 mx-4">
<ng-container *ngIf="product?.OptionGroups?.length > 0 || product?.AttributeGroups?.length > 0; else empty">
<div class="h-100">
<!-- 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 attributeGroupMember of product?.AttributeGroups; let x = index; let xf = first; trackBy: trackById">
<ng-container *ngIf="attributeGroupMember?.AttributeGroup?.ProductAttributes?.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 + attributeGroupMember.Id" [attr.aria-expanded]="xf">
<strong class="ml-2">{{attributeGroupMember?.AttributeGroup?.Name}}</strong>
</button>
</div>
<div [id]="uuid + attributeGroupMember.Id" class="collapse" [class.show]="xf"
[attr.data-parent]="'#' + uuid + product?.Id">
<ng-container
*ngFor="let attribute of attributeGroupMember?.AttributeGroup?.ProductAttributes; let l = last">
<div class="pt-2 px-3" [class.border-bottom]="!l" [class.border-gray]="!l" *ngIf="product.get('item')?.AttributeValue[attribute.Field]">
<apt-output-field [record]="product?._metadata?.item?.AttributeValue" [field]="attribute.Field"
[editable]="false" 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?.Name}}</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?.Name}}</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 || isOrderLineItem || isQuoteLineItem"></ng-template>
</div>
<div class="d-flex flex-nowrap justify-content-between width-fixed mx-2">
<ng-container *ngIf="cartItem; else productPrice">
<div>
{{'COMMON.QTY' | translate}}: {{cartItem?.Quantity}}
</div>
<div class="ml-auto">
<apt-price [record]="cartItem" [quantity]="cartItem?.Quantity"></apt-price>
</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"
[quantity]="(option?.DefaultQuantity) ? (option?.DefaultQuantity) : 1"></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 attributeGroupMember 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]="'#' + option?.Id + attributeGroupMember.Id" [attr.aria-expanded]="true">
<i>{{attributeGroupMember?.AttributeGroup?.Name}}</i>
</button>
<div [id]="option?.Id + attributeGroupMember.Id" class="collapse" [class.show]="true"
[attr.data-parent]="'#' + parent + option?.Id">
<div
*ngFor="let attribute of attributeGroupMember?.AttributeGroup?.ProductAttributes; let l = last">
<div class="pt-2 px-3 border-bottom border-gray" *ngIf="cartItem?.AttributeValue && cartItem?.AttributeValue[attribute.Field]">
<apt-output-field [record]="cartItem?.AttributeValue" [field]="attribute.Field"
[editable]="false" 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;
.modal-content{
height: 100vh;
}
}
}
}
.width-fixed{
min-width: 8rem;
max-width: 8rem;
}