tango-app-ui-store-builder 1.2.13 → 1.2.15

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.
@@ -725,12 +725,22 @@ class StoreBuilderService {
725
725
  return this.http.post(`${this.storeBuilderApiUrl}/runVmPlacementQuery`, data);
726
726
  }
727
727
  // Look Plano Collections
728
+ /** Whether the current session user may access the Looks feature
729
+ * (email present in planostaticdatas `allowedLookUsers`). */
730
+ getLookAccess() {
731
+ return this.http.get(`${this.storeBuilderApiUrl}/lookPlanoCollection/access`);
732
+ }
728
733
  getLookPlanoCollectionList(data) {
729
734
  return this.http.post(`${this.storeBuilderApiUrl}/lookPlanoCollection/getCollectionList`, data);
730
735
  }
731
736
  getLookPlanoCollectionDetails(collectionId) {
732
737
  return this.http.get(`${this.storeBuilderApiUrl}/lookPlanoCollection/getCollectionDetails`, { params: { collectionId } });
733
738
  }
739
+ /** MBQ bucket / Brand / Fixture Header dropdown options + combinations,
740
+ * seeded into planostaticdatas (type='lookComboOptions') from the sheet. */
741
+ getLookComboOptions(clientId) {
742
+ return this.http.get(`${this.storeBuilderApiUrl}/lookPlanoCollection/comboOptions`, { params: { clientId } });
743
+ }
734
744
  createLookPlanoCollection(data) {
735
745
  return this.http.post(`${this.storeBuilderApiUrl}/lookPlanoCollection/createCollection`, data);
736
746
  }
@@ -23246,6 +23256,9 @@ class TemplateVmsComponent {
23246
23256
  }
23247
23257
  }
23248
23258
  setPageData() {
23259
+ // When embedded inside the Looks form (looksMode), don't override the parent page's breadcrumb/title.
23260
+ if (this.looksMode)
23261
+ return;
23249
23262
  this.pageInfo.setTitle("Visual Merchandise");
23250
23263
  this.pageInfo.setBreadcrumbs([
23251
23264
  {
@@ -24468,6 +24481,9 @@ class TemplateProductsComponent {
24468
24481
  });
24469
24482
  }
24470
24483
  setPageData() {
24484
+ // When embedded inside the Looks form (looksMode), don't override the parent page's breadcrumb/title.
24485
+ if (this.looksMode)
24486
+ return;
24471
24487
  this.pageInfo.setTitle("Products");
24472
24488
  this.pageInfo.setBreadcrumbs([
24473
24489
  {
@@ -52292,8 +52308,10 @@ class StoreSelectComponent {
52292
52308
  let hasChanges = false;
52293
52309
  const valuesToProcess = this.singleSelect ? values.slice(0, 1) : values;
52294
52310
  valuesToProcess.forEach(val => {
52295
- // Find matching option by label (case-insensitive)
52296
- const matchedOption = this.options.find(opt => opt.label.toLowerCase() === val.toLowerCase());
52311
+ // Match by label (store name) OR id (store code), case-insensitive
52312
+ const needle = val.toLowerCase();
52313
+ const matchedOption = this.options.find(opt => opt.label.toLowerCase() === needle ||
52314
+ String(opt.id ?? '').toLowerCase() === needle);
52297
52315
  if (matchedOption && !this.selectedItems.has(matchedOption.id)) {
52298
52316
  if (this.singleSelect) {
52299
52317
  this.selectedItems.clear();
@@ -67815,6 +67833,8 @@ class StoreExportsComponent {
67815
67833
  storeOptions = [];
67816
67834
  initialStores = [];
67817
67835
  selectedStores = [];
67836
+ selectedRevisionStores = [];
67837
+ revisionName = '';
67818
67838
  starting = null;
67819
67839
  activeJob = null;
67820
67840
  busyInfo = null;
@@ -67827,6 +67847,11 @@ class StoreExportsComponent {
67827
67847
  this.gs = gs;
67828
67848
  }
67829
67849
  ngOnInit() {
67850
+ // Store options come from planograms and don't depend on the client/date filter,
67851
+ // so load them immediately. Previously loadStores() only ran inside the
67852
+ // dataRangeValue subscription, so the list stayed empty (no dropdown, no
67853
+ // type-to-match) whenever that observable didn't emit on this page.
67854
+ this.loadStores();
67830
67855
  this.gs.dataRangeValue
67831
67856
  .pipe(takeUntil(this.destroy$), debounceTime(200), filter$1((data) => !!data), distinctUntilChanged$1())
67832
67857
  .subscribe((data) => {
@@ -67851,6 +67876,12 @@ class StoreExportsComponent {
67851
67876
  onStoresSelected(stores) {
67852
67877
  this.selectedStores = stores;
67853
67878
  }
67879
+ onRevisionStoresSelected(stores) {
67880
+ this.selectedRevisionStores = stores;
67881
+ }
67882
+ get totalSelectedStores() {
67883
+ return this.selectedStores.length + this.selectedRevisionStores.length;
67884
+ }
67854
67885
  onExportMbq() {
67855
67886
  this.runExport('mbq');
67856
67887
  }
@@ -67880,18 +67911,30 @@ class StoreExportsComponent {
67880
67911
  runExport(kind) {
67881
67912
  if (this.isWorking)
67882
67913
  return;
67883
- if (this.selectedStores.length === 0) {
67914
+ const liveStores = this.selectedStores.map((s) => s.label);
67915
+ const revisionStores = this.selectedRevisionStores.map((s) => s.label);
67916
+ const revisionName = this.revisionName.trim();
67917
+ if (liveStores.length === 0 && revisionStores.length === 0) {
67884
67918
  this.showMessage('Please select at least one store.', 'error');
67885
67919
  return;
67886
67920
  }
67887
- const storeName = this.selectedStores.map((s) => s.label);
67921
+ if (revisionStores.length > 0 && !revisionName) {
67922
+ this.showMessage('Enter a revision name for the revision stores.', 'error');
67923
+ return;
67924
+ }
67925
+ const payload = { storeName: liveStores };
67926
+ if (revisionStores.length > 0) {
67927
+ payload.revisionStores = revisionStores;
67928
+ payload.revisionName = revisionName;
67929
+ }
67930
+ const totalStores = liveStores.length + revisionStores.length;
67888
67931
  this.busyInfo = null;
67889
67932
  this.activeJob = null;
67890
67933
  this.starting = kind;
67891
67934
  this.message = '';
67892
67935
  const submit$ = kind === 'mbq'
67893
- ? this.sbs.exportMBQSheetAsync({ storeName })
67894
- : this.sbs.exportPlanoOrderSheetAsync({ storeName });
67936
+ ? this.sbs.exportMBQSheetAsync(payload)
67937
+ : this.sbs.exportPlanoOrderSheetAsync(payload);
67895
67938
  submit$.subscribe({
67896
67939
  next: (resp) => {
67897
67940
  this.starting = null;
@@ -67906,7 +67949,7 @@ class StoreExportsComponent {
67906
67949
  kind,
67907
67950
  status: 'running',
67908
67951
  startedAt: new Date().toISOString(),
67909
- storeCount: storeName.length,
67952
+ storeCount: totalStores,
67910
67953
  };
67911
67954
  this.startPolling(jobId);
67912
67955
  },
@@ -68000,11 +68043,11 @@ class StoreExportsComponent {
68000
68043
  this.destroy$.complete();
68001
68044
  }
68002
68045
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: StoreExportsComponent, deps: [{ token: StoreBuilderService }, { token: i2$1.GlobalStateService }], target: i0.ɵɵFactoryTarget.Component });
68003
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: StoreExportsComponent, selector: "lib-store-exports", ngImport: i0, template: "<section class=\"store-exports p-3\">\r\n <div class=\"card p-4\" style=\"max-width: 700px;\">\r\n <h5 class=\"mb-1\">Store Exports</h5>\r\n <p class=\"text-muted small mb-3\">\r\n Generate per-store fixture exports. Select one or more stores, then choose an export.\r\n Large exports run in the background and are saved to S3 \u2014 a download link appears here when ready.\r\n </p>\r\n\r\n <div class=\"mb-3\">\r\n <label class=\"form-label\">Stores</label>\r\n <lib-store-select\r\n [options]=\"storeOptions\"\r\n [initialSelected]=\"initialStores\"\r\n (selectedChange)=\"onStoresSelected($event)\">\r\n </lib-store-select>\r\n </div>\r\n\r\n <div class=\"d-flex gap-2 flex-wrap\">\r\n <button\r\n class=\"btn btn-primary\"\r\n (click)=\"onExportMbq()\"\r\n [disabled]=\"isWorking || selectedStores.length === 0\">\r\n @if (starting === 'mbq') {\r\n Starting MBQ...\r\n } @else if (activeJob?.status === 'running' && activeJob?.kind === 'mbq') {\r\n Running MBQ...\r\n } @else {\r\n Export MBQ\r\n }\r\n </button>\r\n\r\n <button\r\n class=\"btn btn-success\"\r\n (click)=\"onExportOrderSheet()\"\r\n [disabled]=\"isWorking || selectedStores.length === 0\">\r\n @if (starting === 'order') {\r\n Starting Order Sheet...\r\n } @else if (activeJob?.status === 'running' && activeJob?.kind === 'order') {\r\n Running Order Sheet...\r\n } @else {\r\n Export Order Sheet\r\n }\r\n </button>\r\n </div>\r\n\r\n @if (busyInfo) {\r\n <div class=\"mt-3 alert alert-warning\">\r\n Another {{ kindLabel(busyInfo.kind) }} export is currently running@if (busyInfo.storeCount) { ({{ busyInfo.storeCount }} store{{ busyInfo.storeCount === 1 ? '' : 's' }}) }@if (busyInfo.startedAt) {, started {{ startedAgo(busyInfo.startedAt) }}}. Please wait until it finishes before starting a new export.\r\n </div>\r\n }\r\n\r\n @if (activeJob) {\r\n <div\r\n class=\"mt-3 alert\"\r\n [class.alert-info]=\"activeJob.status === 'running'\"\r\n [class.alert-success]=\"activeJob.status === 'succeeded'\"\r\n [class.alert-danger]=\"activeJob.status === 'failed'\">\r\n @if (activeJob.status === 'running') {\r\n <div>\r\n {{ kindLabel(activeJob.kind) }} export is processing@if (activeJob.storeCount) { ({{ activeJob.storeCount }} store{{ activeJob.storeCount === 1 ? '' : 's' }}) }. Started {{ startedAgo(activeJob.startedAt) }}. Keep this page open \u2014 a download link will appear when it finishes.\r\n </div>\r\n } @else if (activeJob.status === 'succeeded' && activeJob.downloadUrl) {\r\n <div class=\"d-flex align-items-center justify-content-between gap-2 flex-wrap\">\r\n <span>\r\n {{ kindLabel(activeJob.kind) }} export ready@if (activeJob.rowCount) { \u2014 {{ activeJob.rowCount }} row{{ activeJob.rowCount === 1 ? '' : 's' }} }.\r\n </span>\r\n <div class=\"d-flex gap-2\">\r\n <a [href]=\"activeJob.downloadUrl\" class=\"btn btn-sm btn-success\" target=\"_blank\" rel=\"noopener\">Download</a>\r\n <button class=\"btn btn-sm btn-outline-secondary\" (click)=\"dismissResult()\">Dismiss</button>\r\n </div>\r\n </div>\r\n } @else if (activeJob.status === 'failed') {\r\n <div class=\"d-flex align-items-center justify-content-between gap-2 flex-wrap\">\r\n <span>{{ kindLabel(activeJob.kind) }} export failed: {{ activeJob.error || 'Unknown error' }}</span>\r\n <button class=\"btn btn-sm btn-outline-secondary\" (click)=\"dismissResult()\">Dismiss</button>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n @if (message) {\r\n <div\r\n class=\"mt-3 alert\"\r\n [class.alert-success]=\"messageType === 'success'\"\r\n [class.alert-danger]=\"messageType === 'error'\">\r\n {{ message }}\r\n </div>\r\n }\r\n </div>\r\n</section>\r\n", styles: [".store-exports .card{border:1px solid #e0e0e0}\n"], dependencies: [{ kind: "component", type: StoreSelectComponent, selector: "lib-store-select", inputs: ["options", "initialSelected", "singleSelect"], outputs: ["selectedChange"] }] });
68046
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: StoreExportsComponent, selector: "lib-store-exports", ngImport: i0, template: "<section class=\"store-exports p-3\">\r\n <div class=\"card p-4\" style=\"max-width: 700px;\">\r\n <h5 class=\"mb-1\">Store Exports</h5>\r\n <p class=\"text-muted small mb-3\">\r\n Generate per-store fixture exports. Select one or more stores, then choose an export.\r\n Large exports run in the background and are saved to S3 \u2014 a download link appears here when ready.\r\n </p>\r\n\r\n <div class=\"mb-3\">\r\n <label class=\"form-label\">Stores \u2014 Manage Planogram (live)</label>\r\n <lib-store-select\r\n [options]=\"storeOptions\"\r\n [initialSelected]=\"initialStores\"\r\n (selectedChange)=\"onStoresSelected($event)\">\r\n </lib-store-select>\r\n </div>\r\n\r\n <div class=\"mb-3\">\r\n <label class=\"form-label\">Stores \u2014 from a Revision</label>\r\n <lib-store-select\r\n [options]=\"storeOptions\"\r\n [initialSelected]=\"initialStores\"\r\n (selectedChange)=\"onRevisionStoresSelected($event)\">\r\n </lib-store-select>\r\n </div>\r\n\r\n <div class=\"mb-3\">\r\n <label class=\"form-label\">Revision name</label>\r\n <input\r\n type=\"text\"\r\n class=\"form-control\"\r\n [(ngModel)]=\"revisionName\"\r\n name=\"revisionName\"\r\n autocomplete=\"off\"\r\n placeholder=\"e.g. Q1-2026 Layout\"\r\n [disabled]=\"isWorking\" />\r\n <div class=\"form-text\">\r\n Applies to the \u201Cfrom a Revision\u201D stores above \u2014 each is exported from its revision with this exact name (stores without it are skipped). Live stores use current data; both sets are combined into one export.\r\n </div>\r\n </div>\r\n\r\n <div class=\"d-flex gap-2 flex-wrap\">\r\n <button\r\n class=\"btn btn-primary\"\r\n (click)=\"onExportMbq()\"\r\n [disabled]=\"isWorking || totalSelectedStores === 0\">\r\n @if (starting === 'mbq') {\r\n Starting MBQ...\r\n } @else if (activeJob?.status === 'running' && activeJob?.kind === 'mbq') {\r\n Running MBQ...\r\n } @else {\r\n Export MBQ\r\n }\r\n </button>\r\n\r\n <button\r\n class=\"btn btn-success\"\r\n (click)=\"onExportOrderSheet()\"\r\n [disabled]=\"isWorking || totalSelectedStores === 0\">\r\n @if (starting === 'order') {\r\n Starting Order Sheet...\r\n } @else if (activeJob?.status === 'running' && activeJob?.kind === 'order') {\r\n Running Order Sheet...\r\n } @else {\r\n Export Order Sheet\r\n }\r\n </button>\r\n </div>\r\n\r\n @if (busyInfo) {\r\n <div class=\"mt-3 alert alert-warning\">\r\n Another {{ kindLabel(busyInfo.kind) }} export is currently running@if (busyInfo.storeCount) { ({{ busyInfo.storeCount }} store{{ busyInfo.storeCount === 1 ? '' : 's' }}) }@if (busyInfo.startedAt) {, started {{ startedAgo(busyInfo.startedAt) }}}. Please wait until it finishes before starting a new export.\r\n </div>\r\n }\r\n\r\n @if (activeJob) {\r\n <div\r\n class=\"mt-3 alert\"\r\n [class.alert-info]=\"activeJob.status === 'running'\"\r\n [class.alert-success]=\"activeJob.status === 'succeeded'\"\r\n [class.alert-danger]=\"activeJob.status === 'failed'\">\r\n @if (activeJob.status === 'running') {\r\n <div>\r\n {{ kindLabel(activeJob.kind) }} export is processing@if (activeJob.storeCount) { ({{ activeJob.storeCount }} store{{ activeJob.storeCount === 1 ? '' : 's' }}) }. Started {{ startedAgo(activeJob.startedAt) }}. Keep this page open \u2014 a download link will appear when it finishes.\r\n </div>\r\n } @else if (activeJob.status === 'succeeded' && activeJob.downloadUrl) {\r\n <div class=\"d-flex align-items-center justify-content-between gap-2 flex-wrap\">\r\n <span>\r\n {{ kindLabel(activeJob.kind) }} export ready@if (activeJob.rowCount) { \u2014 {{ activeJob.rowCount }} row{{ activeJob.rowCount === 1 ? '' : 's' }} }.\r\n </span>\r\n <div class=\"d-flex gap-2\">\r\n <a [href]=\"activeJob.downloadUrl\" class=\"btn btn-sm btn-success\" target=\"_blank\" rel=\"noopener\">Download</a>\r\n <button class=\"btn btn-sm btn-outline-secondary\" (click)=\"dismissResult()\">Dismiss</button>\r\n </div>\r\n </div>\r\n } @else if (activeJob.status === 'failed') {\r\n <div class=\"d-flex align-items-center justify-content-between gap-2 flex-wrap\">\r\n <span>{{ kindLabel(activeJob.kind) }} export failed: {{ activeJob.error || 'Unknown error' }}</span>\r\n <button class=\"btn btn-sm btn-outline-secondary\" (click)=\"dismissResult()\">Dismiss</button>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n @if (message) {\r\n <div\r\n class=\"mt-3 alert\"\r\n [class.alert-success]=\"messageType === 'success'\"\r\n [class.alert-danger]=\"messageType === 'error'\">\r\n {{ message }}\r\n </div>\r\n }\r\n </div>\r\n</section>\r\n", styles: [".store-exports .card{border:1px solid #e0e0e0}\n"], dependencies: [{ 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: "component", type: StoreSelectComponent, selector: "lib-store-select", inputs: ["options", "initialSelected", "singleSelect"], outputs: ["selectedChange"] }] });
68004
68047
  }
68005
68048
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: StoreExportsComponent, decorators: [{
68006
68049
  type: Component,
68007
- args: [{ selector: 'lib-store-exports', template: "<section class=\"store-exports p-3\">\r\n <div class=\"card p-4\" style=\"max-width: 700px;\">\r\n <h5 class=\"mb-1\">Store Exports</h5>\r\n <p class=\"text-muted small mb-3\">\r\n Generate per-store fixture exports. Select one or more stores, then choose an export.\r\n Large exports run in the background and are saved to S3 \u2014 a download link appears here when ready.\r\n </p>\r\n\r\n <div class=\"mb-3\">\r\n <label class=\"form-label\">Stores</label>\r\n <lib-store-select\r\n [options]=\"storeOptions\"\r\n [initialSelected]=\"initialStores\"\r\n (selectedChange)=\"onStoresSelected($event)\">\r\n </lib-store-select>\r\n </div>\r\n\r\n <div class=\"d-flex gap-2 flex-wrap\">\r\n <button\r\n class=\"btn btn-primary\"\r\n (click)=\"onExportMbq()\"\r\n [disabled]=\"isWorking || selectedStores.length === 0\">\r\n @if (starting === 'mbq') {\r\n Starting MBQ...\r\n } @else if (activeJob?.status === 'running' && activeJob?.kind === 'mbq') {\r\n Running MBQ...\r\n } @else {\r\n Export MBQ\r\n }\r\n </button>\r\n\r\n <button\r\n class=\"btn btn-success\"\r\n (click)=\"onExportOrderSheet()\"\r\n [disabled]=\"isWorking || selectedStores.length === 0\">\r\n @if (starting === 'order') {\r\n Starting Order Sheet...\r\n } @else if (activeJob?.status === 'running' && activeJob?.kind === 'order') {\r\n Running Order Sheet...\r\n } @else {\r\n Export Order Sheet\r\n }\r\n </button>\r\n </div>\r\n\r\n @if (busyInfo) {\r\n <div class=\"mt-3 alert alert-warning\">\r\n Another {{ kindLabel(busyInfo.kind) }} export is currently running@if (busyInfo.storeCount) { ({{ busyInfo.storeCount }} store{{ busyInfo.storeCount === 1 ? '' : 's' }}) }@if (busyInfo.startedAt) {, started {{ startedAgo(busyInfo.startedAt) }}}. Please wait until it finishes before starting a new export.\r\n </div>\r\n }\r\n\r\n @if (activeJob) {\r\n <div\r\n class=\"mt-3 alert\"\r\n [class.alert-info]=\"activeJob.status === 'running'\"\r\n [class.alert-success]=\"activeJob.status === 'succeeded'\"\r\n [class.alert-danger]=\"activeJob.status === 'failed'\">\r\n @if (activeJob.status === 'running') {\r\n <div>\r\n {{ kindLabel(activeJob.kind) }} export is processing@if (activeJob.storeCount) { ({{ activeJob.storeCount }} store{{ activeJob.storeCount === 1 ? '' : 's' }}) }. Started {{ startedAgo(activeJob.startedAt) }}. Keep this page open \u2014 a download link will appear when it finishes.\r\n </div>\r\n } @else if (activeJob.status === 'succeeded' && activeJob.downloadUrl) {\r\n <div class=\"d-flex align-items-center justify-content-between gap-2 flex-wrap\">\r\n <span>\r\n {{ kindLabel(activeJob.kind) }} export ready@if (activeJob.rowCount) { \u2014 {{ activeJob.rowCount }} row{{ activeJob.rowCount === 1 ? '' : 's' }} }.\r\n </span>\r\n <div class=\"d-flex gap-2\">\r\n <a [href]=\"activeJob.downloadUrl\" class=\"btn btn-sm btn-success\" target=\"_blank\" rel=\"noopener\">Download</a>\r\n <button class=\"btn btn-sm btn-outline-secondary\" (click)=\"dismissResult()\">Dismiss</button>\r\n </div>\r\n </div>\r\n } @else if (activeJob.status === 'failed') {\r\n <div class=\"d-flex align-items-center justify-content-between gap-2 flex-wrap\">\r\n <span>{{ kindLabel(activeJob.kind) }} export failed: {{ activeJob.error || 'Unknown error' }}</span>\r\n <button class=\"btn btn-sm btn-outline-secondary\" (click)=\"dismissResult()\">Dismiss</button>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n @if (message) {\r\n <div\r\n class=\"mt-3 alert\"\r\n [class.alert-success]=\"messageType === 'success'\"\r\n [class.alert-danger]=\"messageType === 'error'\">\r\n {{ message }}\r\n </div>\r\n }\r\n </div>\r\n</section>\r\n", styles: [".store-exports .card{border:1px solid #e0e0e0}\n"] }]
68050
+ args: [{ selector: 'lib-store-exports', template: "<section class=\"store-exports p-3\">\r\n <div class=\"card p-4\" style=\"max-width: 700px;\">\r\n <h5 class=\"mb-1\">Store Exports</h5>\r\n <p class=\"text-muted small mb-3\">\r\n Generate per-store fixture exports. Select one or more stores, then choose an export.\r\n Large exports run in the background and are saved to S3 \u2014 a download link appears here when ready.\r\n </p>\r\n\r\n <div class=\"mb-3\">\r\n <label class=\"form-label\">Stores \u2014 Manage Planogram (live)</label>\r\n <lib-store-select\r\n [options]=\"storeOptions\"\r\n [initialSelected]=\"initialStores\"\r\n (selectedChange)=\"onStoresSelected($event)\">\r\n </lib-store-select>\r\n </div>\r\n\r\n <div class=\"mb-3\">\r\n <label class=\"form-label\">Stores \u2014 from a Revision</label>\r\n <lib-store-select\r\n [options]=\"storeOptions\"\r\n [initialSelected]=\"initialStores\"\r\n (selectedChange)=\"onRevisionStoresSelected($event)\">\r\n </lib-store-select>\r\n </div>\r\n\r\n <div class=\"mb-3\">\r\n <label class=\"form-label\">Revision name</label>\r\n <input\r\n type=\"text\"\r\n class=\"form-control\"\r\n [(ngModel)]=\"revisionName\"\r\n name=\"revisionName\"\r\n autocomplete=\"off\"\r\n placeholder=\"e.g. Q1-2026 Layout\"\r\n [disabled]=\"isWorking\" />\r\n <div class=\"form-text\">\r\n Applies to the \u201Cfrom a Revision\u201D stores above \u2014 each is exported from its revision with this exact name (stores without it are skipped). Live stores use current data; both sets are combined into one export.\r\n </div>\r\n </div>\r\n\r\n <div class=\"d-flex gap-2 flex-wrap\">\r\n <button\r\n class=\"btn btn-primary\"\r\n (click)=\"onExportMbq()\"\r\n [disabled]=\"isWorking || totalSelectedStores === 0\">\r\n @if (starting === 'mbq') {\r\n Starting MBQ...\r\n } @else if (activeJob?.status === 'running' && activeJob?.kind === 'mbq') {\r\n Running MBQ...\r\n } @else {\r\n Export MBQ\r\n }\r\n </button>\r\n\r\n <button\r\n class=\"btn btn-success\"\r\n (click)=\"onExportOrderSheet()\"\r\n [disabled]=\"isWorking || totalSelectedStores === 0\">\r\n @if (starting === 'order') {\r\n Starting Order Sheet...\r\n } @else if (activeJob?.status === 'running' && activeJob?.kind === 'order') {\r\n Running Order Sheet...\r\n } @else {\r\n Export Order Sheet\r\n }\r\n </button>\r\n </div>\r\n\r\n @if (busyInfo) {\r\n <div class=\"mt-3 alert alert-warning\">\r\n Another {{ kindLabel(busyInfo.kind) }} export is currently running@if (busyInfo.storeCount) { ({{ busyInfo.storeCount }} store{{ busyInfo.storeCount === 1 ? '' : 's' }}) }@if (busyInfo.startedAt) {, started {{ startedAgo(busyInfo.startedAt) }}}. Please wait until it finishes before starting a new export.\r\n </div>\r\n }\r\n\r\n @if (activeJob) {\r\n <div\r\n class=\"mt-3 alert\"\r\n [class.alert-info]=\"activeJob.status === 'running'\"\r\n [class.alert-success]=\"activeJob.status === 'succeeded'\"\r\n [class.alert-danger]=\"activeJob.status === 'failed'\">\r\n @if (activeJob.status === 'running') {\r\n <div>\r\n {{ kindLabel(activeJob.kind) }} export is processing@if (activeJob.storeCount) { ({{ activeJob.storeCount }} store{{ activeJob.storeCount === 1 ? '' : 's' }}) }. Started {{ startedAgo(activeJob.startedAt) }}. Keep this page open \u2014 a download link will appear when it finishes.\r\n </div>\r\n } @else if (activeJob.status === 'succeeded' && activeJob.downloadUrl) {\r\n <div class=\"d-flex align-items-center justify-content-between gap-2 flex-wrap\">\r\n <span>\r\n {{ kindLabel(activeJob.kind) }} export ready@if (activeJob.rowCount) { \u2014 {{ activeJob.rowCount }} row{{ activeJob.rowCount === 1 ? '' : 's' }} }.\r\n </span>\r\n <div class=\"d-flex gap-2\">\r\n <a [href]=\"activeJob.downloadUrl\" class=\"btn btn-sm btn-success\" target=\"_blank\" rel=\"noopener\">Download</a>\r\n <button class=\"btn btn-sm btn-outline-secondary\" (click)=\"dismissResult()\">Dismiss</button>\r\n </div>\r\n </div>\r\n } @else if (activeJob.status === 'failed') {\r\n <div class=\"d-flex align-items-center justify-content-between gap-2 flex-wrap\">\r\n <span>{{ kindLabel(activeJob.kind) }} export failed: {{ activeJob.error || 'Unknown error' }}</span>\r\n <button class=\"btn btn-sm btn-outline-secondary\" (click)=\"dismissResult()\">Dismiss</button>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n @if (message) {\r\n <div\r\n class=\"mt-3 alert\"\r\n [class.alert-success]=\"messageType === 'success'\"\r\n [class.alert-danger]=\"messageType === 'error'\">\r\n {{ message }}\r\n </div>\r\n }\r\n </div>\r\n</section>\r\n", styles: [".store-exports .card{border:1px solid #e0e0e0}\n"] }]
68008
68051
  }], ctorParameters: () => [{ type: StoreBuilderService }, { type: i2$1.GlobalStateService }] });
68009
68052
 
68010
68053
  class ModifyFixtureNumbersComponent {
@@ -68791,6 +68834,11 @@ class LookPlanoCollectionComponent {
68791
68834
  loading = false;
68792
68835
  items = [];
68793
68836
  totalItems = 0;
68837
+ // Access control — only emails in planostaticdatas `allowedLookUsers` may
68838
+ // use the Looks feature. accessChecked gates the initial render so we don't
68839
+ // flash the list before the check resolves.
68840
+ accessChecked = false;
68841
+ allowed = false;
68794
68842
  searchTerm = '';
68795
68843
  limit = 10;
68796
68844
  offset = 1;
@@ -68806,8 +68854,12 @@ class LookPlanoCollectionComponent {
68806
68854
  this.toast = toast;
68807
68855
  this.pageInfo = pageInfo;
68808
68856
  }
68809
- ngOnInit() {
68857
+ async ngOnInit() {
68810
68858
  this.setPageData();
68859
+ // Gate the whole page on the allowlist before wiring any data loads.
68860
+ await this.checkAccess();
68861
+ if (!this.allowed)
68862
+ return;
68811
68863
  this.gs.dataRangeValue.pipe(takeUntil(this.destroy$)).subscribe((data) => {
68812
68864
  const headerFilters = JSON.parse(localStorage.getItem('header-filters') || '{}');
68813
68865
  this.clientId = data?.client || headerFilters?.client;
@@ -68824,6 +68876,20 @@ class LookPlanoCollectionComponent {
68824
68876
  }
68825
68877
  });
68826
68878
  }
68879
+ /** Resolve whether the session user may access Looks. Never throws. */
68880
+ async checkAccess() {
68881
+ try {
68882
+ const res = await lastValueFrom(this.builderService.getLookAccess());
68883
+ this.allowed = !!res?.data?.allowed;
68884
+ }
68885
+ catch (err) {
68886
+ console.log('@@ ~ checkAccess [ERR]:', err);
68887
+ this.allowed = false;
68888
+ }
68889
+ finally {
68890
+ this.accessChecked = true;
68891
+ }
68892
+ }
68827
68893
  setPageData() {
68828
68894
  this.pageInfo.setTitle('Looks');
68829
68895
  this.pageInfo.setBreadcrumbs([
@@ -68834,7 +68900,6 @@ class LookPlanoCollectionComponent {
68834
68900
  isSeparator: false,
68835
68901
  },
68836
68902
  { title: '', path: '', isActive: false, isSeparator: true },
68837
- { title: 'Looks', path: '/manage/planogram/looks', isActive: true, isSeparator: false },
68838
68903
  ]);
68839
68904
  }
68840
68905
  async loadList() {
@@ -68917,11 +68982,11 @@ class LookPlanoCollectionComponent {
68917
68982
  this.destroy$.complete();
68918
68983
  }
68919
68984
  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 });
68920
- 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" }] });
68985
+ 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 <!-- Checking access -->\r\n <div *ngIf=\"!accessChecked\" class=\"row mx-0 my-5\">\r\n <div class=\"card p-5 col-md-12 text-center text-muted py-10\">Checking access\u2026</div>\r\n </div>\r\n\r\n <!-- Access denied -->\r\n <div *ngIf=\"accessChecked && !allowed\" class=\"row mx-0 my-5\">\r\n <div class=\"card p-5 col-md-12\">\r\n <div class=\"access-denied text-center py-10\">\r\n <div class=\"access-denied-icon mb-4\">\r\n <i class=\"bi bi-shield-lock\"></i>\r\n </div>\r\n <h3 class=\"mb-2\">Access denied</h3>\r\n <p class=\"text-muted mb-0\">\r\n You don't have permission to access Looks. Contact your administrator\r\n to be added to the allowed users list.\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div *ngIf=\"accessChecked && allowed\" 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 <span *ngIf=\"item.isActive\" class=\"badge badge-light-success ms-2 active-look-badge\">\r\n <span class=\"active-dot\"></span>Active\r\n </span>\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}.looks-wrapper .access-denied .access-denied-icon i{font-size:48px;color:#ef4444;line-height:1}.looks-wrapper .active-look-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 9px;border-radius:999px;font-size:11px;font-weight:600;line-height:16px;color:#15803d;background:#dcfce7;border:1px solid #86efac;vertical-align:middle}.looks-wrapper .active-look-badge .active-dot{width:6px;height:6px;border-radius:50%;background:#22c55e;flex-shrink:0}\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" }] });
68921
68986
  }
68922
68987
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LookPlanoCollectionComponent, decorators: [{
68923
68988
  type: Component,
68924
- 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"] }]
68989
+ args: [{ selector: 'lib-look-plano-collection', template: "<section class=\"looks-wrapper\">\r\n <!-- Checking access -->\r\n <div *ngIf=\"!accessChecked\" class=\"row mx-0 my-5\">\r\n <div class=\"card p-5 col-md-12 text-center text-muted py-10\">Checking access\u2026</div>\r\n </div>\r\n\r\n <!-- Access denied -->\r\n <div *ngIf=\"accessChecked && !allowed\" class=\"row mx-0 my-5\">\r\n <div class=\"card p-5 col-md-12\">\r\n <div class=\"access-denied text-center py-10\">\r\n <div class=\"access-denied-icon mb-4\">\r\n <i class=\"bi bi-shield-lock\"></i>\r\n </div>\r\n <h3 class=\"mb-2\">Access denied</h3>\r\n <p class=\"text-muted mb-0\">\r\n You don't have permission to access Looks. Contact your administrator\r\n to be added to the allowed users list.\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div *ngIf=\"accessChecked && allowed\" 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 <span *ngIf=\"item.isActive\" class=\"badge badge-light-success ms-2 active-look-badge\">\r\n <span class=\"active-dot\"></span>Active\r\n </span>\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}.looks-wrapper .access-denied .access-denied-icon i{font-size:48px;color:#ef4444;line-height:1}.looks-wrapper .active-look-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 9px;border-radius:999px;font-size:11px;font-weight:600;line-height:16px;color:#15803d;background:#dcfce7;border:1px solid #86efac;vertical-align:middle}.looks-wrapper .active-look-badge .active-dot{width:6px;height:6px;border-radius:50%;background:#22c55e;flex-shrink:0}\n"] }]
68925
68990
  }], ctorParameters: () => [{ type: i2.Router }, { type: i1$1.NgbModal }, { type: i2$1.GlobalStateService }, { type: StoreBuilderService }, { type: i4.ToastService }, { type: i2$1.PageInfoService }] });
68926
68991
 
68927
68992
  class SearchableSelectComponent {
@@ -69292,6 +69357,10 @@ class LookPlanoCollectionFormComponent {
69292
69357
  isNew = true;
69293
69358
  loading = false;
69294
69359
  saving = false;
69360
+ // Access control — mirrors the Looks list page. Only emails in
69361
+ // planostaticdatas `allowedLookUsers` may open the form.
69362
+ accessChecked = false;
69363
+ allowed = false;
69295
69364
  form;
69296
69365
  selectedLookIndex = -1;
69297
69366
  selectedSlotIndex = -1;
@@ -69303,6 +69372,20 @@ class LookPlanoCollectionFormComponent {
69303
69372
  { id: 'eurocenter', name: 'Floor' },
69304
69373
  ];
69305
69374
  fixtureLibraryItems = [];
69375
+ // MBQ bucket / Brand / Fixture Header dropdown options, seeded from the
69376
+ // sheet into planostaticdatas (type='lookComboOptions'). The form fields
69377
+ // are searchable selects so the user picks a known value rather than typing.
69378
+ // All three are flat lists of every distinct value present in the sheet —
69379
+ // they are NOT cross-filtered, so e.g. the Brand dropdown always offers all
69380
+ // brands regardless of which MBQ bucket the look uses.
69381
+ mbqBucketItems = [];
69382
+ brandItems = [];
69383
+ fixtureHeaderItems = [];
69384
+ // Raw payload from the comboOptions endpoint, kept so we can rebuild the
69385
+ // option lists by merging it with the values already present on the loaded
69386
+ // collection (so the dropdowns are never empty even if the endpoint is
69387
+ // unavailable or hasn't loaded yet).
69388
+ comboRaw = { mbqBuckets: [], brands: [], fixtureHeaders: [], combinations: [] };
69306
69389
  // Dropdowns inside placement rows.
69307
69390
  vmTypeOptions = [];
69308
69391
  vmArtworkOptions = [];
@@ -69754,9 +69837,13 @@ class LookPlanoCollectionFormComponent {
69754
69837
  this.pageInfo = pageInfo;
69755
69838
  this.planoDataService = planoDataService;
69756
69839
  }
69757
- ngOnInit() {
69840
+ async ngOnInit() {
69758
69841
  this.setPageData();
69759
69842
  this.initForm();
69843
+ // Gate on the allowlist before wiring any data loads / subscriptions.
69844
+ await this.checkAccess();
69845
+ if (!this.allowed)
69846
+ return;
69760
69847
  // Listen to the shared fixtureTemplateDetails — when the embedded
69761
69848
  // TemplateProducts/TemplateVms components push edits back into the
69762
69849
  // BehaviorSubject, persist a snapshot under the active slot + library.
@@ -69782,6 +69869,7 @@ class LookPlanoCollectionFormComponent {
69782
69869
  if (this.clientId) {
69783
69870
  this.loadFixtureLibraries();
69784
69871
  this.loadPlacementDropdownData();
69872
+ this.loadComboOptions();
69785
69873
  }
69786
69874
  });
69787
69875
  this.route.params.pipe(takeUntil(this.destroy$)).subscribe(async (params) => {
@@ -69802,6 +69890,20 @@ class LookPlanoCollectionFormComponent {
69802
69890
  replaceUrl: true,
69803
69891
  });
69804
69892
  }
69893
+ /** Resolve whether the session user may access Looks. Never throws. */
69894
+ async checkAccess() {
69895
+ try {
69896
+ const res = await lastValueFrom(this.builderService.getLookAccess());
69897
+ this.allowed = !!res?.data?.allowed;
69898
+ }
69899
+ catch (err) {
69900
+ console.log('@@ ~ checkAccess [ERR]:', err);
69901
+ this.allowed = false;
69902
+ }
69903
+ finally {
69904
+ this.accessChecked = true;
69905
+ }
69906
+ }
69805
69907
  setPageData() {
69806
69908
  this.pageInfo.setTitle('Look Collection');
69807
69909
  this.pageInfo.setBreadcrumbs([
@@ -69809,7 +69911,6 @@ class LookPlanoCollectionFormComponent {
69809
69911
  { title: '', path: '', isActive: false, isSeparator: true },
69810
69912
  { title: 'Looks', path: '/manage/planogram/looks', isActive: false, isSeparator: false },
69811
69913
  { title: '', path: '', isActive: false, isSeparator: true },
69812
- { title: 'Edit', path: '', isActive: true, isSeparator: false },
69813
69914
  ]);
69814
69915
  }
69815
69916
  initForm() {
@@ -70076,6 +70177,85 @@ class LookPlanoCollectionFormComponent {
70076
70177
  console.log('@@ ~ loadPlacementDropdownData [ERR]:', err);
70077
70178
  }
70078
70179
  }
70180
+ /**
70181
+ * Load the MBQ bucket / Brand / Fixture Header dropdown options seeded into
70182
+ * planostaticdatas from the sheet, then rebuild the flat option lists
70183
+ * (merging in any values already present on the loaded collection).
70184
+ */
70185
+ async loadComboOptions() {
70186
+ if (!this.clientId)
70187
+ return;
70188
+ try {
70189
+ const res = await lastValueFrom(this.builderService.getLookComboOptions(this.clientId));
70190
+ const data = res?.data || {};
70191
+ const toStrArr = (arr) => (Array.isArray(arr) ? arr : [])
70192
+ .map((v) => String(v ?? '').trim())
70193
+ .filter((s) => !!s);
70194
+ this.comboRaw = {
70195
+ mbqBuckets: toStrArr(data.mbqBuckets),
70196
+ brands: toStrArr(data.brands),
70197
+ fixtureHeaders: toStrArr(data.fixtureHeaders),
70198
+ combinations: (Array.isArray(data.combinations) ? data.combinations : [])
70199
+ .map((c) => ({
70200
+ mbqBucket: String(c?.mbqBucket ?? '').trim(),
70201
+ brand: String(c?.brand ?? '').trim(),
70202
+ fixtureHeader: String(c?.fixtureHeader ?? '').trim(),
70203
+ })),
70204
+ };
70205
+ }
70206
+ catch (err) {
70207
+ // Endpoint missing / not yet deployed — fall back to values derived
70208
+ // from the loaded collection so the dropdowns still work.
70209
+ console.log('@@ ~ loadComboOptions [ERR]:', err);
70210
+ }
70211
+ this.rebuildComboOptions();
70212
+ }
70213
+ /**
70214
+ * Rebuild the MBQ bucket / Brand / Fixture Header option lists (and the
70215
+ * cascading caches) by MERGING the seeded endpoint payload (comboRaw) with
70216
+ * whatever values are already present on the currently-loaded looks. The
70217
+ * merge guarantees the dropdowns are never empty for an existing collection
70218
+ * even when the endpoint hasn't loaded, and that any value already saved on
70219
+ * a look is always selectable.
70220
+ */
70221
+ rebuildComboOptions() {
70222
+ const buckets = new Set(this.comboRaw.mbqBuckets);
70223
+ const brands = new Set(this.comboRaw.brands);
70224
+ const headers = new Set(this.comboRaw.fixtureHeaders);
70225
+ // Seed combinations also feed the flat per-field lists.
70226
+ for (const c of this.comboRaw.combinations) {
70227
+ if (c.mbqBucket)
70228
+ buckets.add(c.mbqBucket);
70229
+ if (c.brand)
70230
+ brands.add(c.brand);
70231
+ if (c.fixtureHeader)
70232
+ headers.add(c.fixtureHeader);
70233
+ }
70234
+ // Merge in values already present on the loaded looks/slots so the
70235
+ // dropdowns work even when the endpoint hasn't loaded.
70236
+ for (const lookCtrl of this.looks.controls) {
70237
+ const look = lookCtrl;
70238
+ const bucket = String(look.get('mbqBucket')?.value ?? '').trim()
70239
+ || String(look.get('brandCategory')?.value ?? '').trim();
70240
+ if (bucket)
70241
+ buckets.add(bucket);
70242
+ const slots = look.get('fixtureSlots');
70243
+ for (const slotCtrl of slots?.controls || []) {
70244
+ const slot = slotCtrl;
70245
+ const brand = String(slot.get('brand')?.value ?? '').trim();
70246
+ const header = String(slot.get('fixtureLevelZone')?.value ?? '').trim();
70247
+ if (brand)
70248
+ brands.add(brand);
70249
+ if (header)
70250
+ headers.add(header);
70251
+ }
70252
+ }
70253
+ const toItems = (set) => [...set].filter((s) => !!s).sort().map((s) => ({ value: s, label: s }));
70254
+ this.mbqBucketItems = toItems(buckets);
70255
+ this.brandItems = toItems(brands);
70256
+ this.fixtureHeaderItems = toItems(headers);
70257
+ console.log('[Looks form] combo options:', this.mbqBucketItems.length, 'buckets,', this.brandItems.length, 'brands,', this.fixtureHeaderItems.length, 'headers');
70258
+ }
70079
70259
  async loadFixtureLibraries() {
70080
70260
  if (!this.clientId)
70081
70261
  return;
@@ -70144,9 +70324,33 @@ class LookPlanoCollectionFormComponent {
70144
70324
  return;
70145
70325
  }
70146
70326
  this.recomputeFixtureLibraryItemsForSlot();
70327
+ // Re-filter the Fixture Templates list when the Fixture Type changes, and
70328
+ // RESET the current template selections: wall (shelf) and floor
70329
+ // (eurocenter) library sets are disjoint, so anything selected for the old
70330
+ // type is invalid for the new one. Clearing fixtureLibraryRefs cascades —
70331
+ // the refsSub below clears the embedded preview, and we drop the stale
70332
+ // per-library snapshots too.
70333
+ let prevSlotType = String(slot.get('slotType')?.value ?? '');
70147
70334
  const typeSub = slot.get('slotType')?.valueChanges
70148
70335
  .pipe(takeUntil(this.destroy$))
70149
- .subscribe(() => this.recomputeFixtureLibraryItemsForSlot());
70336
+ .subscribe((nextType) => {
70337
+ const changed = String(nextType ?? '') !== prevSlotType;
70338
+ prevSlotType = String(nextType ?? '');
70339
+ this.recomputeFixtureLibraryItemsForSlot();
70340
+ if (!changed)
70341
+ return;
70342
+ const refsCtrl = slot.get('fixtureLibraryRefs');
70343
+ if (refsCtrl && (refsCtrl.value || []).length) {
70344
+ refsCtrl.setValue([]); // triggers refsSub -> clears active preview
70345
+ }
70346
+ const snapsCtrl = slot.get('librarySnapshots');
70347
+ if (snapsCtrl && (snapsCtrl.value || []).length) {
70348
+ snapsCtrl.setValue([], { emitEvent: false });
70349
+ }
70350
+ // Defensive: clear the legacy single-ref fields if they were set.
70351
+ slot.get('fixtureLibraryRef')?.setValue('', { emitEvent: false });
70352
+ slot.get('fixtureLibraryLabel')?.setValue('', { emitEvent: false });
70353
+ });
70150
70354
  if (typeSub)
70151
70355
  this.slotSubs.push(typeSub);
70152
70356
  // Track previous refs so we can detect *new* additions and auto-apply
@@ -70316,6 +70520,9 @@ class LookPlanoCollectionFormComponent {
70316
70520
  this.resetLibraryMap();
70317
70521
  this.bindActiveSlotListeners();
70318
70522
  this.autoSelectFirstLibrary();
70523
+ // Merge this collection's own values into the dropdown options so they
70524
+ // show even if the comboOptions endpoint is unavailable.
70525
+ this.rebuildComboOptions();
70319
70526
  }
70320
70527
  }
70321
70528
  catch (err) {
@@ -70343,6 +70550,22 @@ class LookPlanoCollectionFormComponent {
70343
70550
  this.selectedLookIndex -= 1;
70344
70551
  }
70345
70552
  }
70553
+ /**
70554
+ * Clone the look at `index` (deep copy of its full nested value — slots,
70555
+ * placements, cadHeaderVariants, librarySnapshots) and insert it right
70556
+ * after the original, then select the copy. createLookGroup rebuilds the
70557
+ * whole FormGroup tree from the JSON seed, so the clone shares no control
70558
+ * references with the source.
70559
+ */
70560
+ duplicateLook(index) {
70561
+ const src = this.looks.at(index);
70562
+ if (!src)
70563
+ return;
70564
+ const seed = JSON.parse(JSON.stringify(src.getRawValue()));
70565
+ const clone = this.createLookGroup(seed);
70566
+ this.looks.insert(index + 1, clone);
70567
+ this.selectLook(index + 1);
70568
+ }
70346
70569
  selectLook(index) {
70347
70570
  this.selectedLookIndex = index;
70348
70571
  const slots = this.fixtureSlots(index);
@@ -70564,11 +70787,11 @@ class LookPlanoCollectionFormComponent {
70564
70787
  this.planoDataService.fixtureTemplateDetails.next(null);
70565
70788
  }
70566
70789
  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 });
70567
- 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 <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\">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 <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\">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 .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"] }] });
70790
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: LookPlanoCollectionFormComponent, selector: "lib-look-plano-collection-form", ngImport: i0, template: "<div *ngIf=\"!accessChecked\" class=\"card p-5 m-4 text-center text-muted\">Checking access\u2026</div>\r\n\r\n<div *ngIf=\"accessChecked && !allowed\" class=\"card p-5 m-4\">\r\n <div class=\"access-denied text-center py-10\">\r\n <div class=\"access-denied-icon mb-4\"><i class=\"bi bi-shield-lock\"></i></div>\r\n <h3 class=\"mb-2\">Access denied</h3>\r\n <p class=\"text-muted mb-0\">\r\n You don't have permission to access Looks. Contact your administrator\r\n to be added to the allowed users list.\r\n </p>\r\n </div>\r\n</div>\r\n\r\n<section class=\"look-form\" *ngIf=\"accessChecked && allowed\">\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: [".access-denied .access-denied-icon i{font-size:48px;color:#ef4444;line-height:1}.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"] }] });
70568
70791
  }
70569
70792
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LookPlanoCollectionFormComponent, decorators: [{
70570
70793
  type: Component,
70571
- 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 <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\">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 <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\">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 .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"] }]
70794
+ args: [{ selector: 'lib-look-plano-collection-form', template: "<div *ngIf=\"!accessChecked\" class=\"card p-5 m-4 text-center text-muted\">Checking access\u2026</div>\r\n\r\n<div *ngIf=\"accessChecked && !allowed\" class=\"card p-5 m-4\">\r\n <div class=\"access-denied text-center py-10\">\r\n <div class=\"access-denied-icon mb-4\"><i class=\"bi bi-shield-lock\"></i></div>\r\n <h3 class=\"mb-2\">Access denied</h3>\r\n <p class=\"text-muted mb-0\">\r\n You don't have permission to access Looks. Contact your administrator\r\n to be added to the allowed users list.\r\n </p>\r\n </div>\r\n</div>\r\n\r\n<section class=\"look-form\" *ngIf=\"accessChecked && allowed\">\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: [".access-denied .access-denied-icon i{font-size:48px;color:#ef4444;line-height:1}.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"] }]
70572
70795
  }], 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 }] });
70573
70796
 
70574
70797
  const routes$1 = [