tango-app-ui-store-builder 1.2.12 → 1.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,11 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Component, Injectable, inject, Input, ViewChild, EventEmitter, ChangeDetectionStrategy, HostListener, Output, forwardRef, Pipe, ViewChildren, ChangeDetectorRef, NgModule } from '@angular/core';
2
+ import { Component, Injectable, inject, ViewChild, Input, EventEmitter, Output, HostListener, ChangeDetectionStrategy, forwardRef, Pipe, ViewChildren, ChangeDetectorRef, NgModule } from '@angular/core';
3
3
  import * as i2 from '@angular/router';
4
4
  import { NavigationEnd, RouterLink, RouterOutlet, RouterModule } from '@angular/router';
5
5
  import * as i2$1 from 'tango-app-ui-global';
6
6
  import { GlobalStateService } from 'tango-app-ui-global';
7
7
  import * as i5 from '@angular/common';
8
- import { DatePipe, CommonModule, NgClass, NgStyle, NgFor, NgIf, TitleCasePipe, UpperCasePipe } from '@angular/common';
8
+ import { CommonModule, DatePipe, NgClass, NgStyle, NgFor, NgIf, TitleCasePipe, UpperCasePipe } from '@angular/common';
9
9
  import * as i1$2 from '@angular/forms';
10
10
  import { FormControl, FormGroup, FormsModule, FormArray, NG_VALUE_ACCESSOR, Validators, ReactiveFormsModule } from '@angular/forms';
11
11
  import * as i7 from '@angular/cdk/drag-drop';
@@ -586,6 +586,9 @@ class StoreBuilderService {
586
586
  runAlocation(data) {
587
587
  return this.http.post(`${this.storeBuilderApiUrl}/onboardPlano/runAlocation`, data);
588
588
  }
589
+ runLooksAllocation(data) {
590
+ return this.http.post(`${this.storeBuilderApiUrl}/onboardPlano/runLooksAllocation`, data);
591
+ }
589
592
  getIvmConversionAlias() {
590
593
  return this.http.get(`${this.storeBuilderApiUrl}/onboardPlano/ivmConversionAlias`);
591
594
  }
@@ -728,6 +731,11 @@ class StoreBuilderService {
728
731
  getLookPlanoCollectionDetails(collectionId) {
729
732
  return this.http.get(`${this.storeBuilderApiUrl}/lookPlanoCollection/getCollectionDetails`, { params: { collectionId } });
730
733
  }
734
+ /** MBQ bucket / Brand / Fixture Header dropdown options + combinations,
735
+ * seeded into planostaticdatas (type='lookComboOptions') from the sheet. */
736
+ getLookComboOptions(clientId) {
737
+ return this.http.get(`${this.storeBuilderApiUrl}/lookPlanoCollection/comboOptions`, { params: { clientId } });
738
+ }
731
739
  createLookPlanoCollection(data) {
732
740
  return this.http.post(`${this.storeBuilderApiUrl}/lookPlanoCollection/createCollection`, data);
733
741
  }
@@ -64921,7 +64929,27 @@ class OnboardStorePlanoComponent {
64921
64929
  initial: true,
64922
64930
  sort: false,
64923
64931
  };
64924
- const response = await firstValueFrom(this.apiService.runAlocation(payload));
64932
+ // Prefer the Looks-driven allocation when an Active Look Plano
64933
+ // Collection exists for this client; the backend returns 404 with a
64934
+ // "No active Look Plano Collection" message when none is configured,
64935
+ // in which case we fall back to the legacy spreadsheet-driven flow.
64936
+ let response;
64937
+ let usedLooks = false;
64938
+ try {
64939
+ response = await firstValueFrom(this.apiService.runLooksAllocation(payload));
64940
+ usedLooks = true;
64941
+ }
64942
+ catch (looksErr) {
64943
+ const status = looksErr?.status ?? looksErr?.error?.status;
64944
+ const msg = looksErr?.error?.message ||
64945
+ looksErr?.error?.error ||
64946
+ looksErr?.message ||
64947
+ "";
64948
+ const noActiveCollection = status === 404 || /no active look plano collection/i.test(msg);
64949
+ if (!noActiveCollection)
64950
+ throw looksErr;
64951
+ response = await firstValueFrom(this.apiService.runAlocation(payload));
64952
+ }
64925
64953
  if (response && response.data) {
64926
64954
  this.floorData.layoutPolygon =
64927
64955
  response.data.layoutPolygon || this.floorData.layoutPolygon;
@@ -64937,7 +64965,16 @@ class OnboardStorePlanoComponent {
64937
64965
  this.renderFloor(false);
64938
64966
  this.togglePanel("left", true);
64939
64967
  this.togglePanel("right", false);
64940
- this.toastService.getSuccessToast("Allocation logic applied successfully");
64968
+ if (usedLooks) {
64969
+ const stats = response.data.looksAllocation || {};
64970
+ const name = stats.looksCollectionName
64971
+ ? ` (${stats.looksCollectionName})`
64972
+ : "";
64973
+ this.toastService.getSuccessToast(`Looks allocation applied${name}: ${stats.matched ?? 0} matched, ${stats.unmatched ?? 0} unmatched`);
64974
+ }
64975
+ else {
64976
+ this.toastService.getSuccessToast("Allocation logic applied successfully");
64977
+ }
64941
64978
  }
64942
64979
  this.modalService.dismissAll();
64943
64980
  this.isButtonLoading = false;
@@ -67264,11 +67301,11 @@ class DeleteFloorComponent {
67264
67301
  this.destroy$.complete();
67265
67302
  }
67266
67303
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DeleteFloorComponent, deps: [{ token: StoreBuilderService }, { token: i2$1.GlobalStateService }, { token: i1$1.NgbModal }], target: i0.ɵɵFactoryTarget.Component });
67267
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: DeleteFloorComponent, selector: "lib-delete-floor", ngImport: i0, template: "<section class=\"delete-floor p-3\">\r\n <div class=\"card p-4\" style=\"max-width: 720px;\">\r\n <h5 class=\"mb-1\">Delete Floor</h5>\r\n <p class=\"text-muted small mb-3\">\r\n Removes a floor and every row that references it \u2014 fixtures, shelves, task\r\n compliance, processed tasks, comments, and revisions. If this is the only\r\n floor, the entire planogram is removed. The store entry is never touched.\r\n </p>\r\n\r\n <!-- Store -->\r\n <div class=\"mb-3\">\r\n <lib-reactive-select\r\n label=\"Store\"\r\n [idField]=\"'storeId'\"\r\n [nameField]=\"'storeName'\"\r\n [data]=\"storeOptions\"\r\n [search]=\"true\"\r\n [ngModel]=\"selectedStoreId\"\r\n (ngModelChange)=\"onStoreSelected($event)\">\r\n </lib-reactive-select>\r\n <div class=\"text-muted small mt-1\" *ngIf=\"isLoadingStores\">Loading stores...</div>\r\n </div>\r\n\r\n <!-- Floor -->\r\n <div class=\"mb-3\" *ngIf=\"selectedStoreId\">\r\n <ng-container *ngIf=\"isLoadingFloors; else floorPicker\">\r\n <div class=\"text-muted small\">Loading floors...</div>\r\n </ng-container>\r\n <ng-template #floorPicker>\r\n <ng-container *ngIf=\"floors.length > 0; else noFloors\">\r\n <lib-reactive-select\r\n label=\"Floor\"\r\n [idField]=\"'_id'\"\r\n [nameField]=\"'displayName'\"\r\n [data]=\"floors\"\r\n [search]=\"true\"\r\n [ngModel]=\"selectedFloorId\"\r\n (ngModelChange)=\"onFloorSelected($event)\">\r\n </lib-reactive-select>\r\n </ng-container>\r\n <ng-template #noFloors>\r\n <div class=\"text-muted small\">No floors found for this store.</div>\r\n </ng-template>\r\n </ng-template>\r\n </div>\r\n\r\n <!-- Status + cascade preview -->\r\n <ng-container *ngIf=\"selectedFloorId\">\r\n <ng-container *ngIf=\"isLoadingImpact; else impactBlock\">\r\n <div class=\"text-muted small\">Loading floor details...</div>\r\n </ng-container>\r\n <ng-template #impactBlock>\r\n <ng-container *ngIf=\"impact\">\r\n <div class=\"mb-3 p-3 rounded info-block\">\r\n <div class=\"row gx-3 gy-2 small\">\r\n <div class=\"col-6\">\r\n <span class=\"text-muted d-block\">Floor status</span>\r\n <span class=\"fw-semibold\">{{ formatStatus(impact.floor.status) }}</span>\r\n </div>\r\n <div class=\"col-6\">\r\n <span class=\"text-muted d-block\">Planogram status</span>\r\n <span class=\"fw-semibold\">{{ formatStatus(impact.planogram.status) }}</span>\r\n </div>\r\n <div class=\"col-6\">\r\n <span class=\"text-muted d-block\">Plano approved</span>\r\n <span class=\"fw-semibold\">{{ formatBool(impact.floor.isPlanoApproved) }}</span>\r\n </div>\r\n <div class=\"col-6\">\r\n <span class=\"text-muted d-block\">Verification done</span>\r\n <span class=\"fw-semibold\">{{ formatBool(impact.floor.verificationStatus) }}</span>\r\n </div>\r\n <div class=\"col-6\">\r\n <span class=\"text-muted d-block\">Merch rollout done</span>\r\n <span class=\"fw-semibold\">{{ formatBool(impact.floor.merchRolloutStatus) }}</span>\r\n </div>\r\n <div class=\"col-6\">\r\n <span class=\"text-muted d-block\">VM rollout done</span>\r\n <span class=\"fw-semibold\">{{ formatBool(impact.floor.vmRolloutStatus) }}</span>\r\n </div>\r\n <div class=\"col-12\">\r\n <span class=\"text-muted d-block\">Total floors in this planogram</span>\r\n <span class=\"fw-semibold\">{{ impact.totalFloors }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"mb-3 p-3 rounded cascade-block\">\r\n <div class=\"fw-semibold mb-2\">Will delete</div>\r\n <ul class=\"small mb-0 ps-3\">\r\n <li>1 floor (storelayouts)</li>\r\n <li>{{ impact.cascade.storefixtures }} fixture(s) (storefixtures)</li>\r\n <li>{{ impact.cascade.fixtureshelves }} shelf row(s) (fixtureshelves)</li>\r\n <li>{{ impact.cascade.planotaskcompliances }} task compliance row(s) (planotaskcompliances)</li>\r\n <li>{{ impact.cascade.processedtasks }} processed task(s) (processedtasks)</li>\r\n <li>{{ impact.cascade.planoglobalcomments }} global comment(s) (planoglobalcomments)</li>\r\n <li>{{ impact.cascade.planorevisions }} revision(s) (planorevisions)</li>\r\n </ul>\r\n <div class=\"text-muted small mt-2\">Total downstream rows: {{ totalCascadeRows }}</div>\r\n </div>\r\n\r\n <div class=\"alert alert-warning small\" *ngIf=\"impact.isLastFloor\">\r\n <strong>This is the only floor in this planogram.</strong>\r\n Deleting it will also remove the planogram entry \u2014 the plano will\r\n disappear completely. The store record itself will NOT be deleted.\r\n </div>\r\n </ng-container>\r\n </ng-template>\r\n </ng-container>\r\n\r\n <!-- Submit -->\r\n <button class=\"btn btn-danger\" (click)=\"onSubmit()\" [disabled]=\"!canSubmit\">\r\n {{ isSubmitting ? 'Deleting...' : 'Delete Floor' }}\r\n </button>\r\n\r\n <div class=\"mt-3 alert\" *ngIf=\"message\"\r\n [class.alert-success]=\"messageType === 'success'\"\r\n [class.alert-danger]=\"messageType === 'error'\"\r\n style=\"white-space: pre-line;\">\r\n {{ message }}\r\n </div>\r\n </div>\r\n</section>\r\n", styles: [".delete-floor .info-block{background-color:#f9fafb;border:1px solid #e4e7ec}.delete-floor .cascade-block{background-color:#fff7ed;border:1px solid #fed7aa}\n"], dependencies: [{ kind: "component", type: ReactiveSelectComponent, selector: "lib-reactive-select", inputs: ["idField", "nameField", "subTextField", "searchField", "label", "data", "action", "search", "prefix", "actionLabel", "disabled"], outputs: ["actionClick"] }, { kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
67304
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: DeleteFloorComponent, selector: "lib-delete-floor", ngImport: i0, template: "<section class=\"delete-floor p-3\">\n <div class=\"card p-4\" style=\"max-width: 720px;\">\n <h5 class=\"mb-1\">Delete Floor</h5>\n <p class=\"text-muted small mb-3\">\n Removes a floor and every row that references it \u2014 fixtures, shelves, task\n compliance, processed tasks, comments, and revisions. If this is the only\n floor, the entire planogram is removed. The store entry is never touched.\n </p>\n\n <!-- Store -->\n <div class=\"mb-3\">\n <lib-reactive-select\n label=\"Store\"\n [idField]=\"'storeId'\"\n [nameField]=\"'storeName'\"\n [data]=\"storeOptions\"\n [search]=\"true\"\n [ngModel]=\"selectedStoreId\"\n (ngModelChange)=\"onStoreSelected($event)\">\n </lib-reactive-select>\n <div class=\"text-muted small mt-1\" *ngIf=\"isLoadingStores\">Loading stores...</div>\n </div>\n\n <!-- Floor -->\n <div class=\"mb-3\" *ngIf=\"selectedStoreId\">\n <ng-container *ngIf=\"isLoadingFloors; else floorPicker\">\n <div class=\"text-muted small\">Loading floors...</div>\n </ng-container>\n <ng-template #floorPicker>\n <ng-container *ngIf=\"floors.length > 0; else noFloors\">\n <lib-reactive-select\n label=\"Floor\"\n [idField]=\"'_id'\"\n [nameField]=\"'displayName'\"\n [data]=\"floors\"\n [search]=\"true\"\n [ngModel]=\"selectedFloorId\"\n (ngModelChange)=\"onFloorSelected($event)\">\n </lib-reactive-select>\n </ng-container>\n <ng-template #noFloors>\n <div class=\"text-muted small\">No floors found for this store.</div>\n </ng-template>\n </ng-template>\n </div>\n\n <!-- Status + cascade preview -->\n <ng-container *ngIf=\"selectedFloorId\">\n <ng-container *ngIf=\"isLoadingImpact; else impactBlock\">\n <div class=\"text-muted small\">Loading floor details...</div>\n </ng-container>\n <ng-template #impactBlock>\n <ng-container *ngIf=\"impact\">\n <div class=\"mb-3 p-3 rounded info-block\">\n <div class=\"row gx-3 gy-2 small\">\n <div class=\"col-6\">\n <span class=\"text-muted d-block\">Floor status</span>\n <span class=\"fw-semibold\">{{ formatStatus(impact.floor.status) }}</span>\n </div>\n <div class=\"col-6\">\n <span class=\"text-muted d-block\">Planogram status</span>\n <span class=\"fw-semibold\">{{ formatStatus(impact.planogram.status) }}</span>\n </div>\n <div class=\"col-6\">\n <span class=\"text-muted d-block\">Plano approved</span>\n <span class=\"fw-semibold\">{{ formatBool(impact.floor.isPlanoApproved) }}</span>\n </div>\n <div class=\"col-6\">\n <span class=\"text-muted d-block\">Verification done</span>\n <span class=\"fw-semibold\">{{ formatBool(impact.floor.verificationStatus) }}</span>\n </div>\n <div class=\"col-6\">\n <span class=\"text-muted d-block\">Merch rollout done</span>\n <span class=\"fw-semibold\">{{ formatBool(impact.floor.merchRolloutStatus) }}</span>\n </div>\n <div class=\"col-6\">\n <span class=\"text-muted d-block\">VM rollout done</span>\n <span class=\"fw-semibold\">{{ formatBool(impact.floor.vmRolloutStatus) }}</span>\n </div>\n <div class=\"col-12\">\n <span class=\"text-muted d-block\">Total floors in this planogram</span>\n <span class=\"fw-semibold\">{{ impact.totalFloors }}</span>\n </div>\n </div>\n </div>\n\n <div class=\"mb-3 p-3 rounded cascade-block\">\n <div class=\"fw-semibold mb-2\">Will delete</div>\n <ul class=\"small mb-0 ps-3\">\n <li>1 floor (storelayouts)</li>\n <li>{{ impact.cascade.storefixtures }} fixture(s) (storefixtures)</li>\n <li>{{ impact.cascade.fixtureshelves }} shelf row(s) (fixtureshelves)</li>\n <li>{{ impact.cascade.planotaskcompliances }} task compliance row(s) (planotaskcompliances)</li>\n <li>{{ impact.cascade.processedtasks }} processed task(s) (processedtasks)</li>\n <li>{{ impact.cascade.planoglobalcomments }} global comment(s) (planoglobalcomments)</li>\n <li>{{ impact.cascade.planorevisions }} revision(s) (planorevisions)</li>\n </ul>\n <div class=\"text-muted small mt-2\">Total downstream rows: {{ totalCascadeRows }}</div>\n </div>\n\n <div class=\"alert alert-warning small\" *ngIf=\"impact.isLastFloor\">\n <strong>This is the only floor in this planogram.</strong>\n Deleting it will also remove the planogram entry \u2014 the plano will\n disappear completely. The store record itself will NOT be deleted.\n </div>\n </ng-container>\n </ng-template>\n </ng-container>\n\n <!-- Submit -->\n <button class=\"btn btn-danger\" (click)=\"onSubmit()\" [disabled]=\"!canSubmit\">\n {{ isSubmitting ? 'Deleting...' : 'Delete Floor' }}\n </button>\n\n <div class=\"mt-3 alert\" *ngIf=\"message\"\n [class.alert-success]=\"messageType === 'success'\"\n [class.alert-danger]=\"messageType === 'error'\"\n style=\"white-space: pre-line;\">\n {{ message }}\n </div>\n </div>\n</section>\n", styles: [".delete-floor .info-block{background-color:#f9fafb;border:1px solid #e4e7ec}.delete-floor .cascade-block{background-color:#fff7ed;border:1px solid #fed7aa}\n"], dependencies: [{ kind: "component", type: ReactiveSelectComponent, selector: "lib-reactive-select", inputs: ["idField", "nameField", "subTextField", "searchField", "label", "data", "action", "search", "prefix", "actionLabel", "disabled"], outputs: ["actionClick"] }, { kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
67268
67305
  }
67269
67306
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DeleteFloorComponent, decorators: [{
67270
67307
  type: Component,
67271
- args: [{ selector: "lib-delete-floor", template: "<section class=\"delete-floor p-3\">\r\n <div class=\"card p-4\" style=\"max-width: 720px;\">\r\n <h5 class=\"mb-1\">Delete Floor</h5>\r\n <p class=\"text-muted small mb-3\">\r\n Removes a floor and every row that references it \u2014 fixtures, shelves, task\r\n compliance, processed tasks, comments, and revisions. If this is the only\r\n floor, the entire planogram is removed. The store entry is never touched.\r\n </p>\r\n\r\n <!-- Store -->\r\n <div class=\"mb-3\">\r\n <lib-reactive-select\r\n label=\"Store\"\r\n [idField]=\"'storeId'\"\r\n [nameField]=\"'storeName'\"\r\n [data]=\"storeOptions\"\r\n [search]=\"true\"\r\n [ngModel]=\"selectedStoreId\"\r\n (ngModelChange)=\"onStoreSelected($event)\">\r\n </lib-reactive-select>\r\n <div class=\"text-muted small mt-1\" *ngIf=\"isLoadingStores\">Loading stores...</div>\r\n </div>\r\n\r\n <!-- Floor -->\r\n <div class=\"mb-3\" *ngIf=\"selectedStoreId\">\r\n <ng-container *ngIf=\"isLoadingFloors; else floorPicker\">\r\n <div class=\"text-muted small\">Loading floors...</div>\r\n </ng-container>\r\n <ng-template #floorPicker>\r\n <ng-container *ngIf=\"floors.length > 0; else noFloors\">\r\n <lib-reactive-select\r\n label=\"Floor\"\r\n [idField]=\"'_id'\"\r\n [nameField]=\"'displayName'\"\r\n [data]=\"floors\"\r\n [search]=\"true\"\r\n [ngModel]=\"selectedFloorId\"\r\n (ngModelChange)=\"onFloorSelected($event)\">\r\n </lib-reactive-select>\r\n </ng-container>\r\n <ng-template #noFloors>\r\n <div class=\"text-muted small\">No floors found for this store.</div>\r\n </ng-template>\r\n </ng-template>\r\n </div>\r\n\r\n <!-- Status + cascade preview -->\r\n <ng-container *ngIf=\"selectedFloorId\">\r\n <ng-container *ngIf=\"isLoadingImpact; else impactBlock\">\r\n <div class=\"text-muted small\">Loading floor details...</div>\r\n </ng-container>\r\n <ng-template #impactBlock>\r\n <ng-container *ngIf=\"impact\">\r\n <div class=\"mb-3 p-3 rounded info-block\">\r\n <div class=\"row gx-3 gy-2 small\">\r\n <div class=\"col-6\">\r\n <span class=\"text-muted d-block\">Floor status</span>\r\n <span class=\"fw-semibold\">{{ formatStatus(impact.floor.status) }}</span>\r\n </div>\r\n <div class=\"col-6\">\r\n <span class=\"text-muted d-block\">Planogram status</span>\r\n <span class=\"fw-semibold\">{{ formatStatus(impact.planogram.status) }}</span>\r\n </div>\r\n <div class=\"col-6\">\r\n <span class=\"text-muted d-block\">Plano approved</span>\r\n <span class=\"fw-semibold\">{{ formatBool(impact.floor.isPlanoApproved) }}</span>\r\n </div>\r\n <div class=\"col-6\">\r\n <span class=\"text-muted d-block\">Verification done</span>\r\n <span class=\"fw-semibold\">{{ formatBool(impact.floor.verificationStatus) }}</span>\r\n </div>\r\n <div class=\"col-6\">\r\n <span class=\"text-muted d-block\">Merch rollout done</span>\r\n <span class=\"fw-semibold\">{{ formatBool(impact.floor.merchRolloutStatus) }}</span>\r\n </div>\r\n <div class=\"col-6\">\r\n <span class=\"text-muted d-block\">VM rollout done</span>\r\n <span class=\"fw-semibold\">{{ formatBool(impact.floor.vmRolloutStatus) }}</span>\r\n </div>\r\n <div class=\"col-12\">\r\n <span class=\"text-muted d-block\">Total floors in this planogram</span>\r\n <span class=\"fw-semibold\">{{ impact.totalFloors }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"mb-3 p-3 rounded cascade-block\">\r\n <div class=\"fw-semibold mb-2\">Will delete</div>\r\n <ul class=\"small mb-0 ps-3\">\r\n <li>1 floor (storelayouts)</li>\r\n <li>{{ impact.cascade.storefixtures }} fixture(s) (storefixtures)</li>\r\n <li>{{ impact.cascade.fixtureshelves }} shelf row(s) (fixtureshelves)</li>\r\n <li>{{ impact.cascade.planotaskcompliances }} task compliance row(s) (planotaskcompliances)</li>\r\n <li>{{ impact.cascade.processedtasks }} processed task(s) (processedtasks)</li>\r\n <li>{{ impact.cascade.planoglobalcomments }} global comment(s) (planoglobalcomments)</li>\r\n <li>{{ impact.cascade.planorevisions }} revision(s) (planorevisions)</li>\r\n </ul>\r\n <div class=\"text-muted small mt-2\">Total downstream rows: {{ totalCascadeRows }}</div>\r\n </div>\r\n\r\n <div class=\"alert alert-warning small\" *ngIf=\"impact.isLastFloor\">\r\n <strong>This is the only floor in this planogram.</strong>\r\n Deleting it will also remove the planogram entry \u2014 the plano will\r\n disappear completely. The store record itself will NOT be deleted.\r\n </div>\r\n </ng-container>\r\n </ng-template>\r\n </ng-container>\r\n\r\n <!-- Submit -->\r\n <button class=\"btn btn-danger\" (click)=\"onSubmit()\" [disabled]=\"!canSubmit\">\r\n {{ isSubmitting ? 'Deleting...' : 'Delete Floor' }}\r\n </button>\r\n\r\n <div class=\"mt-3 alert\" *ngIf=\"message\"\r\n [class.alert-success]=\"messageType === 'success'\"\r\n [class.alert-danger]=\"messageType === 'error'\"\r\n style=\"white-space: pre-line;\">\r\n {{ message }}\r\n </div>\r\n </div>\r\n</section>\r\n", styles: [".delete-floor .info-block{background-color:#f9fafb;border:1px solid #e4e7ec}.delete-floor .cascade-block{background-color:#fff7ed;border:1px solid #fed7aa}\n"] }]
67308
+ args: [{ selector: "lib-delete-floor", template: "<section class=\"delete-floor p-3\">\n <div class=\"card p-4\" style=\"max-width: 720px;\">\n <h5 class=\"mb-1\">Delete Floor</h5>\n <p class=\"text-muted small mb-3\">\n Removes a floor and every row that references it \u2014 fixtures, shelves, task\n compliance, processed tasks, comments, and revisions. If this is the only\n floor, the entire planogram is removed. The store entry is never touched.\n </p>\n\n <!-- Store -->\n <div class=\"mb-3\">\n <lib-reactive-select\n label=\"Store\"\n [idField]=\"'storeId'\"\n [nameField]=\"'storeName'\"\n [data]=\"storeOptions\"\n [search]=\"true\"\n [ngModel]=\"selectedStoreId\"\n (ngModelChange)=\"onStoreSelected($event)\">\n </lib-reactive-select>\n <div class=\"text-muted small mt-1\" *ngIf=\"isLoadingStores\">Loading stores...</div>\n </div>\n\n <!-- Floor -->\n <div class=\"mb-3\" *ngIf=\"selectedStoreId\">\n <ng-container *ngIf=\"isLoadingFloors; else floorPicker\">\n <div class=\"text-muted small\">Loading floors...</div>\n </ng-container>\n <ng-template #floorPicker>\n <ng-container *ngIf=\"floors.length > 0; else noFloors\">\n <lib-reactive-select\n label=\"Floor\"\n [idField]=\"'_id'\"\n [nameField]=\"'displayName'\"\n [data]=\"floors\"\n [search]=\"true\"\n [ngModel]=\"selectedFloorId\"\n (ngModelChange)=\"onFloorSelected($event)\">\n </lib-reactive-select>\n </ng-container>\n <ng-template #noFloors>\n <div class=\"text-muted small\">No floors found for this store.</div>\n </ng-template>\n </ng-template>\n </div>\n\n <!-- Status + cascade preview -->\n <ng-container *ngIf=\"selectedFloorId\">\n <ng-container *ngIf=\"isLoadingImpact; else impactBlock\">\n <div class=\"text-muted small\">Loading floor details...</div>\n </ng-container>\n <ng-template #impactBlock>\n <ng-container *ngIf=\"impact\">\n <div class=\"mb-3 p-3 rounded info-block\">\n <div class=\"row gx-3 gy-2 small\">\n <div class=\"col-6\">\n <span class=\"text-muted d-block\">Floor status</span>\n <span class=\"fw-semibold\">{{ formatStatus(impact.floor.status) }}</span>\n </div>\n <div class=\"col-6\">\n <span class=\"text-muted d-block\">Planogram status</span>\n <span class=\"fw-semibold\">{{ formatStatus(impact.planogram.status) }}</span>\n </div>\n <div class=\"col-6\">\n <span class=\"text-muted d-block\">Plano approved</span>\n <span class=\"fw-semibold\">{{ formatBool(impact.floor.isPlanoApproved) }}</span>\n </div>\n <div class=\"col-6\">\n <span class=\"text-muted d-block\">Verification done</span>\n <span class=\"fw-semibold\">{{ formatBool(impact.floor.verificationStatus) }}</span>\n </div>\n <div class=\"col-6\">\n <span class=\"text-muted d-block\">Merch rollout done</span>\n <span class=\"fw-semibold\">{{ formatBool(impact.floor.merchRolloutStatus) }}</span>\n </div>\n <div class=\"col-6\">\n <span class=\"text-muted d-block\">VM rollout done</span>\n <span class=\"fw-semibold\">{{ formatBool(impact.floor.vmRolloutStatus) }}</span>\n </div>\n <div class=\"col-12\">\n <span class=\"text-muted d-block\">Total floors in this planogram</span>\n <span class=\"fw-semibold\">{{ impact.totalFloors }}</span>\n </div>\n </div>\n </div>\n\n <div class=\"mb-3 p-3 rounded cascade-block\">\n <div class=\"fw-semibold mb-2\">Will delete</div>\n <ul class=\"small mb-0 ps-3\">\n <li>1 floor (storelayouts)</li>\n <li>{{ impact.cascade.storefixtures }} fixture(s) (storefixtures)</li>\n <li>{{ impact.cascade.fixtureshelves }} shelf row(s) (fixtureshelves)</li>\n <li>{{ impact.cascade.planotaskcompliances }} task compliance row(s) (planotaskcompliances)</li>\n <li>{{ impact.cascade.processedtasks }} processed task(s) (processedtasks)</li>\n <li>{{ impact.cascade.planoglobalcomments }} global comment(s) (planoglobalcomments)</li>\n <li>{{ impact.cascade.planorevisions }} revision(s) (planorevisions)</li>\n </ul>\n <div class=\"text-muted small mt-2\">Total downstream rows: {{ totalCascadeRows }}</div>\n </div>\n\n <div class=\"alert alert-warning small\" *ngIf=\"impact.isLastFloor\">\n <strong>This is the only floor in this planogram.</strong>\n Deleting it will also remove the planogram entry \u2014 the plano will\n disappear completely. The store record itself will NOT be deleted.\n </div>\n </ng-container>\n </ng-template>\n </ng-container>\n\n <!-- Submit -->\n <button class=\"btn btn-danger\" (click)=\"onSubmit()\" [disabled]=\"!canSubmit\">\n {{ isSubmitting ? 'Deleting...' : 'Delete Floor' }}\n </button>\n\n <div class=\"mt-3 alert\" *ngIf=\"message\"\n [class.alert-success]=\"messageType === 'success'\"\n [class.alert-danger]=\"messageType === 'error'\"\n style=\"white-space: pre-line;\">\n {{ message }}\n </div>\n </div>\n</section>\n", styles: [".delete-floor .info-block{background-color:#f9fafb;border:1px solid #e4e7ec}.delete-floor .cascade-block{background-color:#fff7ed;border:1px solid #fed7aa}\n"] }]
67272
67309
  }], ctorParameters: () => [{ type: StoreBuilderService }, { type: i2$1.GlobalStateService }, { type: i1$1.NgbModal }] });
67273
67310
 
67274
67311
  class UpdateFixtureTypeComponent {
@@ -68885,11 +68922,11 @@ class LookPlanoCollectionComponent {
68885
68922
  this.destroy$.complete();
68886
68923
  }
68887
68924
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LookPlanoCollectionComponent, deps: [{ token: i2.Router }, { token: i1$1.NgbModal }, { token: i2$1.GlobalStateService }, { token: StoreBuilderService }, { token: i4.ToastService }, { token: i2$1.PageInfoService }], target: i0.ɵɵFactoryTarget.Component });
68888
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: LookPlanoCollectionComponent, selector: "lib-look-plano-collection", ngImport: i0, template: "<section class=\"looks-wrapper\">\r\n <div class=\"row mx-0 my-5\">\r\n <div class=\"card p-5 col-md-12\">\r\n <div class=\"d-flex justify-content-between align-items-center mb-5\">\r\n <div>\r\n <h4>Looks</h4>\r\n <span>{{ totalItems }} - Total Look Collections</span>\r\n </div>\r\n <div class=\"d-flex align-items-center gap-2\">\r\n <div style=\"position: relative\">\r\n <input\r\n type=\"text\"\r\n [(ngModel)]=\"searchTerm\"\r\n (change)=\"onSearch()\"\r\n style=\"padding-left: 3rem; padding-right: 3rem\"\r\n class=\"form-control\"\r\n name=\"search\"\r\n autocomplete=\"off\"\r\n placeholder=\"Search by name\"\r\n />\r\n <span class=\"search-icon\">\r\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" fill=\"none\">\r\n <circle cx=\"8\" cy=\"8\" r=\"6\" stroke=\"currentColor\" stroke-width=\"2\" />\r\n <line x1=\"13\" y1=\"13\" x2=\"16\" y2=\"16\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" />\r\n </svg>\r\n </span>\r\n <button *ngIf=\"searchTerm\" type=\"button\" aria-label=\"Clear\" (click)=\"searchTerm = ''; onSearch()\" class=\"clear-search\">\r\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\">\r\n <path d=\"M9 9L15 15\" stroke=\"#667085\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path>\r\n <path d=\"M15 9L9 15\" stroke=\"#667085\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path>\r\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"#667085\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></circle>\r\n </svg>\r\n </button>\r\n </div>\r\n <button type=\"button\" class=\"btn btn-primary text-nowrap\" (click)=\"createNew()\">+ New Look Collection</button>\r\n </div>\r\n </div>\r\n\r\n <!-- Loading -->\r\n <div *ngIf=\"loading\" class=\"text-center text-muted py-10\">Loading\u2026</div>\r\n\r\n <!-- Empty state -->\r\n <div *ngIf=\"!loading && items.length === 0\" class=\"text-center text-muted py-10\">\r\n <div class=\"mb-3\">No Look collections yet.</div>\r\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"createNew()\">Create your first Look collection</button>\r\n </div>\r\n\r\n <!-- Table -->\r\n <div class=\"table-responsive\" *ngIf=\"!loading && items.length > 0\">\r\n <table class=\"table table-row-dashed align-middle gs-0 gy-3\">\r\n <thead>\r\n <tr class=\"fw-bolder text-muted text-uppercase fs-7\">\r\n <th>Name</th>\r\n <th>Description</th>\r\n <th class=\"text-end\">Looks</th>\r\n <th>Created By</th>\r\n <th>Created At</th>\r\n <th class=\"text-end\">Action</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n <tr *ngFor=\"let item of items; trackBy: trackById\">\r\n <td>\r\n <a class=\"text-dark fw-bold text-hover-primary cursor-pointer\" (click)=\"edit(item)\">{{ item.name }}</a>\r\n </td>\r\n <td>{{ item.description || '\u2014' }}</td>\r\n <td class=\"text-end\">{{ item.looksCount || 0 }}</td>\r\n <td>{{ item.createdByName || '\u2014' }}</td>\r\n <td>{{ item.createdAt | date: 'mediumDate' }}</td>\r\n <td class=\"text-end\">\r\n <button class=\"btn btn-sm btn-light me-2\" (click)=\"edit(item)\">Edit</button>\r\n <button class=\"btn btn-sm btn-light me-2\" (click)=\"duplicate(item)\">Duplicate</button>\r\n <button class=\"btn btn-sm btn-light-danger\" (click)=\"confirmDelete(item)\">Delete</button>\r\n </td>\r\n </tr>\r\n </tbody>\r\n </table>\r\n </div>\r\n </div>\r\n </div>\r\n</section>\r\n", styles: [".looks-wrapper .search-icon{position:absolute;top:50%;left:1rem;transform:translateY(-50%);color:#667085;pointer-events:none}.looks-wrapper .clear-search{position:absolute;top:50%;right:.75rem;transform:translateY(-50%);background:transparent;border:0;padding:0;line-height:0}.looks-wrapper .cursor-pointer{cursor:pointer}\n"], dependencies: [{ kind: "directive", type: i5.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: i5.DatePipe, name: "date" }] });
68925
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: LookPlanoCollectionComponent, selector: "lib-look-plano-collection", ngImport: i0, template: "<section class=\"looks-wrapper\">\r\n <div class=\"row mx-0 my-5\">\r\n <div class=\"card p-5 col-md-12\">\r\n <div class=\"d-flex justify-content-between align-items-center mb-5\">\r\n <div>\r\n <h4>Looks</h4>\r\n <span>{{ totalItems }} - Total Look Collections</span>\r\n </div>\r\n <div class=\"d-flex align-items-center gap-2\">\r\n <div style=\"position: relative\">\r\n <input\r\n type=\"text\"\r\n [(ngModel)]=\"searchTerm\"\r\n (change)=\"onSearch()\"\r\n style=\"padding-left: 3rem; padding-right: 3rem\"\r\n class=\"form-control\"\r\n name=\"search\"\r\n autocomplete=\"off\"\r\n placeholder=\"Search by name\"\r\n />\r\n <span class=\"search-icon\">\r\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" fill=\"none\">\r\n <circle cx=\"8\" cy=\"8\" r=\"6\" stroke=\"currentColor\" stroke-width=\"2\" />\r\n <line x1=\"13\" y1=\"13\" x2=\"16\" y2=\"16\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" />\r\n </svg>\r\n </span>\r\n <button *ngIf=\"searchTerm\" type=\"button\" aria-label=\"Clear\" (click)=\"searchTerm = ''; onSearch()\" class=\"clear-search\">\r\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\">\r\n <path d=\"M9 9L15 15\" stroke=\"#667085\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path>\r\n <path d=\"M15 9L9 15\" stroke=\"#667085\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path>\r\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"#667085\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></circle>\r\n </svg>\r\n </button>\r\n </div>\r\n <button type=\"button\" class=\"btn btn-primary text-nowrap\" (click)=\"createNew()\">+ New Look Collection</button>\r\n </div>\r\n </div>\r\n\r\n <!-- Loading -->\r\n <div *ngIf=\"loading\" class=\"text-center text-muted py-10\">Loading\u2026</div>\r\n\r\n <!-- Empty state -->\r\n <div *ngIf=\"!loading && items.length === 0\" class=\"text-center text-muted py-10\">\r\n <div class=\"mb-3\">No Look collections yet.</div>\r\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"createNew()\">Create your first Look collection</button>\r\n </div>\r\n\r\n <!-- Table -->\r\n <div class=\"table-responsive\" *ngIf=\"!loading && items.length > 0\">\r\n <table class=\"table table-row-dashed align-middle gs-0 gy-3\">\r\n <thead>\r\n <tr class=\"fw-bolder text-muted text-uppercase fs-7\">\r\n <th>Name</th>\r\n <th>Description</th>\r\n <th class=\"text-end\">Looks</th>\r\n <th>Created By</th>\r\n <th>Created At</th>\r\n <th class=\"text-end\">Action</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n <tr *ngFor=\"let item of items; trackBy: trackById\">\r\n <td>\r\n <a class=\"text-dark fw-bold text-hover-primary cursor-pointer\" (click)=\"edit(item)\">{{ item.name }}</a>\r\n </td>\r\n <td>{{ item.description || '\u2014' }}</td>\r\n <td class=\"text-end\">{{ item.looksCount || 0 }}</td>\r\n <td>{{ item.createdByName || '\u2014' }}</td>\r\n <td>{{ item.createdAt | date: 'mediumDate' }}</td>\r\n <td class=\"text-end\">\r\n <button type=\"button\" class=\"btn btn-sm btn-light-primary me-2\" (click)=\"edit(item)\">Edit</button>\r\n <button type=\"button\" class=\"btn btn-sm btn-light me-2\" (click)=\"duplicate(item)\">Duplicate</button>\r\n <button type=\"button\" class=\"btn btn-sm btn-light-danger\" (click)=\"confirmDelete(item)\">Delete</button>\r\n </td>\r\n </tr>\r\n </tbody>\r\n </table>\r\n </div>\r\n </div>\r\n </div>\r\n</section>\r\n", styles: [".looks-wrapper .search-icon{position:absolute;top:50%;left:1rem;transform:translateY(-50%);color:#667085;pointer-events:none}.looks-wrapper .clear-search{position:absolute;top:50%;right:.75rem;transform:translateY(-50%);background:transparent;border:0;padding:0;line-height:0}.looks-wrapper .table-responsive{min-height:420px}.looks-wrapper .cursor-pointer{cursor:pointer}\n"], dependencies: [{ kind: "directive", type: i5.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: i5.DatePipe, name: "date" }] });
68889
68926
  }
68890
68927
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LookPlanoCollectionComponent, decorators: [{
68891
68928
  type: Component,
68892
- args: [{ selector: 'lib-look-plano-collection', template: "<section class=\"looks-wrapper\">\r\n <div class=\"row mx-0 my-5\">\r\n <div class=\"card p-5 col-md-12\">\r\n <div class=\"d-flex justify-content-between align-items-center mb-5\">\r\n <div>\r\n <h4>Looks</h4>\r\n <span>{{ totalItems }} - Total Look Collections</span>\r\n </div>\r\n <div class=\"d-flex align-items-center gap-2\">\r\n <div style=\"position: relative\">\r\n <input\r\n type=\"text\"\r\n [(ngModel)]=\"searchTerm\"\r\n (change)=\"onSearch()\"\r\n style=\"padding-left: 3rem; padding-right: 3rem\"\r\n class=\"form-control\"\r\n name=\"search\"\r\n autocomplete=\"off\"\r\n placeholder=\"Search by name\"\r\n />\r\n <span class=\"search-icon\">\r\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" fill=\"none\">\r\n <circle cx=\"8\" cy=\"8\" r=\"6\" stroke=\"currentColor\" stroke-width=\"2\" />\r\n <line x1=\"13\" y1=\"13\" x2=\"16\" y2=\"16\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" />\r\n </svg>\r\n </span>\r\n <button *ngIf=\"searchTerm\" type=\"button\" aria-label=\"Clear\" (click)=\"searchTerm = ''; onSearch()\" class=\"clear-search\">\r\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\">\r\n <path d=\"M9 9L15 15\" stroke=\"#667085\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path>\r\n <path d=\"M15 9L9 15\" stroke=\"#667085\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path>\r\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"#667085\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></circle>\r\n </svg>\r\n </button>\r\n </div>\r\n <button type=\"button\" class=\"btn btn-primary text-nowrap\" (click)=\"createNew()\">+ New Look Collection</button>\r\n </div>\r\n </div>\r\n\r\n <!-- Loading -->\r\n <div *ngIf=\"loading\" class=\"text-center text-muted py-10\">Loading\u2026</div>\r\n\r\n <!-- Empty state -->\r\n <div *ngIf=\"!loading && items.length === 0\" class=\"text-center text-muted py-10\">\r\n <div class=\"mb-3\">No Look collections yet.</div>\r\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"createNew()\">Create your first Look collection</button>\r\n </div>\r\n\r\n <!-- Table -->\r\n <div class=\"table-responsive\" *ngIf=\"!loading && items.length > 0\">\r\n <table class=\"table table-row-dashed align-middle gs-0 gy-3\">\r\n <thead>\r\n <tr class=\"fw-bolder text-muted text-uppercase fs-7\">\r\n <th>Name</th>\r\n <th>Description</th>\r\n <th class=\"text-end\">Looks</th>\r\n <th>Created By</th>\r\n <th>Created At</th>\r\n <th class=\"text-end\">Action</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n <tr *ngFor=\"let item of items; trackBy: trackById\">\r\n <td>\r\n <a class=\"text-dark fw-bold text-hover-primary cursor-pointer\" (click)=\"edit(item)\">{{ item.name }}</a>\r\n </td>\r\n <td>{{ item.description || '\u2014' }}</td>\r\n <td class=\"text-end\">{{ item.looksCount || 0 }}</td>\r\n <td>{{ item.createdByName || '\u2014' }}</td>\r\n <td>{{ item.createdAt | date: 'mediumDate' }}</td>\r\n <td class=\"text-end\">\r\n <button class=\"btn btn-sm btn-light me-2\" (click)=\"edit(item)\">Edit</button>\r\n <button class=\"btn btn-sm btn-light me-2\" (click)=\"duplicate(item)\">Duplicate</button>\r\n <button class=\"btn btn-sm btn-light-danger\" (click)=\"confirmDelete(item)\">Delete</button>\r\n </td>\r\n </tr>\r\n </tbody>\r\n </table>\r\n </div>\r\n </div>\r\n </div>\r\n</section>\r\n", styles: [".looks-wrapper .search-icon{position:absolute;top:50%;left:1rem;transform:translateY(-50%);color:#667085;pointer-events:none}.looks-wrapper .clear-search{position:absolute;top:50%;right:.75rem;transform:translateY(-50%);background:transparent;border:0;padding:0;line-height:0}.looks-wrapper .cursor-pointer{cursor:pointer}\n"] }]
68929
+ args: [{ selector: 'lib-look-plano-collection', template: "<section class=\"looks-wrapper\">\r\n <div class=\"row mx-0 my-5\">\r\n <div class=\"card p-5 col-md-12\">\r\n <div class=\"d-flex justify-content-between align-items-center mb-5\">\r\n <div>\r\n <h4>Looks</h4>\r\n <span>{{ totalItems }} - Total Look Collections</span>\r\n </div>\r\n <div class=\"d-flex align-items-center gap-2\">\r\n <div style=\"position: relative\">\r\n <input\r\n type=\"text\"\r\n [(ngModel)]=\"searchTerm\"\r\n (change)=\"onSearch()\"\r\n style=\"padding-left: 3rem; padding-right: 3rem\"\r\n class=\"form-control\"\r\n name=\"search\"\r\n autocomplete=\"off\"\r\n placeholder=\"Search by name\"\r\n />\r\n <span class=\"search-icon\">\r\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" fill=\"none\">\r\n <circle cx=\"8\" cy=\"8\" r=\"6\" stroke=\"currentColor\" stroke-width=\"2\" />\r\n <line x1=\"13\" y1=\"13\" x2=\"16\" y2=\"16\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" />\r\n </svg>\r\n </span>\r\n <button *ngIf=\"searchTerm\" type=\"button\" aria-label=\"Clear\" (click)=\"searchTerm = ''; onSearch()\" class=\"clear-search\">\r\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\">\r\n <path d=\"M9 9L15 15\" stroke=\"#667085\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path>\r\n <path d=\"M15 9L9 15\" stroke=\"#667085\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path>\r\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"#667085\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></circle>\r\n </svg>\r\n </button>\r\n </div>\r\n <button type=\"button\" class=\"btn btn-primary text-nowrap\" (click)=\"createNew()\">+ New Look Collection</button>\r\n </div>\r\n </div>\r\n\r\n <!-- Loading -->\r\n <div *ngIf=\"loading\" class=\"text-center text-muted py-10\">Loading\u2026</div>\r\n\r\n <!-- Empty state -->\r\n <div *ngIf=\"!loading && items.length === 0\" class=\"text-center text-muted py-10\">\r\n <div class=\"mb-3\">No Look collections yet.</div>\r\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"createNew()\">Create your first Look collection</button>\r\n </div>\r\n\r\n <!-- Table -->\r\n <div class=\"table-responsive\" *ngIf=\"!loading && items.length > 0\">\r\n <table class=\"table table-row-dashed align-middle gs-0 gy-3\">\r\n <thead>\r\n <tr class=\"fw-bolder text-muted text-uppercase fs-7\">\r\n <th>Name</th>\r\n <th>Description</th>\r\n <th class=\"text-end\">Looks</th>\r\n <th>Created By</th>\r\n <th>Created At</th>\r\n <th class=\"text-end\">Action</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n <tr *ngFor=\"let item of items; trackBy: trackById\">\r\n <td>\r\n <a class=\"text-dark fw-bold text-hover-primary cursor-pointer\" (click)=\"edit(item)\">{{ item.name }}</a>\r\n </td>\r\n <td>{{ item.description || '\u2014' }}</td>\r\n <td class=\"text-end\">{{ item.looksCount || 0 }}</td>\r\n <td>{{ item.createdByName || '\u2014' }}</td>\r\n <td>{{ item.createdAt | date: 'mediumDate' }}</td>\r\n <td class=\"text-end\">\r\n <button type=\"button\" class=\"btn btn-sm btn-light-primary me-2\" (click)=\"edit(item)\">Edit</button>\r\n <button type=\"button\" class=\"btn btn-sm btn-light me-2\" (click)=\"duplicate(item)\">Duplicate</button>\r\n <button type=\"button\" class=\"btn btn-sm btn-light-danger\" (click)=\"confirmDelete(item)\">Delete</button>\r\n </td>\r\n </tr>\r\n </tbody>\r\n </table>\r\n </div>\r\n </div>\r\n </div>\r\n</section>\r\n", styles: [".looks-wrapper .search-icon{position:absolute;top:50%;left:1rem;transform:translateY(-50%);color:#667085;pointer-events:none}.looks-wrapper .clear-search{position:absolute;top:50%;right:.75rem;transform:translateY(-50%);background:transparent;border:0;padding:0;line-height:0}.looks-wrapper .table-responsive{min-height:420px}.looks-wrapper .cursor-pointer{cursor:pointer}\n"] }]
68893
68930
  }], ctorParameters: () => [{ type: i2.Router }, { type: i1$1.NgbModal }, { type: i2$1.GlobalStateService }, { type: StoreBuilderService }, { type: i4.ToastService }, { type: i2$1.PageInfoService }] });
68894
68931
 
68895
68932
  class SearchableSelectComponent {
@@ -69271,6 +69308,20 @@ class LookPlanoCollectionFormComponent {
69271
69308
  { id: 'eurocenter', name: 'Floor' },
69272
69309
  ];
69273
69310
  fixtureLibraryItems = [];
69311
+ // MBQ bucket / Brand / Fixture Header dropdown options, seeded from the
69312
+ // sheet into planostaticdatas (type='lookComboOptions'). The form fields
69313
+ // are searchable selects so the user picks a known value rather than typing.
69314
+ // All three are flat lists of every distinct value present in the sheet —
69315
+ // they are NOT cross-filtered, so e.g. the Brand dropdown always offers all
69316
+ // brands regardless of which MBQ bucket the look uses.
69317
+ mbqBucketItems = [];
69318
+ brandItems = [];
69319
+ fixtureHeaderItems = [];
69320
+ // Raw payload from the comboOptions endpoint, kept so we can rebuild the
69321
+ // option lists by merging it with the values already present on the loaded
69322
+ // collection (so the dropdowns are never empty even if the endpoint is
69323
+ // unavailable or hasn't loaded yet).
69324
+ comboRaw = { mbqBuckets: [], brands: [], fixtureHeaders: [], combinations: [] };
69274
69325
  // Dropdowns inside placement rows.
69275
69326
  vmTypeOptions = [];
69276
69327
  vmArtworkOptions = [];
@@ -69393,6 +69444,57 @@ class LookPlanoCollectionFormComponent {
69393
69444
  }
69394
69445
  ctrl.setValue(current);
69395
69446
  }
69447
+ /**
69448
+ * Initial vmConfig coords for a freshly-added VM entry, derived from the
69449
+ * vmType and the target library's shelf layout. Mirrors the wall-layout
69450
+ * rules in `runLooksAllocation` (onboard.controller.js:buildWallVmEntry)
69451
+ * so what the form pre-positions matches what the backend allocator would
69452
+ * place there.
69453
+ *
69454
+ * Top Masking -> first shelf, stretch (top strip of fixture)
69455
+ * Mid 1 / Mid -> first mid shelf, left
69456
+ * Mid 2 -> last mid shelf, right
69457
+ * Mid 3 -> middle mid shelf (or last), stretch
69458
+ * Bottom Slanter -> last shelf, stretch (bottom strip)
69459
+ *
69460
+ * Anything else falls back to the previous neutral default (top, no zone)
69461
+ * so behaviour doesn't change for unknown labels.
69462
+ */
69463
+ initialVmLayout(vmType, shelfConfig) {
69464
+ const norm = String(vmType ?? '').trim().toLowerCase();
69465
+ const shelves = Array.isArray(shelfConfig)
69466
+ ? shelfConfig.slice().sort((a, b) => (a?.shelfNumber || 0) - (b?.shelfNumber || 0))
69467
+ : [];
69468
+ const inZone = (z) => {
69469
+ const target = z.toLowerCase();
69470
+ return shelves.filter((s) => String(s?.zone ?? '').trim().toLowerCase() === target);
69471
+ };
69472
+ const firstShelf = shelves[0]?.shelfNumber || 1;
69473
+ const lastShelf = shelves[shelves.length - 1]?.shelfNumber || firstShelf;
69474
+ if (norm === 'top masking' || norm === 'top') {
69475
+ return { startYPosition: firstShelf, endYPosition: firstShelf, xZone: 'stretch', yZone: 'stretch' };
69476
+ }
69477
+ if (norm === 'mid 1' || norm === 'mid') {
69478
+ const m = inZone('Mid');
69479
+ const sn = m[0]?.shelfNumber || firstShelf;
69480
+ return { startYPosition: sn, endYPosition: sn, xZone: 'left', yZone: 'stretch' };
69481
+ }
69482
+ if (norm === 'mid 2') {
69483
+ const m = inZone('Mid');
69484
+ const sn = m[m.length - 1]?.shelfNumber || firstShelf;
69485
+ return { startYPosition: sn, endYPosition: sn, xZone: 'right', yZone: 'stretch' };
69486
+ }
69487
+ if (norm === 'mid 3') {
69488
+ const m = inZone('Mid');
69489
+ const target = m.length >= 3 ? m[Math.floor(m.length / 2)] : m[m.length - 1];
69490
+ const sn = target?.shelfNumber || firstShelf;
69491
+ return { startYPosition: sn, endYPosition: sn, xZone: 'stretch', yZone: 'stretch' };
69492
+ }
69493
+ if (norm === 'bottom slanter') {
69494
+ return { startYPosition: lastShelf, endYPosition: lastShelf, xZone: 'stretch', yZone: 'stretch' };
69495
+ }
69496
+ return { startYPosition: 1, endYPosition: 1, xZone: '', yZone: '' };
69497
+ }
69396
69498
  /** Resolve a VM placement's artwork NAME (+ vmType) to its planovm _id. */
69397
69499
  resolveVmId(vmName, vmType) {
69398
69500
  const name = this.normVmType(vmName);
@@ -69413,7 +69515,7 @@ class LookPlanoCollectionFormComponent {
69413
69515
  * artwork (vmId/vmName) is updated while the library keeps its own VM
69414
69516
  * positions. vmTypes not yet present are appended.
69415
69517
  */
69416
- applyPlacementsToAssignedLibraries() {
69518
+ async applyPlacementsToAssignedLibraries() {
69417
69519
  const slot = this.selectedSlotGroup;
69418
69520
  if (!slot)
69419
69521
  return;
@@ -69446,10 +69548,30 @@ class LookPlanoCollectionFormComponent {
69446
69548
  label,
69447
69549
  });
69448
69550
  }
69551
+ // Nothing to apply — bail out before any fetch.
69552
+ if (pidByZone.size === 0 && vmByType.size === 0)
69553
+ return;
69554
+ // For any library that doesn't have a snapshot yet, fetch its details so
69555
+ // we can use the doc as the base. Without this, applyPlacements silently
69556
+ // skipped libraries the user hadn't viewed and only the currently-visible
69557
+ // chip ever received updates.
69558
+ const fetchedBases = new Map();
69559
+ await Promise.all(refs.map(async (libId) => {
69560
+ if (this.getLibrarySnapshot(libId))
69561
+ return;
69562
+ try {
69563
+ const res = await lastValueFrom(this.builderService.getFixtureLibraryDetails(libId));
69564
+ if (res?.data)
69565
+ fetchedBases.set(libId, res.data);
69566
+ }
69567
+ catch (err) {
69568
+ console.log('@@ ~ applyPlacementsToAssignedLibraries [fetch ERR]:', err);
69569
+ }
69570
+ }));
69449
69571
  for (const libId of refs) {
69450
- const target = this.getLibrarySnapshot(libId);
69572
+ const target = this.getLibrarySnapshot(libId) || fetchedBases.get(libId);
69451
69573
  if (!target)
69452
- continue; // seed creates one per assigned library
69574
+ continue;
69453
69575
  const shelves = Array.isArray(target.shelfConfig) ? target.shelfConfig : [];
69454
69576
  const newShelfConfig = shelves.map((shelf) => {
69455
69577
  const zone = norm(shelf?.zone || shelf?.label);
@@ -69472,14 +69594,12 @@ class LookPlanoCollectionFormComponent {
69472
69594
  for (const [key, upd] of vmByType) {
69473
69595
  if (seen.has(key))
69474
69596
  continue;
69597
+ const layout = this.initialVmLayout(upd.label, newShelfConfig);
69475
69598
  newVmConfig.push({
69476
69599
  vmType: upd.label,
69477
69600
  vmId: upd.vmId,
69478
69601
  vmName: upd.vmName,
69479
- startYPosition: 1,
69480
- endYPosition: 1,
69481
- xZone: '',
69482
- yZone: '',
69602
+ ...layout,
69483
69603
  position: '',
69484
69604
  });
69485
69605
  }
@@ -69681,6 +69801,7 @@ class LookPlanoCollectionFormComponent {
69681
69801
  if (this.clientId) {
69682
69802
  this.loadFixtureLibraries();
69683
69803
  this.loadPlacementDropdownData();
69804
+ this.loadComboOptions();
69684
69805
  }
69685
69806
  });
69686
69807
  this.route.params.pipe(takeUntil(this.destroy$)).subscribe(async (params) => {
@@ -69754,6 +69875,14 @@ class LookPlanoCollectionFormComponent {
69754
69875
  ? seed.fixtureSlots
69755
69876
  : Array.from({ length: Math.max(1, Number(seed.fixtureCount) || 1) }, (_, i) => ({ slotIndex: i + 1 }));
69756
69877
  const slots = seedSlots.map((s, i) => this.createSlotGroup({ ...s, slotIndex: i + 1 }));
69878
+ // Legacy data may have stored cadHeaderVariants at the slot level. If the
69879
+ // incoming look doesn't have its own variants array, fold the union of
69880
+ // slot-level variants into the look so editing in the new UI doesn't lose
69881
+ // them.
69882
+ const seedLookVariants = Array.isArray(seed.cadHeaderVariants)
69883
+ ? seed.cadHeaderVariants
69884
+ : Array.from(new Set((Array.isArray(seed.fixtureSlots) ? seed.fixtureSlots : [])
69885
+ .flatMap((s) => Array.isArray(s?.cadHeaderVariants) ? s.cadHeaderVariants : [])));
69757
69886
  const lookGroup = this.fb.group({
69758
69887
  keyType: [seed.keyType || 'mbq'],
69759
69888
  mbqBucket: [seed.mbqBucket || ''],
@@ -69762,6 +69891,7 @@ class LookPlanoCollectionFormComponent {
69762
69891
  fixtureType: [Array.isArray(seed.fixtureType) ? seed.fixtureType : []],
69763
69892
  layoutVariant: [seed.layoutVariant || 'shelf'],
69764
69893
  storeProto: [seed.storeProto || ''],
69894
+ cadHeaderVariants: [seedLookVariants],
69765
69895
  fixtureSlots: this.fb.array(slots),
69766
69896
  });
69767
69897
  // fixtureCount → slots length (user-edited count adjusts the slots array)
@@ -69805,7 +69935,6 @@ class LookPlanoCollectionFormComponent {
69805
69935
  fixtureLibraryRef: [seed.fixtureLibraryRef || ''],
69806
69936
  fixtureLibraryLabel: [seed.fixtureLibraryLabel || ''],
69807
69937
  fixtureLibraryRefs: [Array.isArray(seed.fixtureLibraryRefs) ? seed.fixtureLibraryRefs : []],
69808
- cadHeaderVariants: [Array.isArray(seed.cadHeaderVariants) ? seed.cadHeaderVariants : []],
69809
69938
  placements: this.fb.array(placements),
69810
69939
  libraryConfigurations: this.fb.array(libraryConfigs),
69811
69940
  // Per-library snapshot of the embedded fixture-template editor state
@@ -69967,6 +70096,85 @@ class LookPlanoCollectionFormComponent {
69967
70096
  console.log('@@ ~ loadPlacementDropdownData [ERR]:', err);
69968
70097
  }
69969
70098
  }
70099
+ /**
70100
+ * Load the MBQ bucket / Brand / Fixture Header dropdown options seeded into
70101
+ * planostaticdatas from the sheet, then rebuild the flat option lists
70102
+ * (merging in any values already present on the loaded collection).
70103
+ */
70104
+ async loadComboOptions() {
70105
+ if (!this.clientId)
70106
+ return;
70107
+ try {
70108
+ const res = await lastValueFrom(this.builderService.getLookComboOptions(this.clientId));
70109
+ const data = res?.data || {};
70110
+ const toStrArr = (arr) => (Array.isArray(arr) ? arr : [])
70111
+ .map((v) => String(v ?? '').trim())
70112
+ .filter((s) => !!s);
70113
+ this.comboRaw = {
70114
+ mbqBuckets: toStrArr(data.mbqBuckets),
70115
+ brands: toStrArr(data.brands),
70116
+ fixtureHeaders: toStrArr(data.fixtureHeaders),
70117
+ combinations: (Array.isArray(data.combinations) ? data.combinations : [])
70118
+ .map((c) => ({
70119
+ mbqBucket: String(c?.mbqBucket ?? '').trim(),
70120
+ brand: String(c?.brand ?? '').trim(),
70121
+ fixtureHeader: String(c?.fixtureHeader ?? '').trim(),
70122
+ })),
70123
+ };
70124
+ }
70125
+ catch (err) {
70126
+ // Endpoint missing / not yet deployed — fall back to values derived
70127
+ // from the loaded collection so the dropdowns still work.
70128
+ console.log('@@ ~ loadComboOptions [ERR]:', err);
70129
+ }
70130
+ this.rebuildComboOptions();
70131
+ }
70132
+ /**
70133
+ * Rebuild the MBQ bucket / Brand / Fixture Header option lists (and the
70134
+ * cascading caches) by MERGING the seeded endpoint payload (comboRaw) with
70135
+ * whatever values are already present on the currently-loaded looks. The
70136
+ * merge guarantees the dropdowns are never empty for an existing collection
70137
+ * even when the endpoint hasn't loaded, and that any value already saved on
70138
+ * a look is always selectable.
70139
+ */
70140
+ rebuildComboOptions() {
70141
+ const buckets = new Set(this.comboRaw.mbqBuckets);
70142
+ const brands = new Set(this.comboRaw.brands);
70143
+ const headers = new Set(this.comboRaw.fixtureHeaders);
70144
+ // Seed combinations also feed the flat per-field lists.
70145
+ for (const c of this.comboRaw.combinations) {
70146
+ if (c.mbqBucket)
70147
+ buckets.add(c.mbqBucket);
70148
+ if (c.brand)
70149
+ brands.add(c.brand);
70150
+ if (c.fixtureHeader)
70151
+ headers.add(c.fixtureHeader);
70152
+ }
70153
+ // Merge in values already present on the loaded looks/slots so the
70154
+ // dropdowns work even when the endpoint hasn't loaded.
70155
+ for (const lookCtrl of this.looks.controls) {
70156
+ const look = lookCtrl;
70157
+ const bucket = String(look.get('mbqBucket')?.value ?? '').trim()
70158
+ || String(look.get('brandCategory')?.value ?? '').trim();
70159
+ if (bucket)
70160
+ buckets.add(bucket);
70161
+ const slots = look.get('fixtureSlots');
70162
+ for (const slotCtrl of slots?.controls || []) {
70163
+ const slot = slotCtrl;
70164
+ const brand = String(slot.get('brand')?.value ?? '').trim();
70165
+ const header = String(slot.get('fixtureLevelZone')?.value ?? '').trim();
70166
+ if (brand)
70167
+ brands.add(brand);
70168
+ if (header)
70169
+ headers.add(header);
70170
+ }
70171
+ }
70172
+ const toItems = (set) => [...set].filter((s) => !!s).sort().map((s) => ({ value: s, label: s }));
70173
+ this.mbqBucketItems = toItems(buckets);
70174
+ this.brandItems = toItems(brands);
70175
+ this.fixtureHeaderItems = toItems(headers);
70176
+ console.log('[Looks form] combo options:', this.mbqBucketItems.length, 'buckets,', this.brandItems.length, 'brands,', this.fixtureHeaderItems.length, 'headers');
70177
+ }
69970
70178
  async loadFixtureLibraries() {
69971
70179
  if (!this.clientId)
69972
70180
  return;
@@ -70035,9 +70243,33 @@ class LookPlanoCollectionFormComponent {
70035
70243
  return;
70036
70244
  }
70037
70245
  this.recomputeFixtureLibraryItemsForSlot();
70246
+ // Re-filter the Fixture Templates list when the Fixture Type changes, and
70247
+ // RESET the current template selections: wall (shelf) and floor
70248
+ // (eurocenter) library sets are disjoint, so anything selected for the old
70249
+ // type is invalid for the new one. Clearing fixtureLibraryRefs cascades —
70250
+ // the refsSub below clears the embedded preview, and we drop the stale
70251
+ // per-library snapshots too.
70252
+ let prevSlotType = String(slot.get('slotType')?.value ?? '');
70038
70253
  const typeSub = slot.get('slotType')?.valueChanges
70039
70254
  .pipe(takeUntil(this.destroy$))
70040
- .subscribe(() => this.recomputeFixtureLibraryItemsForSlot());
70255
+ .subscribe((nextType) => {
70256
+ const changed = String(nextType ?? '') !== prevSlotType;
70257
+ prevSlotType = String(nextType ?? '');
70258
+ this.recomputeFixtureLibraryItemsForSlot();
70259
+ if (!changed)
70260
+ return;
70261
+ const refsCtrl = slot.get('fixtureLibraryRefs');
70262
+ if (refsCtrl && (refsCtrl.value || []).length) {
70263
+ refsCtrl.setValue([]); // triggers refsSub -> clears active preview
70264
+ }
70265
+ const snapsCtrl = slot.get('librarySnapshots');
70266
+ if (snapsCtrl && (snapsCtrl.value || []).length) {
70267
+ snapsCtrl.setValue([], { emitEvent: false });
70268
+ }
70269
+ // Defensive: clear the legacy single-ref fields if they were set.
70270
+ slot.get('fixtureLibraryRef')?.setValue('', { emitEvent: false });
70271
+ slot.get('fixtureLibraryLabel')?.setValue('', { emitEvent: false });
70272
+ });
70041
70273
  if (typeSub)
70042
70274
  this.slotSubs.push(typeSub);
70043
70275
  // Track previous refs so we can detect *new* additions and auto-apply
@@ -70160,15 +70392,17 @@ class LookPlanoCollectionFormComponent {
70160
70392
  if (existingVms.length === 0) {
70161
70393
  const vmPlacements = placements.filter((p) => p?.kind === 'vm' && p?.rawValue);
70162
70394
  if (vmPlacements.length > 0) {
70163
- newVmConfig = vmPlacements.map((p) => ({
70164
- vmType: String(p?.position || ''),
70165
- vmId: String(p?.rawValue || ''),
70166
- startYPosition: 1,
70167
- endYPosition: 1,
70168
- xZone: '',
70169
- yZone: '',
70170
- position: '',
70171
- }));
70395
+ newVmConfig = vmPlacements.map((p) => {
70396
+ const vmType = String(p?.position || '');
70397
+ const layout = this.initialVmLayout(vmType, newShelfConfig);
70398
+ return {
70399
+ vmType,
70400
+ vmId: String(p?.rawValue || ''),
70401
+ vmName: String(p?.rawValue || ''),
70402
+ ...layout,
70403
+ position: '',
70404
+ };
70405
+ });
70172
70406
  vmsApplied = true;
70173
70407
  }
70174
70408
  }
@@ -70205,6 +70439,9 @@ class LookPlanoCollectionFormComponent {
70205
70439
  this.resetLibraryMap();
70206
70440
  this.bindActiveSlotListeners();
70207
70441
  this.autoSelectFirstLibrary();
70442
+ // Merge this collection's own values into the dropdown options so they
70443
+ // show even if the comboOptions endpoint is unavailable.
70444
+ this.rebuildComboOptions();
70208
70445
  }
70209
70446
  }
70210
70447
  catch (err) {
@@ -70232,6 +70469,22 @@ class LookPlanoCollectionFormComponent {
70232
70469
  this.selectedLookIndex -= 1;
70233
70470
  }
70234
70471
  }
70472
+ /**
70473
+ * Clone the look at `index` (deep copy of its full nested value — slots,
70474
+ * placements, cadHeaderVariants, librarySnapshots) and insert it right
70475
+ * after the original, then select the copy. createLookGroup rebuilds the
70476
+ * whole FormGroup tree from the JSON seed, so the clone shares no control
70477
+ * references with the source.
70478
+ */
70479
+ duplicateLook(index) {
70480
+ const src = this.looks.at(index);
70481
+ if (!src)
70482
+ return;
70483
+ const seed = JSON.parse(JSON.stringify(src.getRawValue()));
70484
+ const clone = this.createLookGroup(seed);
70485
+ this.looks.insert(index + 1, clone);
70486
+ this.selectLook(index + 1);
70487
+ }
70235
70488
  selectLook(index) {
70236
70489
  this.selectedLookIndex = index;
70237
70490
  const slots = this.fixtureSlots(index);
@@ -70285,6 +70538,32 @@ class LookPlanoCollectionFormComponent {
70285
70538
  const count = lookGroup.get('fixtureCount')?.value;
70286
70539
  return `${key || '(unnamed)'} — ${count || 0} fixtures`;
70287
70540
  }
70541
+ /**
70542
+ * Short visual indicator for the slotType mix of a look — rendered as a
70543
+ * coloured chip in the MBQ Bucket - Fixture Combinations list. Returns
70544
+ * `null` for an empty look so the chip is hidden.
70545
+ */
70546
+ fixtureTypeBadge(lookGroup) {
70547
+ const slots = lookGroup.get('fixtureSlots');
70548
+ if (!slots || slots.length === 0)
70549
+ return null;
70550
+ let hasShelf = false;
70551
+ let hasEuro = false;
70552
+ for (const c of slots.controls) {
70553
+ const t = c.get('slotType')?.value;
70554
+ if (t === 'eurocenter')
70555
+ hasEuro = true;
70556
+ else
70557
+ hasShelf = true;
70558
+ if (hasShelf && hasEuro)
70559
+ break;
70560
+ }
70561
+ if (hasShelf && hasEuro)
70562
+ return { label: 'Wall + Floor', cls: 'badge-mixed' };
70563
+ if (hasEuro)
70564
+ return { label: 'Floor', cls: 'badge-floor' };
70565
+ return { label: 'Wall', cls: 'badge-wall' };
70566
+ }
70288
70567
  bucketKey(lookGroup) {
70289
70568
  const keyType = lookGroup.get('keyType')?.value;
70290
70569
  const key = keyType === 'brandCategory'
@@ -70427,11 +70706,11 @@ class LookPlanoCollectionFormComponent {
70427
70706
  this.planoDataService.fixtureTemplateDetails.next(null);
70428
70707
  }
70429
70708
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LookPlanoCollectionFormComponent, deps: [{ token: i1$2.FormBuilder }, { token: i2.ActivatedRoute }, { token: i2.Router }, { token: i2$1.GlobalStateService }, { token: StoreBuilderService }, { token: i4.ToastService }, { token: i2$1.PageInfoService }, { token: PlanoDataService }], target: i0.ɵɵFactoryTarget.Component });
70430
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: LookPlanoCollectionFormComponent, selector: "lib-look-plano-collection-form", ngImport: i0, template: "<section class=\"look-form\">\r\n <!-- Top bar (bound to the outer form) -->\r\n <div class=\"card p-4 mb-4\" [formGroup]=\"form\">\r\n <div class=\"row align-items-end\">\r\n <div class=\"col-md-4\">\r\n <label class=\"form-label fw-bold\">Name *</label>\r\n <input type=\"text\" class=\"form-control\" formControlName=\"name\" placeholder=\"e.g. Master Look Plan FY26\" />\r\n </div>\r\n <div class=\"col-md-5\">\r\n <label class=\"form-label fw-bold\">Description</label>\r\n <input type=\"text\" class=\"form-control\" formControlName=\"description\" placeholder=\"Optional description\" />\r\n </div>\r\n <div class=\"col-md-1 text-center\">\r\n <label class=\"form-label fw-bold d-block\">Active</label>\r\n <input type=\"checkbox\" class=\"form-check-input\" formControlName=\"isActive\" />\r\n </div>\r\n <div class=\"col-md-2 text-end\">\r\n <button type=\"button\" class=\"btn btn-light me-2\" (click)=\"cancel()\">Cancel</button>\r\n <button type=\"button\" class=\"btn btn-primary\" [disabled]=\"saving\" (click)=\"save()\">\r\n {{ saving ? 'Saving\u2026' : 'Save' }}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Three-pane editor -->\r\n <div class=\"row mx-0 g-3\">\r\n <!-- Left: Looks list (no form binding needed \u2014 just navigation) -->\r\n <div class=\"col-md-3\">\r\n <div class=\"card p-3 h-100\">\r\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\r\n <h6 class=\"m-0\">MBQ Bucket - Fixture Combinations</h6>\r\n <button type=\"button\" class=\"btn btn-sm btn-primary\" (click)=\"addLook()\">+ Add Look</button>\r\n </div>\r\n <div *ngIf=\"looks.length === 0\" class=\"text-muted small\">No looks yet. Add one to start.</div>\r\n <div class=\"looks-tree\">\r\n <div\r\n *ngFor=\"let look of looks.controls; let i = index; trackBy: trackByIndex\"\r\n class=\"look-row d-flex justify-content-between align-items-center px-2 py-2 mb-1 rounded\"\r\n [class.selected]=\"selectedLookIndex === i\"\r\n [style.background-color]=\"bucketColor($any(look))\"\r\n (click)=\"selectLook(i)\"\r\n >\r\n <span class=\"d-flex align-items-center gap-2 text-truncate\">\r\n <span class=\"bucket-dot\" [style.background-color]=\"bucketColor($any(look))\"></span>\r\n <span class=\"text-truncate\">{{ lookTitle($any(look)) }}</span>\r\n </span>\r\n <button type=\"button\" class=\"btn btn-sm btn-light-danger ms-2\" (click)=\"$event.stopPropagation(); removeLook(i)\">\u00D7</button>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Centre: selected Look metadata + slots -->\r\n <div class=\"col-md-3\">\r\n <ng-container *ngIf=\"selectedLookGroup as lookGroup; else noLook\">\r\n <div class=\"card p-3 h-100\" [formGroup]=\"lookGroup\">\r\n <h6 class=\"mb-3\">Fixture Combination Details</h6>\r\n\r\n <div class=\"row g-2 mb-3\">\r\n <div class=\"col-md-6\">\r\n <label class=\"form-label small\">MBQ bucket</label>\r\n <input type=\"text\" class=\"form-control form-control-sm\" formControlName=\"mbqBucket\" placeholder=\"e.g. JJ / OD Eye\" />\r\n </div>\r\n <div class=\"col-md-6\">\r\n <label class=\"form-label small\">Fixture count *</label>\r\n <input type=\"number\" min=\"0\" max=\"30\" class=\"form-control form-control-sm\" formControlName=\"fixtureCount\" />\r\n </div>\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small\">Fixture Positions</label>\r\n <multiselect-chip-dropdown\r\n [items]=\"fixtureTypeOptions\"\r\n idField=\"id\"\r\n nameField=\"name\"\r\n placeholder=\"Select fixture position\"\r\n formControlName=\"fixtureType\">\r\n </multiselect-chip-dropdown>\r\n </div>\r\n </div>\r\n\r\n <hr />\r\n\r\n <div class=\"d-flex justify-content-between align-items-center mb-2\">\r\n <h6 class=\"m-0\">Fixtures</h6>\r\n <button type=\"button\" class=\"btn btn-sm btn-light\" (click)=\"addSlot()\">+ Add Fixture</button>\r\n </div>\r\n <div class=\"slots-list\">\r\n <div\r\n *ngFor=\"let slot of (selectedSlotsArray?.controls || []); let s = index; trackBy: trackByIndex\"\r\n class=\"slot-row d-flex justify-content-between align-items-center px-2 py-2 mb-1 rounded\"\r\n [class.selected]=\"selectedSlotIndex === s\"\r\n (click)=\"selectSlot(s)\"\r\n >\r\n <div class=\"d-flex flex-column\">\r\n <span class=\"fw-bold small\">\r\n Fixture {{ $any(slot).get('slotIndex')?.value }} \u2014 {{ $any(slot).get('slotType')?.value }}\r\n </span>\r\n <span class=\"text-muted small\">\r\n {{ $any(slot).get('brand')?.value || '(no brand)' }} \u00B7\r\n {{ $any(slot).get('fixtureLevelZone')?.value || '(no zone)' }}\r\n </span>\r\n </div>\r\n <button type=\"button\" class=\"btn btn-sm btn-light-danger ms-2\" (click)=\"$event.stopPropagation(); removeSlot(s)\">\u00D7</button>\r\n </div>\r\n </div>\r\n </div>\r\n </ng-container>\r\n <ng-template #noLook>\r\n <div class=\"card p-5 h-100 text-center text-muted\">Select or add a Look to edit it.</div>\r\n </ng-template>\r\n </div>\r\n\r\n <!-- Right: selected slot \u2014 placements editor -->\r\n <div class=\"col-md-6\">\r\n <ng-container *ngIf=\"selectedSlotGroup as slotGroup; else noSlot\">\r\n <div class=\"card p-3 h-100\" [formGroup]=\"slotGroup\">\r\n <h6 class=\"mb-3\">Fixture {{ selectedSlotIndex + 1 }} - Definition</h6>\r\n\r\n <div class=\"row g-2 mb-3\">\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small text-muted\">Slot index (auto)</label>\r\n <input type=\"text\" class=\"form-control form-control-sm\" [value]=\"selectedSlotIndex + 1\" readonly disabled />\r\n </div>\r\n <div class=\"col-md-6\">\r\n <label class=\"form-label small\">Fixture Type</label>\r\n <select class=\"form-select form-select-sm\" formControlName=\"slotType\">\r\n <option *ngFor=\"let opt of availableSlotTypes\" [value]=\"opt.id\">{{ opt.name }}</option>\r\n </select>\r\n </div>\r\n <div class=\"col-md-6\">\r\n <label class=\"form-label small\">Brand</label>\r\n <input type=\"text\" class=\"form-control form-control-sm\" formControlName=\"brand\" placeholder=\"John Jacobs\" />\r\n </div>\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small\">Fixture Header</label>\r\n <input type=\"text\" class=\"form-control form-control-sm\" formControlName=\"fixtureLevelZone\" placeholder=\"Premium Trending\" />\r\n </div>\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small\">CAD Header Varients</label>\r\n <lib-chips-input\r\n formControlName=\"cadHeaderVariants\"\r\n placeholder=\"Type a variant and press Enter\">\r\n </lib-chips-input>\r\n </div>\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small\">Fixture Templates</label>\r\n <multiselect-chip-dropdown\r\n class=\"fixture-libraries-select\"\r\n [items]=\"fixtureLibraryItemsForSlot\"\r\n idField=\"id\"\r\n nameField=\"name\"\r\n [search]=\"true\"\r\n searchField=\"name\"\r\n placeholder=\"Select fixture templates\"\r\n formControlName=\"fixtureLibraryRefs\">\r\n </multiselect-chip-dropdown>\r\n <small class=\"text-muted\" *ngIf=\"slotGroup.get('slotType')?.value === 'eurocenter'\">\r\n Showing floor fixtures (eurocenter slot).\r\n </small>\r\n <small class=\"text-muted\" *ngIf=\"slotGroup.get('slotType')?.value !== 'eurocenter'\">\r\n Showing wall fixtures (shelf slot).\r\n </small>\r\n </div>\r\n </div>\r\n\r\n <hr />\r\n\r\n <div class=\"d-flex justify-content-between align-items-center mb-2\">\r\n <h6 class=\"m-0\">Look and Visual Merchandising Placements</h6>\r\n <button type=\"button\" class=\"btn btn-sm btn-light\" (click)=\"addPlacement()\">+ Add placement</button>\r\n </div>\r\n <div *ngIf=\"(selectedPlacements?.length || 0) === 0\" class=\"text-muted small mb-2\">\r\n No placements yet. Click <strong>+ Add placement</strong> to add one.\r\n </div>\r\n <div class=\"placements-scroll\" formArrayName=\"placements\">\r\n <div\r\n *ngFor=\"let p of (selectedPlacements?.controls || []); let pi = index; trackBy: trackByIndex\"\r\n class=\"placement-row mb-2\"\r\n [formGroupName]=\"pi\"\r\n >\r\n <div class=\"d-flex align-items-start gap-2 placement-fields\">\r\n <div class=\"ph-field\">\r\n <select class=\"form-select form-select-sm\" formControlName=\"kind\">\r\n <option value=\"pid\">Product Merchandising</option>\r\n <option value=\"vm\">Visual Merchandising</option>\r\n </select>\r\n </div>\r\n\r\n <!-- Visual Merchandising rows: VM Type (single) + VM Artwork (single) -->\r\n <ng-container *ngIf=\"$any(p).get('kind')?.value === 'vm'; else pidInputs\">\r\n <div class=\"ph-field\">\r\n <lib-searchable-select\r\n [items]=\"vmTypeOptions\"\r\n idField=\"value\"\r\n nameField=\"label\"\r\n placeholder=\"Visual Merchandising Type\"\r\n formControlName=\"position\">\r\n </lib-searchable-select>\r\n </div>\r\n <div class=\"ph-field\">\r\n <lib-searchable-select\r\n [items]=\"vmArtworkOptionsFor($any(p).get('position')?.value)\"\r\n idField=\"value\"\r\n nameField=\"label\"\r\n placeholder=\"Visual Merchandising Artwork\"\r\n formControlName=\"rawValue\">\r\n </lib-searchable-select>\r\n </div>\r\n </ng-container>\r\n\r\n <!-- Product Merchandising rows: Top/Mid/Bottom position + multi-select product/SKU -->\r\n <ng-template #pidInputs>\r\n <div class=\"ph-field\">\r\n <select class=\"form-select form-select-sm\" formControlName=\"position\">\r\n <option value=\"\">Position</option>\r\n <option value=\"Top\">Top</option>\r\n <option value=\"Mid\">Mid</option>\r\n <option value=\"Bottom\">Bottom</option>\r\n </select>\r\n </div>\r\n <div class=\"ph-field\">\r\n <lib-searchable-select\r\n [items]=\"productOptions\"\r\n idField=\"value\"\r\n nameField=\"label\"\r\n [multiple]=\"true\"\r\n placeholder=\"Product / SKU\"\r\n formControlName=\"rawValues\">\r\n </lib-searchable-select>\r\n </div>\r\n </ng-template>\r\n\r\n <button type=\"button\" class=\"btn btn-sm btn-light-danger ph-remove\" (click)=\"removePlacement(pi)\">\u00D7</button>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </ng-container>\r\n <ng-template #noSlot>\r\n <div class=\"card p-5 h-100 text-center text-muted\">Select a fixture slot to edit placements.</div>\r\n </ng-template>\r\n </div>\r\n </div>\r\n\r\n <!-- Per-slot Fixture Library Configuration card -->\r\n <ng-container *ngIf=\"selectedSlotGroup as slotGroupForMap\">\r\n <div class=\"card p-4 mt-4 library-config-card\" [formGroup]=\"slotGroupForMap\">\r\n <div class=\"mb-4\">\r\n <h5 class=\"form-label d-block mb-3\">Fixture library</h5>\r\n <div class=\"library-chips\">\r\n <button\r\n *ngFor=\"let lib of selectedSlotLibrariesForDropdown\"\r\n type=\"button\"\r\n class=\"library-chip\"\r\n [class.active]=\"selectedLibraryIdInMap === lib.id\"\r\n (click)=\"onLibraryMapSelect(selectedLibraryIdInMap === lib.id ? '' : lib.id)\">\r\n {{ lib.name }}\r\n </button>\r\n </div>\r\n <small *ngIf=\"selectedSlotLibrariesForDropdown.length === 0\" class=\"text-muted\">\r\n No libraries linked to this slot yet. Add some in the \"Fixture libraries\" multiselect above.\r\n </small>\r\n </div>\r\n\r\n <div *ngIf=\"loadingLibraryDetails\" class=\"text-muted\">Loading fixture details\u2026</div>\r\n\r\n <!--\r\n Use *ngFor + trackBy keyed on (look, slot, library) so Angular treats\r\n a switch as a new item \u2014 that forces the embedded template-products\r\n / template-vms components to remount and rerun their ngOnInit\r\n (otherwise their `isPageLoading` guard skips the form rebuild).\r\n -->\r\n <ng-container *ngFor=\"let _ of embeddedHostKeys; trackBy: trackByEmbeddedKey\">\r\n <ul class=\"nav nav-tabs custom-tabs mb-3\">\r\n <li class=\"nav-item\">\r\n <button type=\"button\" class=\"nav-link\"\r\n [class.active]=\"embeddedTab === 'products'\"\r\n (click)=\"embeddedTab = 'products'\">Product Merchandising</button>\r\n </li>\r\n <li class=\"nav-item\">\r\n <button type=\"button\" class=\"nav-link\"\r\n [class.active]=\"embeddedTab === 'vms'\"\r\n (click)=\"embeddedTab = 'vms'\">Visual Merchandising</button>\r\n </li>\r\n </ul>\r\n\r\n <div *ngIf=\"embeddedTab === 'products'\">\r\n <lib-template-products [looksMode]=\"true\"></lib-template-products>\r\n </div>\r\n <div *ngIf=\"embeddedTab === 'vms'\">\r\n <lib-template-vms [looksMode]=\"true\"></lib-template-vms>\r\n </div>\r\n </ng-container>\r\n </div>\r\n </ng-container>\r\n</section>\r\n", styles: [".look-form .col-md-3>.card,.look-form .col-md-6>.card{display:flex!important;flex-direction:column;height:75vh!important;min-height:600px}.look-form .looks-tree,.look-form .slots-list,.look-form .placements-scroll{flex:1 1 auto;min-height:0;overflow-y:auto}.look-form .look-row,.look-form .slot-row{cursor:pointer;border:1px solid transparent;background:#f9fafb;transition:filter .15s ease,box-shadow .15s ease,border-color .15s ease}.look-form .look-row:hover,.look-form .slot-row:hover{filter:brightness(.96)}.look-form .look-row.selected,.look-form .slot-row.selected{filter:brightness(.96);outline:1px solid rgba(14,165,233,.55);outline-offset:-1px}.look-form .bucket-dot{display:inline-block;width:10px;height:10px;border-radius:50%;flex-shrink:0;border:1px solid rgba(0,0,0,.12)}.look-form .fixture-libraries-select{display:block}.look-form .fixture-libraries-select ::ng-deep .multiselect-container{align-items:flex-start}.look-form .fixture-libraries-select ::ng-deep .chip-list{max-height:84px;overflow-y:auto;padding-right:4px}.look-form .placement-row .placement-fields .ph-field{flex:1 1 0;min-width:0}.look-form .placement-row .placement-fields .ph-remove{flex:0 0 auto}.look-form .placement-row .placement-fields ::ng-deep multiselect-chip-dropdown .multiselect-container{padding:4px 10px;min-height:31px}.look-form .placement-row .placement-fields ::ng-deep multiselect-chip-dropdown .chip-list{min-height:21px}.look-form .placement-row .placement-fields ::ng-deep multiselect-chip-dropdown .multi-placeholder{font-size:13px;line-height:21px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.look-form .library-config-card .library-chips{display:flex;flex-wrap:wrap;gap:8px}.look-form .library-config-card .library-chip{background:#f1f5f9;border:1px solid #cbd5e1;color:#1e293b;border-radius:999px;padding:6px 14px;font-size:13px;font-weight:500;cursor:pointer;transition:background-color .15s ease,border-color .15s ease,color .15s ease}.look-form .library-config-card .library-chip:hover{background:#e2e8f0;border-color:#94a3b8}.look-form .library-config-card .library-chip.active{background:#0ea5e9;border-color:#0284c7;color:#fff}.look-form .library-config-card ::ng-deep #fixture-template-products .cols.col>ul.nav.nav-tabs.custom-tabs{display:none!important}.look-form .library-config-card ::ng-deep #fixture-template-products .cols.col-8>ul.nav.nav-tabs.custom-tabs{display:none!important}.look-form .library-config-card ::ng-deep #fixture-template-products input[readonly]{background-color:var(--bs-gray-200, #e9ecef);cursor:not-allowed}.look-form .library-config-card .fixture-preview{border:1px solid #e2e8f0;border-radius:8px;background:#fff;padding:12px}.look-form .library-config-card .fixture-preview .fx-header,.look-form .library-config-card .fixture-preview .fx-footer{background:#f1f5f9;border:1px dashed #cbd5e1;border-radius:6px;padding:6px 10px;font-size:12px;color:#475569;text-align:center;margin:4px 0}.look-form .library-config-card .fixture-preview .fx-body{display:flex;flex-direction:column;gap:4px;padding:4px 0}.look-form .library-config-card .fixture-preview .fx-shelf{background:linear-gradient(180deg,#f8fafc,#eef2f7);border:1px solid #cbd5e1;border-radius:4px;padding:8px 10px;display:flex;align-items:center;gap:8px;font-size:13px;min-height:36px}.look-form .library-config-card .fixture-preview .fx-shelf .fx-shelf-badge{background:#0ea5e9;color:#fff;font-weight:700;padding:2px 8px;border-radius:999px;font-size:11px;flex-shrink:0}.look-form .library-config-card .fixture-preview .fx-shelf .fx-shelf-label{font-weight:500;color:#1e293b}.look-form .library-config-card .fixture-preview .fx-shelf .fx-shelf-meta{color:#64748b;font-size:11px;margin-left:auto}.look-form .library-config-card .fixture-preview .fx-shelf.fx-shelf-tray{background:linear-gradient(180deg,#fef3c7,#fde68a);border-color:#f59e0b}.look-form .library-config-card .fixture-preview .fx-shelf.fx-shelf-tray .fx-shelf-badge{background:#d97706}.look-form .library-config-card .fixture-preview .fx-shelf.fx-shelf-vmonly{background:linear-gradient(180deg,#ede9fe,#ddd6fe);border-color:#8b5cf6}.look-form .library-config-card .fixture-preview .fx-shelf.fx-shelf-vmonly .fx-shelf-badge{background:#7c3aed}.look-form .library-config-card .fixture-preview .fx-dims{text-align:center}.look-form .library-config-card .shelf-mappings{max-height:70vh;overflow-y:auto;padding-right:4px}.look-form .library-config-card .shelf-mappings .shelf-mapping{border:1px solid #e5e7eb;border-radius:8px;padding:12px;background:#f9fafb}\n"], dependencies: [{ kind: "directive", type: i5.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i1$2.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i1$2.FormArrayName, selector: "[formArrayName]", inputs: ["formArrayName"] }, { kind: "component", type: MultiselectChipDropdownComponent, selector: "multiselect-chip-dropdown", inputs: ["idField", "nameField", "placeholder", "items", "search", "searchField", "maxSelection", "compact", "extraActionLabel", "extraActionActive", "disabled"], outputs: ["extraActionClick"] }, { kind: "component", type: TemplateProductsComponent, selector: "lib-template-products", inputs: ["looksMode"] }, { kind: "component", type: SearchableSelectComponent, selector: "lib-searchable-select", inputs: ["items", "idField", "nameField", "searchField", "placeholder", "multiple", "compact"] }, { kind: "component", type: ChipsInputComponent, selector: "lib-chips-input", inputs: ["placeholder", "allowDuplicates"] }, { kind: "component", type: TemplateVmsComponent, selector: "lib-template-vms", inputs: ["looksMode"] }] });
70709
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: LookPlanoCollectionFormComponent, selector: "lib-look-plano-collection-form", ngImport: i0, template: "<section class=\"look-form\">\r\n <!-- Top bar (bound to the outer form) -->\r\n <div class=\"card p-4 mb-4\" [formGroup]=\"form\">\r\n <div class=\"row align-items-end\">\r\n <div class=\"col-md-4\">\r\n <label class=\"form-label fw-bold\">Name *</label>\r\n <input type=\"text\" class=\"form-control\" formControlName=\"name\" placeholder=\"e.g. Master Look Plan FY26\" />\r\n </div>\r\n <div class=\"col-md-5\">\r\n <label class=\"form-label fw-bold\">Description</label>\r\n <input type=\"text\" class=\"form-control\" formControlName=\"description\" placeholder=\"Optional description\" />\r\n </div>\r\n <div class=\"col-md-1 text-center\">\r\n <label class=\"form-label fw-bold d-block\">Active</label>\r\n <input type=\"checkbox\" class=\"form-check-input\" formControlName=\"isActive\" />\r\n </div>\r\n <div class=\"col-md-2 text-end\">\r\n <button type=\"button\" class=\"btn btn-light me-2\" (click)=\"cancel()\">Cancel</button>\r\n <button type=\"button\" class=\"btn btn-primary\" [disabled]=\"saving\" (click)=\"save()\">\r\n {{ saving ? 'Saving\u2026' : 'Save' }}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Three-pane editor -->\r\n <div class=\"row mx-0 g-3\">\r\n <!-- Left: Looks list (no form binding needed \u2014 just navigation) -->\r\n <div class=\"col-md-3\">\r\n <div class=\"card p-3 h-100\">\r\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\r\n <h6 class=\"m-0\">MBQ Bucket - Fixture Combinations</h6>\r\n <button type=\"button\" class=\"btn btn-sm btn-primary\" (click)=\"addLook()\">+ Add Look</button>\r\n </div>\r\n <div *ngIf=\"looks.length === 0\" class=\"text-muted small\">No looks yet. Add one to start.</div>\r\n <div class=\"looks-tree\">\r\n <div\r\n *ngFor=\"let look of looks.controls; let i = index; trackBy: trackByIndex\"\r\n class=\"look-row d-flex justify-content-between align-items-center px-2 py-2 mb-1 rounded\"\r\n [class.selected]=\"selectedLookIndex === i\"\r\n [style.background-color]=\"bucketColor($any(look))\"\r\n (click)=\"selectLook(i)\"\r\n >\r\n <span class=\"d-flex align-items-center gap-2 text-truncate\">\r\n <span class=\"bucket-dot\" [style.background-color]=\"bucketColor($any(look))\"></span>\r\n <span class=\"text-truncate\">{{ lookTitle($any(look)) }}</span>\r\n <ng-container *ngIf=\"fixtureTypeBadge($any(look)) as ftb\">\r\n <span class=\"fixture-type-badge\" [ngClass]=\"ftb.cls\">{{ ftb.label }}</span>\r\n </ng-container>\r\n </span>\r\n <span class=\"look-row-actions\">\r\n <button type=\"button\" class=\"row-action-btn duplicate\" title=\"Duplicate look\"\r\n (click)=\"$event.stopPropagation(); duplicateLook(i)\">\r\n <i class=\"bi bi-files\"></i>\r\n </button>\r\n <button type=\"button\" class=\"row-action-btn remove\" title=\"Remove look\"\r\n (click)=\"$event.stopPropagation(); removeLook(i)\">\r\n <i class=\"bi bi-trash3\"></i>\r\n </button>\r\n </span>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Centre: selected Look metadata + slots -->\r\n <div class=\"col-md-3\">\r\n <ng-container *ngIf=\"selectedLookGroup as lookGroup; else noLook\">\r\n <div class=\"card p-3 h-100\" [formGroup]=\"lookGroup\">\r\n <h6 class=\"mb-3\">Fixture Combination Details</h6>\r\n\r\n <div class=\"row g-2 mb-3\">\r\n <div class=\"col-md-6\">\r\n <label class=\"form-label small\">MBQ bucket</label>\r\n <lib-searchable-select\r\n [items]=\"mbqBucketItems\"\r\n idField=\"value\"\r\n nameField=\"label\"\r\n placeholder=\"Select MBQ bucket\"\r\n formControlName=\"mbqBucket\">\r\n </lib-searchable-select>\r\n </div>\r\n <div class=\"col-md-6\">\r\n <label class=\"form-label small\">Fixture count *</label>\r\n <input type=\"number\" min=\"0\" max=\"30\" class=\"form-control form-control-sm\" formControlName=\"fixtureCount\" />\r\n </div>\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small\">CAD Header Varients</label>\r\n <lib-chips-input\r\n class=\"cad-header-variants-input\"\r\n formControlName=\"cadHeaderVariants\"\r\n placeholder=\"Type a variant and press Enter\">\r\n </lib-chips-input>\r\n </div>\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small\">Fixture Positions</label>\r\n <multiselect-chip-dropdown\r\n [items]=\"fixtureTypeOptions\"\r\n idField=\"id\"\r\n nameField=\"name\"\r\n placeholder=\"Select fixture position\"\r\n formControlName=\"fixtureType\">\r\n </multiselect-chip-dropdown>\r\n </div>\r\n </div>\r\n\r\n <hr />\r\n\r\n <div class=\"d-flex justify-content-between align-items-center mb-2\">\r\n <h6 class=\"m-0\">Fixtures</h6>\r\n <button type=\"button\" class=\"btn btn-sm btn-light\" (click)=\"addSlot()\">+ Add Fixture</button>\r\n </div>\r\n <div class=\"slots-list\">\r\n <div\r\n *ngFor=\"let slot of (selectedSlotsArray?.controls || []); let s = index; trackBy: trackByIndex\"\r\n class=\"slot-row d-flex justify-content-between align-items-center px-2 py-2 mb-1 rounded\"\r\n [class.selected]=\"selectedSlotIndex === s\"\r\n (click)=\"selectSlot(s)\"\r\n >\r\n <div class=\"d-flex flex-column\">\r\n <span class=\"fw-bold small\">\r\n Fixture {{ $any(slot).get('slotIndex')?.value }} \u2014 {{ $any(slot).get('slotType')?.value }}\r\n </span>\r\n <span class=\"text-muted small\">\r\n {{ $any(slot).get('brand')?.value || '(no brand)' }} \u00B7\r\n {{ $any(slot).get('fixtureLevelZone')?.value || '(no zone)' }}\r\n </span>\r\n </div>\r\n <button type=\"button\" class=\"btn btn-sm btn-light-danger ms-2\" (click)=\"$event.stopPropagation(); removeSlot(s)\">\u00D7</button>\r\n </div>\r\n </div>\r\n </div>\r\n </ng-container>\r\n <ng-template #noLook>\r\n <div class=\"card p-5 h-100 text-center text-muted\">Select or add a Look to edit it.</div>\r\n </ng-template>\r\n </div>\r\n\r\n <!-- Right: selected slot \u2014 placements editor -->\r\n <div class=\"col-md-6\">\r\n <ng-container *ngIf=\"selectedSlotGroup as slotGroup; else noSlot\">\r\n <div class=\"card p-3 h-100\" [formGroup]=\"slotGroup\">\r\n <h6 class=\"mb-3\">Fixture {{ selectedSlotIndex + 1 }} - Definition</h6>\r\n\r\n <div class=\"row g-2 mb-3\">\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small text-muted\">Slot index (auto)</label>\r\n <input type=\"text\" class=\"form-control form-control-sm\" [value]=\"selectedSlotIndex + 1\" readonly disabled />\r\n </div>\r\n <div class=\"col-md-6\">\r\n <label class=\"form-label small\">Fixture Type</label>\r\n <select class=\"form-select form-select-sm\" formControlName=\"slotType\">\r\n <option *ngFor=\"let opt of availableSlotTypes\" [value]=\"opt.id\">{{ opt.name }}</option>\r\n </select>\r\n </div>\r\n <div class=\"col-md-6\">\r\n <label class=\"form-label small\">Brand</label>\r\n <lib-searchable-select\r\n [items]=\"brandItems\"\r\n idField=\"value\"\r\n nameField=\"label\"\r\n placeholder=\"Select brand\"\r\n formControlName=\"brand\">\r\n </lib-searchable-select>\r\n </div>\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small\">Fixture Header</label>\r\n <lib-searchable-select\r\n [items]=\"fixtureHeaderItems\"\r\n idField=\"value\"\r\n nameField=\"label\"\r\n placeholder=\"Select fixture header\"\r\n formControlName=\"fixtureLevelZone\">\r\n </lib-searchable-select>\r\n </div>\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small\">Fixture Templates</label>\r\n <multiselect-chip-dropdown\r\n class=\"fixture-libraries-select\"\r\n [items]=\"fixtureLibraryItemsForSlot\"\r\n idField=\"id\"\r\n nameField=\"name\"\r\n [search]=\"true\"\r\n searchField=\"name\"\r\n placeholder=\"Select fixture templates\"\r\n formControlName=\"fixtureLibraryRefs\">\r\n </multiselect-chip-dropdown>\r\n <small class=\"text-muted\" *ngIf=\"slotGroup.get('slotType')?.value === 'eurocenter'\">\r\n Showing floor fixtures (eurocenter slot).\r\n </small>\r\n <small class=\"text-muted\" *ngIf=\"slotGroup.get('slotType')?.value !== 'eurocenter'\">\r\n Showing wall fixtures (shelf slot).\r\n </small>\r\n </div>\r\n </div>\r\n\r\n <hr />\r\n\r\n <div class=\"d-flex justify-content-between align-items-center mb-2\">\r\n <h6 class=\"m-0\">Look and Visual Merchandising Placements</h6>\r\n <button type=\"button\" class=\"btn btn-sm btn-light\" (click)=\"addPlacement()\">+ Add placement</button>\r\n </div>\r\n <div *ngIf=\"(selectedPlacements?.length || 0) === 0\" class=\"text-muted small mb-2\">\r\n No placements yet. Click <strong>+ Add placement</strong> to add one.\r\n </div>\r\n <div class=\"placements-scroll\" formArrayName=\"placements\">\r\n <div\r\n *ngFor=\"let p of (selectedPlacements?.controls || []); let pi = index; trackBy: trackByIndex\"\r\n class=\"placement-row mb-2\"\r\n [formGroupName]=\"pi\"\r\n >\r\n <div class=\"d-flex align-items-start gap-2 placement-fields\">\r\n <div class=\"ph-field\">\r\n <select class=\"form-select form-select-sm\" formControlName=\"kind\">\r\n <option value=\"pid\">Product Merchandising</option>\r\n <option value=\"vm\">Visual Merchandising</option>\r\n </select>\r\n </div>\r\n\r\n <!-- Visual Merchandising rows: VM Type (single) + VM Artwork (single) -->\r\n <ng-container *ngIf=\"$any(p).get('kind')?.value === 'vm'; else pidInputs\">\r\n <div class=\"ph-field\">\r\n <lib-searchable-select\r\n [items]=\"vmTypeOptions\"\r\n idField=\"value\"\r\n nameField=\"label\"\r\n placeholder=\"Visual Merchandising Type\"\r\n formControlName=\"position\">\r\n </lib-searchable-select>\r\n </div>\r\n <div class=\"ph-field\">\r\n <lib-searchable-select\r\n [items]=\"vmArtworkOptionsFor($any(p).get('position')?.value)\"\r\n idField=\"value\"\r\n nameField=\"label\"\r\n placeholder=\"Visual Merchandising Artwork\"\r\n formControlName=\"rawValue\">\r\n </lib-searchable-select>\r\n </div>\r\n </ng-container>\r\n\r\n <!-- Product Merchandising rows: Top/Mid/Bottom position + multi-select product/SKU -->\r\n <ng-template #pidInputs>\r\n <div class=\"ph-field\">\r\n <select class=\"form-select form-select-sm\" formControlName=\"position\">\r\n <option value=\"\">Position</option>\r\n <option value=\"Top\">Top</option>\r\n <option value=\"Mid\">Mid</option>\r\n <option value=\"Bottom\">Bottom</option>\r\n </select>\r\n </div>\r\n <div class=\"ph-field\">\r\n <lib-searchable-select\r\n [items]=\"productOptions\"\r\n idField=\"value\"\r\n nameField=\"label\"\r\n [multiple]=\"true\"\r\n placeholder=\"Product / SKU\"\r\n formControlName=\"rawValues\">\r\n </lib-searchable-select>\r\n </div>\r\n </ng-template>\r\n\r\n <button type=\"button\" class=\"btn btn-sm btn-light-danger ph-remove\" (click)=\"removePlacement(pi)\">\u00D7</button>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </ng-container>\r\n <ng-template #noSlot>\r\n <div class=\"card p-5 h-100 text-center text-muted\">Select a fixture slot to edit placements.</div>\r\n </ng-template>\r\n </div>\r\n </div>\r\n\r\n <!-- Per-slot Fixture Library Configuration card -->\r\n <ng-container *ngIf=\"selectedSlotGroup as slotGroupForMap\">\r\n <div class=\"card p-4 mt-4 library-config-card\" [formGroup]=\"slotGroupForMap\">\r\n <div class=\"mb-4\">\r\n <h5 class=\"form-label d-block mb-3\">Fixture library</h5>\r\n <div class=\"library-chips\">\r\n <button\r\n *ngFor=\"let lib of selectedSlotLibrariesForDropdown\"\r\n type=\"button\"\r\n class=\"library-chip\"\r\n [class.active]=\"selectedLibraryIdInMap === lib.id\"\r\n (click)=\"onLibraryMapSelect(selectedLibraryIdInMap === lib.id ? '' : lib.id)\">\r\n {{ lib.name }}\r\n </button>\r\n </div>\r\n <small *ngIf=\"selectedSlotLibrariesForDropdown.length === 0\" class=\"text-muted\">\r\n No libraries linked to this slot yet. Add some in the \"Fixture libraries\" multiselect above.\r\n </small>\r\n </div>\r\n\r\n <div *ngIf=\"loadingLibraryDetails\" class=\"text-muted\">Loading fixture details\u2026</div>\r\n\r\n <!--\r\n Use *ngFor + trackBy keyed on (look, slot, library) so Angular treats\r\n a switch as a new item \u2014 that forces the embedded template-products\r\n / template-vms components to remount and rerun their ngOnInit\r\n (otherwise their `isPageLoading` guard skips the form rebuild).\r\n -->\r\n <ng-container *ngFor=\"let _ of embeddedHostKeys; trackBy: trackByEmbeddedKey\">\r\n <ul class=\"nav nav-tabs custom-tabs mb-3\">\r\n <li class=\"nav-item\">\r\n <button type=\"button\" class=\"nav-link\"\r\n [class.active]=\"embeddedTab === 'products'\"\r\n (click)=\"embeddedTab = 'products'\">Product Merchandising</button>\r\n </li>\r\n <li class=\"nav-item\">\r\n <button type=\"button\" class=\"nav-link\"\r\n [class.active]=\"embeddedTab === 'vms'\"\r\n (click)=\"embeddedTab = 'vms'\">Visual Merchandising</button>\r\n </li>\r\n </ul>\r\n\r\n <div *ngIf=\"embeddedTab === 'products'\">\r\n <lib-template-products [looksMode]=\"true\"></lib-template-products>\r\n </div>\r\n <div *ngIf=\"embeddedTab === 'vms'\">\r\n <lib-template-vms [looksMode]=\"true\"></lib-template-vms>\r\n </div>\r\n </ng-container>\r\n </div>\r\n </ng-container>\r\n</section>\r\n", styles: [".look-form .col-md-3>.card,.look-form .col-md-6>.card{display:flex!important;flex-direction:column;height:85vh!important;min-height:720px}.look-form .looks-tree,.look-form .slots-list,.look-form .placements-scroll{flex:1 1 auto;min-height:0;overflow-y:auto}.look-form .look-row,.look-form .slot-row{cursor:pointer;border:1px solid transparent;background:#f9fafb;transition:filter .15s ease,box-shadow .15s ease,border-color .15s ease}.look-form .look-row:hover,.look-form .slot-row:hover{filter:brightness(.96)}.look-form .look-row.selected,.look-form .slot-row.selected{filter:brightness(.96);outline:1px solid rgba(14,165,233,.55);outline-offset:-1px}.look-form .bucket-dot{display:inline-block;width:10px;height:10px;border-radius:50%;flex-shrink:0;border:1px solid rgba(0,0,0,.12)}.look-form .look-row-actions{flex-shrink:0;display:flex;align-items:center;gap:2px;opacity:0;transition:opacity .15s ease}.look-form .look-row-actions .row-action-btn{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:0;border-radius:6px;background:transparent;color:#94a3b8;cursor:pointer;transition:background-color .15s ease,color .15s ease}.look-form .look-row-actions .row-action-btn i{font-size:13px;line-height:1}.look-form .look-row-actions .row-action-btn:hover{background:#fff}.look-form .look-row-actions .row-action-btn.duplicate:hover{color:#0ea5e9}.look-form .look-row-actions .row-action-btn.remove:hover{color:#ef4444}.look-form .look-row:hover .look-row-actions,.look-form .look-row.selected .look-row-actions{opacity:1}.look-form .fixture-type-badge{display:inline-block;flex-shrink:0;padding:1px 8px;border-radius:999px;font-size:10px;font-weight:600;line-height:14px;letter-spacing:.2px;border:1px solid transparent;white-space:nowrap}.look-form .fixture-type-badge.badge-wall{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}.look-form .fixture-type-badge.badge-floor{background:#fed7aa;color:#9a3412;border-color:#fdba74}.look-form .fixture-type-badge.badge-mixed{background:#ede9fe;color:#6d28d9;border-color:#c4b5fd}.look-form .fixture-libraries-select{display:block}.look-form .fixture-libraries-select ::ng-deep .multiselect-container{align-items:flex-start}.look-form .fixture-libraries-select ::ng-deep .chip-list{max-height:84px;overflow-y:auto;padding-right:4px}.look-form .cad-header-variants-input{display:block}.look-form .cad-header-variants-input ::ng-deep .chips-input{max-height:84px;overflow-y:auto;align-content:flex-start;padding-right:4px}.look-form .placement-row .placement-fields .ph-field{flex:1 1 0;min-width:0}.look-form .placement-row .placement-fields .ph-remove{flex:0 0 auto}.look-form .placement-row .placement-fields ::ng-deep multiselect-chip-dropdown .multiselect-container{padding:4px 10px;min-height:31px}.look-form .placement-row .placement-fields ::ng-deep multiselect-chip-dropdown .chip-list{min-height:21px}.look-form .placement-row .placement-fields ::ng-deep multiselect-chip-dropdown .multi-placeholder{font-size:13px;line-height:21px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.look-form .library-config-card .library-chips{display:flex;flex-wrap:wrap;gap:8px}.look-form .library-config-card .library-chip{background:#f1f5f9;border:1px solid #cbd5e1;color:#1e293b;border-radius:999px;padding:6px 14px;font-size:13px;font-weight:500;cursor:pointer;transition:background-color .15s ease,border-color .15s ease,color .15s ease}.look-form .library-config-card .library-chip:hover{background:#e2e8f0;border-color:#94a3b8}.look-form .library-config-card .library-chip.active{background:#0ea5e9;border-color:#0284c7;color:#fff}.look-form .library-config-card ::ng-deep #fixture-template-products .cols.col>ul.nav.nav-tabs.custom-tabs{display:none!important}.look-form .library-config-card ::ng-deep #fixture-template-products .cols.col-8>ul.nav.nav-tabs.custom-tabs{display:none!important}.look-form .library-config-card ::ng-deep #fixture-template-products input[readonly]{background-color:var(--bs-gray-200, #e9ecef);cursor:not-allowed}.look-form .library-config-card .fixture-preview{border:1px solid #e2e8f0;border-radius:8px;background:#fff;padding:12px}.look-form .library-config-card .fixture-preview .fx-header,.look-form .library-config-card .fixture-preview .fx-footer{background:#f1f5f9;border:1px dashed #cbd5e1;border-radius:6px;padding:6px 10px;font-size:12px;color:#475569;text-align:center;margin:4px 0}.look-form .library-config-card .fixture-preview .fx-body{display:flex;flex-direction:column;gap:4px;padding:4px 0}.look-form .library-config-card .fixture-preview .fx-shelf{background:linear-gradient(180deg,#f8fafc,#eef2f7);border:1px solid #cbd5e1;border-radius:4px;padding:8px 10px;display:flex;align-items:center;gap:8px;font-size:13px;min-height:36px}.look-form .library-config-card .fixture-preview .fx-shelf .fx-shelf-badge{background:#0ea5e9;color:#fff;font-weight:700;padding:2px 8px;border-radius:999px;font-size:11px;flex-shrink:0}.look-form .library-config-card .fixture-preview .fx-shelf .fx-shelf-label{font-weight:500;color:#1e293b}.look-form .library-config-card .fixture-preview .fx-shelf .fx-shelf-meta{color:#64748b;font-size:11px;margin-left:auto}.look-form .library-config-card .fixture-preview .fx-shelf.fx-shelf-tray{background:linear-gradient(180deg,#fef3c7,#fde68a);border-color:#f59e0b}.look-form .library-config-card .fixture-preview .fx-shelf.fx-shelf-tray .fx-shelf-badge{background:#d97706}.look-form .library-config-card .fixture-preview .fx-shelf.fx-shelf-vmonly{background:linear-gradient(180deg,#ede9fe,#ddd6fe);border-color:#8b5cf6}.look-form .library-config-card .fixture-preview .fx-shelf.fx-shelf-vmonly .fx-shelf-badge{background:#7c3aed}.look-form .library-config-card .fixture-preview .fx-dims{text-align:center}.look-form .library-config-card .shelf-mappings{max-height:70vh;overflow-y:auto;padding-right:4px}.look-form .library-config-card .shelf-mappings .shelf-mapping{border:1px solid #e5e7eb;border-radius:8px;padding:12px;background:#f9fafb}\n"], dependencies: [{ kind: "directive", type: i5.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i5.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i1$2.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i1$2.FormArrayName, selector: "[formArrayName]", inputs: ["formArrayName"] }, { kind: "component", type: MultiselectChipDropdownComponent, selector: "multiselect-chip-dropdown", inputs: ["idField", "nameField", "placeholder", "items", "search", "searchField", "maxSelection", "compact", "extraActionLabel", "extraActionActive", "disabled"], outputs: ["extraActionClick"] }, { kind: "component", type: TemplateProductsComponent, selector: "lib-template-products", inputs: ["looksMode"] }, { kind: "component", type: SearchableSelectComponent, selector: "lib-searchable-select", inputs: ["items", "idField", "nameField", "searchField", "placeholder", "multiple", "compact"] }, { kind: "component", type: ChipsInputComponent, selector: "lib-chips-input", inputs: ["placeholder", "allowDuplicates"] }, { kind: "component", type: TemplateVmsComponent, selector: "lib-template-vms", inputs: ["looksMode"] }] });
70431
70710
  }
70432
70711
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LookPlanoCollectionFormComponent, decorators: [{
70433
70712
  type: Component,
70434
- args: [{ selector: 'lib-look-plano-collection-form', template: "<section class=\"look-form\">\r\n <!-- Top bar (bound to the outer form) -->\r\n <div class=\"card p-4 mb-4\" [formGroup]=\"form\">\r\n <div class=\"row align-items-end\">\r\n <div class=\"col-md-4\">\r\n <label class=\"form-label fw-bold\">Name *</label>\r\n <input type=\"text\" class=\"form-control\" formControlName=\"name\" placeholder=\"e.g. Master Look Plan FY26\" />\r\n </div>\r\n <div class=\"col-md-5\">\r\n <label class=\"form-label fw-bold\">Description</label>\r\n <input type=\"text\" class=\"form-control\" formControlName=\"description\" placeholder=\"Optional description\" />\r\n </div>\r\n <div class=\"col-md-1 text-center\">\r\n <label class=\"form-label fw-bold d-block\">Active</label>\r\n <input type=\"checkbox\" class=\"form-check-input\" formControlName=\"isActive\" />\r\n </div>\r\n <div class=\"col-md-2 text-end\">\r\n <button type=\"button\" class=\"btn btn-light me-2\" (click)=\"cancel()\">Cancel</button>\r\n <button type=\"button\" class=\"btn btn-primary\" [disabled]=\"saving\" (click)=\"save()\">\r\n {{ saving ? 'Saving\u2026' : 'Save' }}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Three-pane editor -->\r\n <div class=\"row mx-0 g-3\">\r\n <!-- Left: Looks list (no form binding needed \u2014 just navigation) -->\r\n <div class=\"col-md-3\">\r\n <div class=\"card p-3 h-100\">\r\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\r\n <h6 class=\"m-0\">MBQ Bucket - Fixture Combinations</h6>\r\n <button type=\"button\" class=\"btn btn-sm btn-primary\" (click)=\"addLook()\">+ Add Look</button>\r\n </div>\r\n <div *ngIf=\"looks.length === 0\" class=\"text-muted small\">No looks yet. Add one to start.</div>\r\n <div class=\"looks-tree\">\r\n <div\r\n *ngFor=\"let look of looks.controls; let i = index; trackBy: trackByIndex\"\r\n class=\"look-row d-flex justify-content-between align-items-center px-2 py-2 mb-1 rounded\"\r\n [class.selected]=\"selectedLookIndex === i\"\r\n [style.background-color]=\"bucketColor($any(look))\"\r\n (click)=\"selectLook(i)\"\r\n >\r\n <span class=\"d-flex align-items-center gap-2 text-truncate\">\r\n <span class=\"bucket-dot\" [style.background-color]=\"bucketColor($any(look))\"></span>\r\n <span class=\"text-truncate\">{{ lookTitle($any(look)) }}</span>\r\n </span>\r\n <button type=\"button\" class=\"btn btn-sm btn-light-danger ms-2\" (click)=\"$event.stopPropagation(); removeLook(i)\">\u00D7</button>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Centre: selected Look metadata + slots -->\r\n <div class=\"col-md-3\">\r\n <ng-container *ngIf=\"selectedLookGroup as lookGroup; else noLook\">\r\n <div class=\"card p-3 h-100\" [formGroup]=\"lookGroup\">\r\n <h6 class=\"mb-3\">Fixture Combination Details</h6>\r\n\r\n <div class=\"row g-2 mb-3\">\r\n <div class=\"col-md-6\">\r\n <label class=\"form-label small\">MBQ bucket</label>\r\n <input type=\"text\" class=\"form-control form-control-sm\" formControlName=\"mbqBucket\" placeholder=\"e.g. JJ / OD Eye\" />\r\n </div>\r\n <div class=\"col-md-6\">\r\n <label class=\"form-label small\">Fixture count *</label>\r\n <input type=\"number\" min=\"0\" max=\"30\" class=\"form-control form-control-sm\" formControlName=\"fixtureCount\" />\r\n </div>\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small\">Fixture Positions</label>\r\n <multiselect-chip-dropdown\r\n [items]=\"fixtureTypeOptions\"\r\n idField=\"id\"\r\n nameField=\"name\"\r\n placeholder=\"Select fixture position\"\r\n formControlName=\"fixtureType\">\r\n </multiselect-chip-dropdown>\r\n </div>\r\n </div>\r\n\r\n <hr />\r\n\r\n <div class=\"d-flex justify-content-between align-items-center mb-2\">\r\n <h6 class=\"m-0\">Fixtures</h6>\r\n <button type=\"button\" class=\"btn btn-sm btn-light\" (click)=\"addSlot()\">+ Add Fixture</button>\r\n </div>\r\n <div class=\"slots-list\">\r\n <div\r\n *ngFor=\"let slot of (selectedSlotsArray?.controls || []); let s = index; trackBy: trackByIndex\"\r\n class=\"slot-row d-flex justify-content-between align-items-center px-2 py-2 mb-1 rounded\"\r\n [class.selected]=\"selectedSlotIndex === s\"\r\n (click)=\"selectSlot(s)\"\r\n >\r\n <div class=\"d-flex flex-column\">\r\n <span class=\"fw-bold small\">\r\n Fixture {{ $any(slot).get('slotIndex')?.value }} \u2014 {{ $any(slot).get('slotType')?.value }}\r\n </span>\r\n <span class=\"text-muted small\">\r\n {{ $any(slot).get('brand')?.value || '(no brand)' }} \u00B7\r\n {{ $any(slot).get('fixtureLevelZone')?.value || '(no zone)' }}\r\n </span>\r\n </div>\r\n <button type=\"button\" class=\"btn btn-sm btn-light-danger ms-2\" (click)=\"$event.stopPropagation(); removeSlot(s)\">\u00D7</button>\r\n </div>\r\n </div>\r\n </div>\r\n </ng-container>\r\n <ng-template #noLook>\r\n <div class=\"card p-5 h-100 text-center text-muted\">Select or add a Look to edit it.</div>\r\n </ng-template>\r\n </div>\r\n\r\n <!-- Right: selected slot \u2014 placements editor -->\r\n <div class=\"col-md-6\">\r\n <ng-container *ngIf=\"selectedSlotGroup as slotGroup; else noSlot\">\r\n <div class=\"card p-3 h-100\" [formGroup]=\"slotGroup\">\r\n <h6 class=\"mb-3\">Fixture {{ selectedSlotIndex + 1 }} - Definition</h6>\r\n\r\n <div class=\"row g-2 mb-3\">\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small text-muted\">Slot index (auto)</label>\r\n <input type=\"text\" class=\"form-control form-control-sm\" [value]=\"selectedSlotIndex + 1\" readonly disabled />\r\n </div>\r\n <div class=\"col-md-6\">\r\n <label class=\"form-label small\">Fixture Type</label>\r\n <select class=\"form-select form-select-sm\" formControlName=\"slotType\">\r\n <option *ngFor=\"let opt of availableSlotTypes\" [value]=\"opt.id\">{{ opt.name }}</option>\r\n </select>\r\n </div>\r\n <div class=\"col-md-6\">\r\n <label class=\"form-label small\">Brand</label>\r\n <input type=\"text\" class=\"form-control form-control-sm\" formControlName=\"brand\" placeholder=\"John Jacobs\" />\r\n </div>\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small\">Fixture Header</label>\r\n <input type=\"text\" class=\"form-control form-control-sm\" formControlName=\"fixtureLevelZone\" placeholder=\"Premium Trending\" />\r\n </div>\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small\">CAD Header Varients</label>\r\n <lib-chips-input\r\n formControlName=\"cadHeaderVariants\"\r\n placeholder=\"Type a variant and press Enter\">\r\n </lib-chips-input>\r\n </div>\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small\">Fixture Templates</label>\r\n <multiselect-chip-dropdown\r\n class=\"fixture-libraries-select\"\r\n [items]=\"fixtureLibraryItemsForSlot\"\r\n idField=\"id\"\r\n nameField=\"name\"\r\n [search]=\"true\"\r\n searchField=\"name\"\r\n placeholder=\"Select fixture templates\"\r\n formControlName=\"fixtureLibraryRefs\">\r\n </multiselect-chip-dropdown>\r\n <small class=\"text-muted\" *ngIf=\"slotGroup.get('slotType')?.value === 'eurocenter'\">\r\n Showing floor fixtures (eurocenter slot).\r\n </small>\r\n <small class=\"text-muted\" *ngIf=\"slotGroup.get('slotType')?.value !== 'eurocenter'\">\r\n Showing wall fixtures (shelf slot).\r\n </small>\r\n </div>\r\n </div>\r\n\r\n <hr />\r\n\r\n <div class=\"d-flex justify-content-between align-items-center mb-2\">\r\n <h6 class=\"m-0\">Look and Visual Merchandising Placements</h6>\r\n <button type=\"button\" class=\"btn btn-sm btn-light\" (click)=\"addPlacement()\">+ Add placement</button>\r\n </div>\r\n <div *ngIf=\"(selectedPlacements?.length || 0) === 0\" class=\"text-muted small mb-2\">\r\n No placements yet. Click <strong>+ Add placement</strong> to add one.\r\n </div>\r\n <div class=\"placements-scroll\" formArrayName=\"placements\">\r\n <div\r\n *ngFor=\"let p of (selectedPlacements?.controls || []); let pi = index; trackBy: trackByIndex\"\r\n class=\"placement-row mb-2\"\r\n [formGroupName]=\"pi\"\r\n >\r\n <div class=\"d-flex align-items-start gap-2 placement-fields\">\r\n <div class=\"ph-field\">\r\n <select class=\"form-select form-select-sm\" formControlName=\"kind\">\r\n <option value=\"pid\">Product Merchandising</option>\r\n <option value=\"vm\">Visual Merchandising</option>\r\n </select>\r\n </div>\r\n\r\n <!-- Visual Merchandising rows: VM Type (single) + VM Artwork (single) -->\r\n <ng-container *ngIf=\"$any(p).get('kind')?.value === 'vm'; else pidInputs\">\r\n <div class=\"ph-field\">\r\n <lib-searchable-select\r\n [items]=\"vmTypeOptions\"\r\n idField=\"value\"\r\n nameField=\"label\"\r\n placeholder=\"Visual Merchandising Type\"\r\n formControlName=\"position\">\r\n </lib-searchable-select>\r\n </div>\r\n <div class=\"ph-field\">\r\n <lib-searchable-select\r\n [items]=\"vmArtworkOptionsFor($any(p).get('position')?.value)\"\r\n idField=\"value\"\r\n nameField=\"label\"\r\n placeholder=\"Visual Merchandising Artwork\"\r\n formControlName=\"rawValue\">\r\n </lib-searchable-select>\r\n </div>\r\n </ng-container>\r\n\r\n <!-- Product Merchandising rows: Top/Mid/Bottom position + multi-select product/SKU -->\r\n <ng-template #pidInputs>\r\n <div class=\"ph-field\">\r\n <select class=\"form-select form-select-sm\" formControlName=\"position\">\r\n <option value=\"\">Position</option>\r\n <option value=\"Top\">Top</option>\r\n <option value=\"Mid\">Mid</option>\r\n <option value=\"Bottom\">Bottom</option>\r\n </select>\r\n </div>\r\n <div class=\"ph-field\">\r\n <lib-searchable-select\r\n [items]=\"productOptions\"\r\n idField=\"value\"\r\n nameField=\"label\"\r\n [multiple]=\"true\"\r\n placeholder=\"Product / SKU\"\r\n formControlName=\"rawValues\">\r\n </lib-searchable-select>\r\n </div>\r\n </ng-template>\r\n\r\n <button type=\"button\" class=\"btn btn-sm btn-light-danger ph-remove\" (click)=\"removePlacement(pi)\">\u00D7</button>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </ng-container>\r\n <ng-template #noSlot>\r\n <div class=\"card p-5 h-100 text-center text-muted\">Select a fixture slot to edit placements.</div>\r\n </ng-template>\r\n </div>\r\n </div>\r\n\r\n <!-- Per-slot Fixture Library Configuration card -->\r\n <ng-container *ngIf=\"selectedSlotGroup as slotGroupForMap\">\r\n <div class=\"card p-4 mt-4 library-config-card\" [formGroup]=\"slotGroupForMap\">\r\n <div class=\"mb-4\">\r\n <h5 class=\"form-label d-block mb-3\">Fixture library</h5>\r\n <div class=\"library-chips\">\r\n <button\r\n *ngFor=\"let lib of selectedSlotLibrariesForDropdown\"\r\n type=\"button\"\r\n class=\"library-chip\"\r\n [class.active]=\"selectedLibraryIdInMap === lib.id\"\r\n (click)=\"onLibraryMapSelect(selectedLibraryIdInMap === lib.id ? '' : lib.id)\">\r\n {{ lib.name }}\r\n </button>\r\n </div>\r\n <small *ngIf=\"selectedSlotLibrariesForDropdown.length === 0\" class=\"text-muted\">\r\n No libraries linked to this slot yet. Add some in the \"Fixture libraries\" multiselect above.\r\n </small>\r\n </div>\r\n\r\n <div *ngIf=\"loadingLibraryDetails\" class=\"text-muted\">Loading fixture details\u2026</div>\r\n\r\n <!--\r\n Use *ngFor + trackBy keyed on (look, slot, library) so Angular treats\r\n a switch as a new item \u2014 that forces the embedded template-products\r\n / template-vms components to remount and rerun their ngOnInit\r\n (otherwise their `isPageLoading` guard skips the form rebuild).\r\n -->\r\n <ng-container *ngFor=\"let _ of embeddedHostKeys; trackBy: trackByEmbeddedKey\">\r\n <ul class=\"nav nav-tabs custom-tabs mb-3\">\r\n <li class=\"nav-item\">\r\n <button type=\"button\" class=\"nav-link\"\r\n [class.active]=\"embeddedTab === 'products'\"\r\n (click)=\"embeddedTab = 'products'\">Product Merchandising</button>\r\n </li>\r\n <li class=\"nav-item\">\r\n <button type=\"button\" class=\"nav-link\"\r\n [class.active]=\"embeddedTab === 'vms'\"\r\n (click)=\"embeddedTab = 'vms'\">Visual Merchandising</button>\r\n </li>\r\n </ul>\r\n\r\n <div *ngIf=\"embeddedTab === 'products'\">\r\n <lib-template-products [looksMode]=\"true\"></lib-template-products>\r\n </div>\r\n <div *ngIf=\"embeddedTab === 'vms'\">\r\n <lib-template-vms [looksMode]=\"true\"></lib-template-vms>\r\n </div>\r\n </ng-container>\r\n </div>\r\n </ng-container>\r\n</section>\r\n", styles: [".look-form .col-md-3>.card,.look-form .col-md-6>.card{display:flex!important;flex-direction:column;height:75vh!important;min-height:600px}.look-form .looks-tree,.look-form .slots-list,.look-form .placements-scroll{flex:1 1 auto;min-height:0;overflow-y:auto}.look-form .look-row,.look-form .slot-row{cursor:pointer;border:1px solid transparent;background:#f9fafb;transition:filter .15s ease,box-shadow .15s ease,border-color .15s ease}.look-form .look-row:hover,.look-form .slot-row:hover{filter:brightness(.96)}.look-form .look-row.selected,.look-form .slot-row.selected{filter:brightness(.96);outline:1px solid rgba(14,165,233,.55);outline-offset:-1px}.look-form .bucket-dot{display:inline-block;width:10px;height:10px;border-radius:50%;flex-shrink:0;border:1px solid rgba(0,0,0,.12)}.look-form .fixture-libraries-select{display:block}.look-form .fixture-libraries-select ::ng-deep .multiselect-container{align-items:flex-start}.look-form .fixture-libraries-select ::ng-deep .chip-list{max-height:84px;overflow-y:auto;padding-right:4px}.look-form .placement-row .placement-fields .ph-field{flex:1 1 0;min-width:0}.look-form .placement-row .placement-fields .ph-remove{flex:0 0 auto}.look-form .placement-row .placement-fields ::ng-deep multiselect-chip-dropdown .multiselect-container{padding:4px 10px;min-height:31px}.look-form .placement-row .placement-fields ::ng-deep multiselect-chip-dropdown .chip-list{min-height:21px}.look-form .placement-row .placement-fields ::ng-deep multiselect-chip-dropdown .multi-placeholder{font-size:13px;line-height:21px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.look-form .library-config-card .library-chips{display:flex;flex-wrap:wrap;gap:8px}.look-form .library-config-card .library-chip{background:#f1f5f9;border:1px solid #cbd5e1;color:#1e293b;border-radius:999px;padding:6px 14px;font-size:13px;font-weight:500;cursor:pointer;transition:background-color .15s ease,border-color .15s ease,color .15s ease}.look-form .library-config-card .library-chip:hover{background:#e2e8f0;border-color:#94a3b8}.look-form .library-config-card .library-chip.active{background:#0ea5e9;border-color:#0284c7;color:#fff}.look-form .library-config-card ::ng-deep #fixture-template-products .cols.col>ul.nav.nav-tabs.custom-tabs{display:none!important}.look-form .library-config-card ::ng-deep #fixture-template-products .cols.col-8>ul.nav.nav-tabs.custom-tabs{display:none!important}.look-form .library-config-card ::ng-deep #fixture-template-products input[readonly]{background-color:var(--bs-gray-200, #e9ecef);cursor:not-allowed}.look-form .library-config-card .fixture-preview{border:1px solid #e2e8f0;border-radius:8px;background:#fff;padding:12px}.look-form .library-config-card .fixture-preview .fx-header,.look-form .library-config-card .fixture-preview .fx-footer{background:#f1f5f9;border:1px dashed #cbd5e1;border-radius:6px;padding:6px 10px;font-size:12px;color:#475569;text-align:center;margin:4px 0}.look-form .library-config-card .fixture-preview .fx-body{display:flex;flex-direction:column;gap:4px;padding:4px 0}.look-form .library-config-card .fixture-preview .fx-shelf{background:linear-gradient(180deg,#f8fafc,#eef2f7);border:1px solid #cbd5e1;border-radius:4px;padding:8px 10px;display:flex;align-items:center;gap:8px;font-size:13px;min-height:36px}.look-form .library-config-card .fixture-preview .fx-shelf .fx-shelf-badge{background:#0ea5e9;color:#fff;font-weight:700;padding:2px 8px;border-radius:999px;font-size:11px;flex-shrink:0}.look-form .library-config-card .fixture-preview .fx-shelf .fx-shelf-label{font-weight:500;color:#1e293b}.look-form .library-config-card .fixture-preview .fx-shelf .fx-shelf-meta{color:#64748b;font-size:11px;margin-left:auto}.look-form .library-config-card .fixture-preview .fx-shelf.fx-shelf-tray{background:linear-gradient(180deg,#fef3c7,#fde68a);border-color:#f59e0b}.look-form .library-config-card .fixture-preview .fx-shelf.fx-shelf-tray .fx-shelf-badge{background:#d97706}.look-form .library-config-card .fixture-preview .fx-shelf.fx-shelf-vmonly{background:linear-gradient(180deg,#ede9fe,#ddd6fe);border-color:#8b5cf6}.look-form .library-config-card .fixture-preview .fx-shelf.fx-shelf-vmonly .fx-shelf-badge{background:#7c3aed}.look-form .library-config-card .fixture-preview .fx-dims{text-align:center}.look-form .library-config-card .shelf-mappings{max-height:70vh;overflow-y:auto;padding-right:4px}.look-form .library-config-card .shelf-mappings .shelf-mapping{border:1px solid #e5e7eb;border-radius:8px;padding:12px;background:#f9fafb}\n"] }]
70713
+ args: [{ selector: 'lib-look-plano-collection-form', template: "<section class=\"look-form\">\r\n <!-- Top bar (bound to the outer form) -->\r\n <div class=\"card p-4 mb-4\" [formGroup]=\"form\">\r\n <div class=\"row align-items-end\">\r\n <div class=\"col-md-4\">\r\n <label class=\"form-label fw-bold\">Name *</label>\r\n <input type=\"text\" class=\"form-control\" formControlName=\"name\" placeholder=\"e.g. Master Look Plan FY26\" />\r\n </div>\r\n <div class=\"col-md-5\">\r\n <label class=\"form-label fw-bold\">Description</label>\r\n <input type=\"text\" class=\"form-control\" formControlName=\"description\" placeholder=\"Optional description\" />\r\n </div>\r\n <div class=\"col-md-1 text-center\">\r\n <label class=\"form-label fw-bold d-block\">Active</label>\r\n <input type=\"checkbox\" class=\"form-check-input\" formControlName=\"isActive\" />\r\n </div>\r\n <div class=\"col-md-2 text-end\">\r\n <button type=\"button\" class=\"btn btn-light me-2\" (click)=\"cancel()\">Cancel</button>\r\n <button type=\"button\" class=\"btn btn-primary\" [disabled]=\"saving\" (click)=\"save()\">\r\n {{ saving ? 'Saving\u2026' : 'Save' }}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Three-pane editor -->\r\n <div class=\"row mx-0 g-3\">\r\n <!-- Left: Looks list (no form binding needed \u2014 just navigation) -->\r\n <div class=\"col-md-3\">\r\n <div class=\"card p-3 h-100\">\r\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\r\n <h6 class=\"m-0\">MBQ Bucket - Fixture Combinations</h6>\r\n <button type=\"button\" class=\"btn btn-sm btn-primary\" (click)=\"addLook()\">+ Add Look</button>\r\n </div>\r\n <div *ngIf=\"looks.length === 0\" class=\"text-muted small\">No looks yet. Add one to start.</div>\r\n <div class=\"looks-tree\">\r\n <div\r\n *ngFor=\"let look of looks.controls; let i = index; trackBy: trackByIndex\"\r\n class=\"look-row d-flex justify-content-between align-items-center px-2 py-2 mb-1 rounded\"\r\n [class.selected]=\"selectedLookIndex === i\"\r\n [style.background-color]=\"bucketColor($any(look))\"\r\n (click)=\"selectLook(i)\"\r\n >\r\n <span class=\"d-flex align-items-center gap-2 text-truncate\">\r\n <span class=\"bucket-dot\" [style.background-color]=\"bucketColor($any(look))\"></span>\r\n <span class=\"text-truncate\">{{ lookTitle($any(look)) }}</span>\r\n <ng-container *ngIf=\"fixtureTypeBadge($any(look)) as ftb\">\r\n <span class=\"fixture-type-badge\" [ngClass]=\"ftb.cls\">{{ ftb.label }}</span>\r\n </ng-container>\r\n </span>\r\n <span class=\"look-row-actions\">\r\n <button type=\"button\" class=\"row-action-btn duplicate\" title=\"Duplicate look\"\r\n (click)=\"$event.stopPropagation(); duplicateLook(i)\">\r\n <i class=\"bi bi-files\"></i>\r\n </button>\r\n <button type=\"button\" class=\"row-action-btn remove\" title=\"Remove look\"\r\n (click)=\"$event.stopPropagation(); removeLook(i)\">\r\n <i class=\"bi bi-trash3\"></i>\r\n </button>\r\n </span>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Centre: selected Look metadata + slots -->\r\n <div class=\"col-md-3\">\r\n <ng-container *ngIf=\"selectedLookGroup as lookGroup; else noLook\">\r\n <div class=\"card p-3 h-100\" [formGroup]=\"lookGroup\">\r\n <h6 class=\"mb-3\">Fixture Combination Details</h6>\r\n\r\n <div class=\"row g-2 mb-3\">\r\n <div class=\"col-md-6\">\r\n <label class=\"form-label small\">MBQ bucket</label>\r\n <lib-searchable-select\r\n [items]=\"mbqBucketItems\"\r\n idField=\"value\"\r\n nameField=\"label\"\r\n placeholder=\"Select MBQ bucket\"\r\n formControlName=\"mbqBucket\">\r\n </lib-searchable-select>\r\n </div>\r\n <div class=\"col-md-6\">\r\n <label class=\"form-label small\">Fixture count *</label>\r\n <input type=\"number\" min=\"0\" max=\"30\" class=\"form-control form-control-sm\" formControlName=\"fixtureCount\" />\r\n </div>\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small\">CAD Header Varients</label>\r\n <lib-chips-input\r\n class=\"cad-header-variants-input\"\r\n formControlName=\"cadHeaderVariants\"\r\n placeholder=\"Type a variant and press Enter\">\r\n </lib-chips-input>\r\n </div>\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small\">Fixture Positions</label>\r\n <multiselect-chip-dropdown\r\n [items]=\"fixtureTypeOptions\"\r\n idField=\"id\"\r\n nameField=\"name\"\r\n placeholder=\"Select fixture position\"\r\n formControlName=\"fixtureType\">\r\n </multiselect-chip-dropdown>\r\n </div>\r\n </div>\r\n\r\n <hr />\r\n\r\n <div class=\"d-flex justify-content-between align-items-center mb-2\">\r\n <h6 class=\"m-0\">Fixtures</h6>\r\n <button type=\"button\" class=\"btn btn-sm btn-light\" (click)=\"addSlot()\">+ Add Fixture</button>\r\n </div>\r\n <div class=\"slots-list\">\r\n <div\r\n *ngFor=\"let slot of (selectedSlotsArray?.controls || []); let s = index; trackBy: trackByIndex\"\r\n class=\"slot-row d-flex justify-content-between align-items-center px-2 py-2 mb-1 rounded\"\r\n [class.selected]=\"selectedSlotIndex === s\"\r\n (click)=\"selectSlot(s)\"\r\n >\r\n <div class=\"d-flex flex-column\">\r\n <span class=\"fw-bold small\">\r\n Fixture {{ $any(slot).get('slotIndex')?.value }} \u2014 {{ $any(slot).get('slotType')?.value }}\r\n </span>\r\n <span class=\"text-muted small\">\r\n {{ $any(slot).get('brand')?.value || '(no brand)' }} \u00B7\r\n {{ $any(slot).get('fixtureLevelZone')?.value || '(no zone)' }}\r\n </span>\r\n </div>\r\n <button type=\"button\" class=\"btn btn-sm btn-light-danger ms-2\" (click)=\"$event.stopPropagation(); removeSlot(s)\">\u00D7</button>\r\n </div>\r\n </div>\r\n </div>\r\n </ng-container>\r\n <ng-template #noLook>\r\n <div class=\"card p-5 h-100 text-center text-muted\">Select or add a Look to edit it.</div>\r\n </ng-template>\r\n </div>\r\n\r\n <!-- Right: selected slot \u2014 placements editor -->\r\n <div class=\"col-md-6\">\r\n <ng-container *ngIf=\"selectedSlotGroup as slotGroup; else noSlot\">\r\n <div class=\"card p-3 h-100\" [formGroup]=\"slotGroup\">\r\n <h6 class=\"mb-3\">Fixture {{ selectedSlotIndex + 1 }} - Definition</h6>\r\n\r\n <div class=\"row g-2 mb-3\">\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small text-muted\">Slot index (auto)</label>\r\n <input type=\"text\" class=\"form-control form-control-sm\" [value]=\"selectedSlotIndex + 1\" readonly disabled />\r\n </div>\r\n <div class=\"col-md-6\">\r\n <label class=\"form-label small\">Fixture Type</label>\r\n <select class=\"form-select form-select-sm\" formControlName=\"slotType\">\r\n <option *ngFor=\"let opt of availableSlotTypes\" [value]=\"opt.id\">{{ opt.name }}</option>\r\n </select>\r\n </div>\r\n <div class=\"col-md-6\">\r\n <label class=\"form-label small\">Brand</label>\r\n <lib-searchable-select\r\n [items]=\"brandItems\"\r\n idField=\"value\"\r\n nameField=\"label\"\r\n placeholder=\"Select brand\"\r\n formControlName=\"brand\">\r\n </lib-searchable-select>\r\n </div>\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small\">Fixture Header</label>\r\n <lib-searchable-select\r\n [items]=\"fixtureHeaderItems\"\r\n idField=\"value\"\r\n nameField=\"label\"\r\n placeholder=\"Select fixture header\"\r\n formControlName=\"fixtureLevelZone\">\r\n </lib-searchable-select>\r\n </div>\r\n <div class=\"col-md-12\">\r\n <label class=\"form-label small\">Fixture Templates</label>\r\n <multiselect-chip-dropdown\r\n class=\"fixture-libraries-select\"\r\n [items]=\"fixtureLibraryItemsForSlot\"\r\n idField=\"id\"\r\n nameField=\"name\"\r\n [search]=\"true\"\r\n searchField=\"name\"\r\n placeholder=\"Select fixture templates\"\r\n formControlName=\"fixtureLibraryRefs\">\r\n </multiselect-chip-dropdown>\r\n <small class=\"text-muted\" *ngIf=\"slotGroup.get('slotType')?.value === 'eurocenter'\">\r\n Showing floor fixtures (eurocenter slot).\r\n </small>\r\n <small class=\"text-muted\" *ngIf=\"slotGroup.get('slotType')?.value !== 'eurocenter'\">\r\n Showing wall fixtures (shelf slot).\r\n </small>\r\n </div>\r\n </div>\r\n\r\n <hr />\r\n\r\n <div class=\"d-flex justify-content-between align-items-center mb-2\">\r\n <h6 class=\"m-0\">Look and Visual Merchandising Placements</h6>\r\n <button type=\"button\" class=\"btn btn-sm btn-light\" (click)=\"addPlacement()\">+ Add placement</button>\r\n </div>\r\n <div *ngIf=\"(selectedPlacements?.length || 0) === 0\" class=\"text-muted small mb-2\">\r\n No placements yet. Click <strong>+ Add placement</strong> to add one.\r\n </div>\r\n <div class=\"placements-scroll\" formArrayName=\"placements\">\r\n <div\r\n *ngFor=\"let p of (selectedPlacements?.controls || []); let pi = index; trackBy: trackByIndex\"\r\n class=\"placement-row mb-2\"\r\n [formGroupName]=\"pi\"\r\n >\r\n <div class=\"d-flex align-items-start gap-2 placement-fields\">\r\n <div class=\"ph-field\">\r\n <select class=\"form-select form-select-sm\" formControlName=\"kind\">\r\n <option value=\"pid\">Product Merchandising</option>\r\n <option value=\"vm\">Visual Merchandising</option>\r\n </select>\r\n </div>\r\n\r\n <!-- Visual Merchandising rows: VM Type (single) + VM Artwork (single) -->\r\n <ng-container *ngIf=\"$any(p).get('kind')?.value === 'vm'; else pidInputs\">\r\n <div class=\"ph-field\">\r\n <lib-searchable-select\r\n [items]=\"vmTypeOptions\"\r\n idField=\"value\"\r\n nameField=\"label\"\r\n placeholder=\"Visual Merchandising Type\"\r\n formControlName=\"position\">\r\n </lib-searchable-select>\r\n </div>\r\n <div class=\"ph-field\">\r\n <lib-searchable-select\r\n [items]=\"vmArtworkOptionsFor($any(p).get('position')?.value)\"\r\n idField=\"value\"\r\n nameField=\"label\"\r\n placeholder=\"Visual Merchandising Artwork\"\r\n formControlName=\"rawValue\">\r\n </lib-searchable-select>\r\n </div>\r\n </ng-container>\r\n\r\n <!-- Product Merchandising rows: Top/Mid/Bottom position + multi-select product/SKU -->\r\n <ng-template #pidInputs>\r\n <div class=\"ph-field\">\r\n <select class=\"form-select form-select-sm\" formControlName=\"position\">\r\n <option value=\"\">Position</option>\r\n <option value=\"Top\">Top</option>\r\n <option value=\"Mid\">Mid</option>\r\n <option value=\"Bottom\">Bottom</option>\r\n </select>\r\n </div>\r\n <div class=\"ph-field\">\r\n <lib-searchable-select\r\n [items]=\"productOptions\"\r\n idField=\"value\"\r\n nameField=\"label\"\r\n [multiple]=\"true\"\r\n placeholder=\"Product / SKU\"\r\n formControlName=\"rawValues\">\r\n </lib-searchable-select>\r\n </div>\r\n </ng-template>\r\n\r\n <button type=\"button\" class=\"btn btn-sm btn-light-danger ph-remove\" (click)=\"removePlacement(pi)\">\u00D7</button>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </ng-container>\r\n <ng-template #noSlot>\r\n <div class=\"card p-5 h-100 text-center text-muted\">Select a fixture slot to edit placements.</div>\r\n </ng-template>\r\n </div>\r\n </div>\r\n\r\n <!-- Per-slot Fixture Library Configuration card -->\r\n <ng-container *ngIf=\"selectedSlotGroup as slotGroupForMap\">\r\n <div class=\"card p-4 mt-4 library-config-card\" [formGroup]=\"slotGroupForMap\">\r\n <div class=\"mb-4\">\r\n <h5 class=\"form-label d-block mb-3\">Fixture library</h5>\r\n <div class=\"library-chips\">\r\n <button\r\n *ngFor=\"let lib of selectedSlotLibrariesForDropdown\"\r\n type=\"button\"\r\n class=\"library-chip\"\r\n [class.active]=\"selectedLibraryIdInMap === lib.id\"\r\n (click)=\"onLibraryMapSelect(selectedLibraryIdInMap === lib.id ? '' : lib.id)\">\r\n {{ lib.name }}\r\n </button>\r\n </div>\r\n <small *ngIf=\"selectedSlotLibrariesForDropdown.length === 0\" class=\"text-muted\">\r\n No libraries linked to this slot yet. Add some in the \"Fixture libraries\" multiselect above.\r\n </small>\r\n </div>\r\n\r\n <div *ngIf=\"loadingLibraryDetails\" class=\"text-muted\">Loading fixture details\u2026</div>\r\n\r\n <!--\r\n Use *ngFor + trackBy keyed on (look, slot, library) so Angular treats\r\n a switch as a new item \u2014 that forces the embedded template-products\r\n / template-vms components to remount and rerun their ngOnInit\r\n (otherwise their `isPageLoading` guard skips the form rebuild).\r\n -->\r\n <ng-container *ngFor=\"let _ of embeddedHostKeys; trackBy: trackByEmbeddedKey\">\r\n <ul class=\"nav nav-tabs custom-tabs mb-3\">\r\n <li class=\"nav-item\">\r\n <button type=\"button\" class=\"nav-link\"\r\n [class.active]=\"embeddedTab === 'products'\"\r\n (click)=\"embeddedTab = 'products'\">Product Merchandising</button>\r\n </li>\r\n <li class=\"nav-item\">\r\n <button type=\"button\" class=\"nav-link\"\r\n [class.active]=\"embeddedTab === 'vms'\"\r\n (click)=\"embeddedTab = 'vms'\">Visual Merchandising</button>\r\n </li>\r\n </ul>\r\n\r\n <div *ngIf=\"embeddedTab === 'products'\">\r\n <lib-template-products [looksMode]=\"true\"></lib-template-products>\r\n </div>\r\n <div *ngIf=\"embeddedTab === 'vms'\">\r\n <lib-template-vms [looksMode]=\"true\"></lib-template-vms>\r\n </div>\r\n </ng-container>\r\n </div>\r\n </ng-container>\r\n</section>\r\n", styles: [".look-form .col-md-3>.card,.look-form .col-md-6>.card{display:flex!important;flex-direction:column;height:85vh!important;min-height:720px}.look-form .looks-tree,.look-form .slots-list,.look-form .placements-scroll{flex:1 1 auto;min-height:0;overflow-y:auto}.look-form .look-row,.look-form .slot-row{cursor:pointer;border:1px solid transparent;background:#f9fafb;transition:filter .15s ease,box-shadow .15s ease,border-color .15s ease}.look-form .look-row:hover,.look-form .slot-row:hover{filter:brightness(.96)}.look-form .look-row.selected,.look-form .slot-row.selected{filter:brightness(.96);outline:1px solid rgba(14,165,233,.55);outline-offset:-1px}.look-form .bucket-dot{display:inline-block;width:10px;height:10px;border-radius:50%;flex-shrink:0;border:1px solid rgba(0,0,0,.12)}.look-form .look-row-actions{flex-shrink:0;display:flex;align-items:center;gap:2px;opacity:0;transition:opacity .15s ease}.look-form .look-row-actions .row-action-btn{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:0;border-radius:6px;background:transparent;color:#94a3b8;cursor:pointer;transition:background-color .15s ease,color .15s ease}.look-form .look-row-actions .row-action-btn i{font-size:13px;line-height:1}.look-form .look-row-actions .row-action-btn:hover{background:#fff}.look-form .look-row-actions .row-action-btn.duplicate:hover{color:#0ea5e9}.look-form .look-row-actions .row-action-btn.remove:hover{color:#ef4444}.look-form .look-row:hover .look-row-actions,.look-form .look-row.selected .look-row-actions{opacity:1}.look-form .fixture-type-badge{display:inline-block;flex-shrink:0;padding:1px 8px;border-radius:999px;font-size:10px;font-weight:600;line-height:14px;letter-spacing:.2px;border:1px solid transparent;white-space:nowrap}.look-form .fixture-type-badge.badge-wall{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}.look-form .fixture-type-badge.badge-floor{background:#fed7aa;color:#9a3412;border-color:#fdba74}.look-form .fixture-type-badge.badge-mixed{background:#ede9fe;color:#6d28d9;border-color:#c4b5fd}.look-form .fixture-libraries-select{display:block}.look-form .fixture-libraries-select ::ng-deep .multiselect-container{align-items:flex-start}.look-form .fixture-libraries-select ::ng-deep .chip-list{max-height:84px;overflow-y:auto;padding-right:4px}.look-form .cad-header-variants-input{display:block}.look-form .cad-header-variants-input ::ng-deep .chips-input{max-height:84px;overflow-y:auto;align-content:flex-start;padding-right:4px}.look-form .placement-row .placement-fields .ph-field{flex:1 1 0;min-width:0}.look-form .placement-row .placement-fields .ph-remove{flex:0 0 auto}.look-form .placement-row .placement-fields ::ng-deep multiselect-chip-dropdown .multiselect-container{padding:4px 10px;min-height:31px}.look-form .placement-row .placement-fields ::ng-deep multiselect-chip-dropdown .chip-list{min-height:21px}.look-form .placement-row .placement-fields ::ng-deep multiselect-chip-dropdown .multi-placeholder{font-size:13px;line-height:21px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.look-form .library-config-card .library-chips{display:flex;flex-wrap:wrap;gap:8px}.look-form .library-config-card .library-chip{background:#f1f5f9;border:1px solid #cbd5e1;color:#1e293b;border-radius:999px;padding:6px 14px;font-size:13px;font-weight:500;cursor:pointer;transition:background-color .15s ease,border-color .15s ease,color .15s ease}.look-form .library-config-card .library-chip:hover{background:#e2e8f0;border-color:#94a3b8}.look-form .library-config-card .library-chip.active{background:#0ea5e9;border-color:#0284c7;color:#fff}.look-form .library-config-card ::ng-deep #fixture-template-products .cols.col>ul.nav.nav-tabs.custom-tabs{display:none!important}.look-form .library-config-card ::ng-deep #fixture-template-products .cols.col-8>ul.nav.nav-tabs.custom-tabs{display:none!important}.look-form .library-config-card ::ng-deep #fixture-template-products input[readonly]{background-color:var(--bs-gray-200, #e9ecef);cursor:not-allowed}.look-form .library-config-card .fixture-preview{border:1px solid #e2e8f0;border-radius:8px;background:#fff;padding:12px}.look-form .library-config-card .fixture-preview .fx-header,.look-form .library-config-card .fixture-preview .fx-footer{background:#f1f5f9;border:1px dashed #cbd5e1;border-radius:6px;padding:6px 10px;font-size:12px;color:#475569;text-align:center;margin:4px 0}.look-form .library-config-card .fixture-preview .fx-body{display:flex;flex-direction:column;gap:4px;padding:4px 0}.look-form .library-config-card .fixture-preview .fx-shelf{background:linear-gradient(180deg,#f8fafc,#eef2f7);border:1px solid #cbd5e1;border-radius:4px;padding:8px 10px;display:flex;align-items:center;gap:8px;font-size:13px;min-height:36px}.look-form .library-config-card .fixture-preview .fx-shelf .fx-shelf-badge{background:#0ea5e9;color:#fff;font-weight:700;padding:2px 8px;border-radius:999px;font-size:11px;flex-shrink:0}.look-form .library-config-card .fixture-preview .fx-shelf .fx-shelf-label{font-weight:500;color:#1e293b}.look-form .library-config-card .fixture-preview .fx-shelf .fx-shelf-meta{color:#64748b;font-size:11px;margin-left:auto}.look-form .library-config-card .fixture-preview .fx-shelf.fx-shelf-tray{background:linear-gradient(180deg,#fef3c7,#fde68a);border-color:#f59e0b}.look-form .library-config-card .fixture-preview .fx-shelf.fx-shelf-tray .fx-shelf-badge{background:#d97706}.look-form .library-config-card .fixture-preview .fx-shelf.fx-shelf-vmonly{background:linear-gradient(180deg,#ede9fe,#ddd6fe);border-color:#8b5cf6}.look-form .library-config-card .fixture-preview .fx-shelf.fx-shelf-vmonly .fx-shelf-badge{background:#7c3aed}.look-form .library-config-card .fixture-preview .fx-dims{text-align:center}.look-form .library-config-card .shelf-mappings{max-height:70vh;overflow-y:auto;padding-right:4px}.look-form .library-config-card .shelf-mappings .shelf-mapping{border:1px solid #e5e7eb;border-radius:8px;padding:12px;background:#f9fafb}\n"] }]
70435
70714
  }], ctorParameters: () => [{ type: i1$2.FormBuilder }, { type: i2.ActivatedRoute }, { type: i2.Router }, { type: i2$1.GlobalStateService }, { type: StoreBuilderService }, { type: i4.ToastService }, { type: i2$1.PageInfoService }, { type: PlanoDataService }] });
70436
70715
 
70437
70716
  const routes$1 = [