Commit 05a33135 authored by Michail Tzanatos's avatar Michail Tzanatos
Browse files

Merge branch 'product-specs-characteristics-listing' into...

Merge branch 'product-specs-characteristics-listing' into 38-implement-product-specification-characteristics-listing
parents c8d2c9f8 dde467a9
Loading
Loading
Loading
Loading
Loading
+6 −1
Original line number Diff line number Diff line
@@ -10,6 +10,8 @@ import { ListProductSpecsComponent } from './p_product/admin/productCatalogManag
import { EditProductCatalogsComponent } from './p_product/admin/productCatalogManagement/edit-product-catalogs/edit-product-catalogs.component';
import { EditProductCategoriesComponent } from './p_product/admin/productCatalogManagement/edit-product-categories/edit-product-categories.component';
import { EditProductSpecsComponent } from './p_product/admin/productCatalogManagement/edit-product-specs/edit-product-specs.component';
import { EditProductSpecCharacteristicsComponent } from './p_product/admin/productCatalogManagement/edit-product-specs/edit-product-spec-characteristics/edit-product-spec-characteristics.component';
import { DeleteProductSpecCharacteristicsComponent } from './p_product/admin/productCatalogManagement/edit-product-specs/delete-product-spec-characteristics/delete-product-spec-characteristics.component';
import { DeleteProductCatalogsComponent } from './p_product/admin/productCatalogManagement/delete-product-catalogs/delete-product-catalogs.component';
import { DeleteProductCategoriesComponent } from './p_product/admin/productCatalogManagement/delete-product-categories/delete-product-categories.component';
import { DeleteProductSpecsComponent } from './p_product/admin/productCatalogManagement/delete-product-specs/delete-product-specs.component';
@@ -21,7 +23,7 @@ import { EditProductOfferingsComponent } from './p_product/admin/productCatalogM
import { PreviewMarketPlaceItemComponent } from './p_product/marketplace/preview-market-place-item/preview-market-place-item.component';
import { AssignProductOfferingsComponent } from './p_product/admin/productCatalogManagement/edit-product-categories/assign-product-offerings/assign-product-offerings.component';
import { AssignSubcategoriesComponent } from './p_product/admin/productCatalogManagement/edit-product-categories/assign-subcategories/assign-subcategories.component';

import { ConfirmCharacteristicAssignmentComponent } from './p_product/admin/productCatalogManagement/edit-product-specs/assign-service-specification/confirm-characteristic-assignment/confirm-characteristic-assignment.component';


@NgModule({
@@ -34,10 +36,13 @@ import { AssignSubcategoriesComponent } from './p_product/admin/productCatalogMa
        EditProductCatalogsComponent,
        EditProductCategoriesComponent,
        EditProductSpecsComponent,
        EditProductSpecCharacteristicsComponent,
        DeleteProductSpecCharacteristicsComponent,
        DeleteProductCatalogsComponent,
        DeleteProductCategoriesComponent,
        DeleteProductSpecsComponent,
        AssignServiceSpecificationComponent,
        ConfirmCharacteristicAssignmentComponent,
        ListProductOfferingsComponent,
        DeleteProductOfferingComponent,
        EditProductOfferingsComponent,
+9 −2
Original line number Diff line number Diff line
@@ -14,6 +14,9 @@
                <mat-autocomplete #autoComplete="matAutocomplete" (optionSelected)="selected($event)" autoActiveFirstOption>
                    <mat-option *ngFor="let spec of filteredSpecs$ | async" [value]="spec">
                        {{spec.name}} 
                        <b class="badge badge-info ml-1">
                            {{ spec['@type'] === 'CustomerFacingServiceSpecification' ? 'CFSS' : (spec['@type'] === 'ResourceFacingServiceSpecification' ? 'RFSS' : 'null') }}
                        </b>
                    </mat-option>
                </mat-autocomplete>
            </mat-form-field>
@@ -26,7 +29,11 @@
                    <th mat-header-cell *matHeaderCellDef mat-sort-header> Assigned Service Specifications </th>
                    <td mat-cell *matCellDef="let element" matTooltipClass="universal-tooltip"
                        [matTooltip]="element.description" matTooltipPosition="above">
                        <b>{{element.name}}</b> </td>
                        <b>{{element.name}}</b>
                        <b class="badge badge-info ml-1">
                            {{ element['@type'] === 'CustomerFacingServiceSpecification' ? 'CFSS' : (element['@type'] === 'ResourceFacingServiceSpecification' ? 'RFSS' : 'null') }}
                        </b> 
                    </td>
                </ng-container>

                <ng-container matColumnDef="actions">
+208 −33
Original line number Diff line number Diff line
import { Component, OnInit, Inject, ViewChild, ElementRef } from '@angular/core';
import { UntypedFormControl } from '@angular/forms';
import { MatAutocomplete, MatAutocompleteSelectedEvent, MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { Observable } from 'rxjs';
import { ToastrService } from 'ngx-toastr';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators'
import { ProductSpecification, ProductSpecificationUpdate } from 'src/app/openApis/productCatalogManagement/models';
import { ProductSpecificationService } from 'src/app/openApis/productCatalogManagement/services';
import { ServiceSpecification } from 'src/app/openApis/serviceCatalogManagement/models';
import { ServiceSpecificationService } from 'src/app/openApis/serviceCatalogManagement/services';
import { ConfirmCharacteristicAssignmentComponent } from './confirm-characteristic-assignment/confirm-characteristic-assignment.component';


@Component({
  selector: 'app-assign-service-specification',
@@ -24,7 +27,10 @@ export class AssignServiceSpecificationComponent implements OnInit {
    },
    private dialogRef: MatDialogRef<AssignServiceSpecificationComponent>,
    private productSpecService: ProductSpecificationService,
    private serviceSpecService: ServiceSpecificationService
    private serviceSpecService: ServiceSpecificationService,
    private toast: ToastrService,
    private dialog: MatDialog
    
  ) { }

  @ViewChild('specInput') specInput: ElementRef<HTMLInputElement>;  
@@ -37,44 +43,78 @@ export class AssignServiceSpecificationComponent implements OnInit {
  @ViewChild(MatSort, {static: true}) sort: MatSort;

  specInputCtrl = new UntypedFormControl();
  nonSelectedSpecs: ProductSpecification[]
  nonSelectedSpecs: ProductSpecification[] = [];
  filteredSpecs$: Observable<ProductSpecification[]>
  selectedSpecs: ProductSpecification[] = []

  // the subject holds the data when it arrives from the api
  private availableSpecsSubject = new BehaviorSubject<ProductSpecification[]>([]);


  ngOnInit(): void {
    this.listServiceSpecs()
    this.listServiceSpecs();

    // for the filtering to work when user starts typing in the input
    this.filteredSpecs$ = combineLatest([
      this.specInputCtrl.valueChanges.pipe(startWith('')),
      this.availableSpecsSubject.asObservable()
      ]).pipe(
        map(([inputValue, specs]) => {
          const filterValue = typeof inputValue === 'string' ? inputValue.toLowerCase() : '';
          return this.performFilter(filterValue, specs);
        })
      );
  }

  listServiceSpecs() {
    this.serviceSpecService.listServiceSpecification({}).subscribe(
      data => this.nonSelectedSpecs = data,
      error => console.error(error),
      () => {
        //remove self from available spec list as well as the allready assigned specs
        const initiallyAssignedSpecIDs: string[] = this.data.productSpec.serviceSpecification.map(el => el.id)
        this.nonSelectedSpecs = this.nonSelectedSpecs.filter(spec => !initiallyAssignedSpecIDs.includes(spec.id))
      data => {
        // remove the already assigned specs
        const initiallyAssignedIDs = this.data.productSpec.serviceSpecification.map(el => el.id);
        const filteredData = data.filter(spec => !initiallyAssignedIDs.includes(spec.id));
        
        this.selectedSpecs = this.data.productSpec.serviceSpecification.slice()
        this.dataSource.data = this.selectedSpecs
        this.dataSource.sort = this.sort
        // push data into subject
        this.availableSpecsSubject.next(filteredData);
        
        this.filteredSpecs$ = this.specInputCtrl.valueChanges.pipe(
          startWith(null),
          map( (spec: string | ServiceSpecification) => typeof(spec) === 'string' ? this._filter(spec) : this.nonSelectedSpecs.slice() )
        )
        this.selectedSpecs = this.data.productSpec.serviceSpecification.slice();
        this.dataSource.data = this.selectedSpecs;
        this.dataSource.sort = this.sort;
      },
      error => console.error(error)
    );
  }
    )

  private performFilter(filterValue: string, specs: ProductSpecification[]): ProductSpecification[] {
    if (!filterValue) {
      return specs;
    }
    return specs.filter(spec => {
      const name = spec.name ? spec.name.toLowerCase() : '';
      return name.indexOf(filterValue) !== -1;
    });
  }

  selected(event: MatAutocompleteSelectedEvent): void {
    this.selectedSpecs.push(event.option.value);
    this.dataSource.data = this.selectedSpecs

    this.nonSelectedSpecs = this.nonSelectedSpecs.filter(el => el.name != event.option.value.name)

    const selectedLiteSpec = event.option.value;
    //retrieve full service specs details, @type and spec characteristics once a service spec is selected
    this.specInput.nativeElement.value = '';
    this.specInputCtrl.setValue(null);

    this.serviceSpecService.retrieveServiceSpecification({ 
      id: selectedLiteSpec.id 
    }).subscribe(
      (fullSpec: ServiceSpecification) => {

        this.selectedSpecs.push(fullSpec);
        this.dataSource.data = this.selectedSpecs;
        this.nonSelectedSpecs = this.nonSelectedSpecs.filter(
          el => el.name != selectedLiteSpec.name
        );
      },
      (error) => {
        this.toast.error('Failed to fetch full service specification details', error);
      }
    );
  }

  private _filter(value: string): ProductSpecification[] {
@@ -103,16 +143,151 @@ export class AssignServiceSpecificationComponent implements OnInit {

  confirmAssignment() {
    
    const updateRelationshipsObj: ProductSpecificationUpdate = {
      serviceSpecification: this.selectedSpecs.map(spec =>{ return {id: spec.id, name: spec.name}})
    const hasConfigurableCFSS = this.selectedSpecs.some(spec => {
      const s = spec as any;
      const type = s['@type'] || 'ServiceSpecification';
      const chars = s.serviceSpecCharacteristic || [];
      const hasConfigurable = chars.some(c => c.configurable === true);
      
      return type === 'CustomerFacingServiceSpecification' && hasConfigurable;
    });

    if (hasConfigurableCFSS) {
      // Open the dialog to ask the user
      const dialogRef = this.dialog.open(ConfirmCharacteristicAssignmentComponent, {
        width: '600px',
        disableClose: true
      });

      dialogRef.afterClosed().subscribe(result => {
        console.log(result);
        this.executeAssignment(result);
      });
    }
    else {
      this.executeAssignment(false);
    }
  }

    this.productSpecService.patchProductSpecification({id: this.data.productSpec.id, productSpecification: updateRelationshipsObj}).subscribe(
  executeAssignment(importCharacteristics: boolean) {
    const serviceRefs: any[] = [];
    
    let tempCharacteristics = [...(this.data.productSpec.productSpecCharacteristic || [])];

    this.selectedSpecs.forEach(spec => {
      const sourceSpec = spec as any;
      const type = sourceSpec['@type'] || 'ServiceSpecification';

      serviceRefs.push({
        id: sourceSpec.id,
        name: sourceSpec.name,
        '@type': type
      });

      // If user said YES, we manually prepare the chars to add
      if (importCharacteristics) {
        const serviceChars = sourceSpec.serviceSpecCharacteristic || [];
        const configurableChars = serviceChars.filter(char => char.configurable === true);

        configurableChars.forEach(sourceChar => {
          const cleanValues = (sourceChar.serviceSpecCharacteristicValue || []).map(val => ({
            valueType: val.valueType,
            isDefault: val.isDefault,
            value: val.value,
            unitOfMeasure: val.unitOfMeasure,
            valueFrom: val.valueFrom,
            valueTo: val.valueTo,
            rangeInterval: val.rangeInterval,
            alias: val.alias
          }));

          const newCharObj = {
            name: sourceChar.name,
            description: sourceChar.description,
            valueType: sourceChar.valueType,
            configurable: sourceChar.configurable,
            minCardinality: sourceChar.minCardinality,
            maxCardinality: sourceChar.maxCardinality,
            productSpecCharacteristicValue: cleanValues,
            '@type': 'ProductSpecCharacteristic'
          };

          // if characteristic already exists by name, overwrite it anyway
          const existingCharIndex = tempCharacteristics.findIndex(c => c.name === sourceChar.name);
          if (existingCharIndex > -1) {
            tempCharacteristics[existingCharIndex] = { ...newCharObj, uuid: tempCharacteristics[existingCharIndex].uuid };
          } else {
            tempCharacteristics.push(newCharObj);
          }
        });
      }
    });

    const payloadStep1: ProductSpecificationUpdate = {
      serviceSpecification: serviceRefs,
      productSpecCharacteristic: tempCharacteristics
    };

    this.productSpecService.patchProductSpecification({
      id: this.data.productSpec.id,
      productSpecification: payloadStep1
    }).subscribe(
      (updatedSpec) => { 
        
        // If user said NO, and we have CFSS, we need to check if backend added them 
        // and remove them (patching the list without them)
        if (!importCharacteristics) {
          this.removeUnwantedCharacteristics(updatedSpec);
        } else {
          this.toast.success('Product Specification updated successfully');
          this.dialogRef.close('updated');
        }
      },
      error => {
        console.error('Update failed', error);
        this.toast.error('Failed to update Product Specification');
      }
    );
  }

  removeUnwantedCharacteristics(spec: ProductSpecification) {
    // identify characteristics to remove (from CFSS) if user clicks no
    const unwantedNames: string[] = [];
    
    this.selectedSpecs.forEach(s => {
      const sourceSpec = s as any;
      if (sourceSpec['@type'] === 'CustomerFacingServiceSpecification') {
         const chars = sourceSpec.serviceSpecCharacteristic || [];
         chars.filter(c => c.configurable).forEach(c => unwantedNames.push(c.name));
      }
    });

    const currentChars = spec.productSpecCharacteristic || [];
    const newSpecCharacteristicArray = currentChars.filter(c => !unwantedNames.includes(c.name));

    // If there is nothing to change, just close
    if (newSpecCharacteristicArray.length === currentChars.length) {
      this.dialogRef.close('updated');
      return;
    }

    const updateCharacteristicObj: ProductSpecificationUpdate = {
      productSpecCharacteristic: newSpecCharacteristicArray
    }

    this.productSpecService.patchProductSpecification({
      id: spec.id, 
      productSpecification: updateCharacteristicObj
    }).subscribe(
      data => {},
      error => console.error(error),
      () => {this.dialogRef.close('updated')}
      error => {
        this.toast.warning('Service assigned, but failed to clean up auto-assigned characteristics.');
        this.dialogRef.close('updated'); 
      },
      () => {
        this.dialogRef.close('updated');
      }
    )
  }


}
+13 −0
Original line number Diff line number Diff line
<h2 mat-dialog-title>Import Characteristics?</h2>
<mat-dialog-content class="mat-typography">
    <p>
    The selected <b>CustomerFacingServiceSpecification</b>(s) contain configurable characteristics.
    </p>
    <p>
    Do you want to automatically add these as <b>Product Characteristics</b> to the Product Specification?
    </p>
</mat-dialog-content>
<mat-dialog-actions align="end">
    <button (click)="onYes()" class="btn btn-success ml-2">Yes, Import Characteristics</button>
    <button (click)="onNo()" class="btn btn-primary ml-2">No, just link Service Specification</button>
</mat-dialog-actions>
 No newline at end of file
+22 −0
Original line number Diff line number Diff line
import { Component } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';

@Component({
  selector: 'app-confirm-characteristic-assignment',
  templateUrl: './confirm-characteristic-assignment.component.html',
  styles: []
})
export class ConfirmCharacteristicAssignmentComponent {

  constructor(
    public dialogRef: MatDialogRef<ConfirmCharacteristicAssignmentComponent>
  ) {}

  onNo(): void {
    this.dialogRef.close(false);
  }

  onYes(): void {
    this.dialogRef.close(true);
  }
}
 No newline at end of file
Loading