tango-app-ui-store-builder 1.2.21 → 1.2.23

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.
@@ -7624,7 +7624,7 @@ class CadSourceService {
7624
7624
  async open(storeName, cadFiles) {
7625
7625
  if (!this.hasSource(cadFiles))
7626
7626
  return;
7627
- const { DxfSourceViewerComponent } = await import('./tango-app-ui-store-builder-dxf-source-viewer.component-B0ut7XVu.mjs');
7627
+ const { DxfSourceViewerComponent } = await import('./tango-app-ui-store-builder-dxf-source-viewer.component-BplzhK87.mjs');
7628
7628
  const ref = this.ngbModal.open(DxfSourceViewerComponent, {
7629
7629
  size: "xl",
7630
7630
  centered: true,
@@ -62369,31 +62369,88 @@ class OnboardStorePlanoComponent {
62369
62369
  height: maxY - minY,
62370
62370
  };
62371
62371
  }
62372
- // Bounding box of just the wall + entrance objects — the store perimeter.
62373
- // Used as the fit target so stray fixtures placed outside the layout (common
62374
- // on CAD imports, where a fixture's relativePosition can be unset/garbage)
62375
- // can't inflate the frame. Returns zero-size when there is no wall/entrance
62376
- // on the canvas so callers can fall back to full bounds.
62377
- getWallPerimeterBounds() {
62378
- const objects = this.canvas
62372
+ // Shared median helper for the robust (outlier-tolerant) bounds below.
62373
+ median(values) {
62374
+ if (!values.length)
62375
+ return 0;
62376
+ const sorted = [...values].sort((a, b) => a - b);
62377
+ const mid = Math.floor(sorted.length / 2);
62378
+ return sorted.length % 2
62379
+ ? sorted[mid]
62380
+ : (sorted[mid - 1] + sorted[mid]) / 2;
62381
+ }
62382
+ // Union bounds of the canvas objects with gross positional outliers removed.
62383
+ // A single element carrying a bad far coordinate — a CAD orphan wall stranded
62384
+ // at the origin, an unplaced fixture — would otherwise inflate the frame and
62385
+ // collapse the fit zoom to an invisible speck. We drop objects whose center
62386
+ // lies far outside the cluster (median center + a generous multiple of the
62387
+ // spread) and fit to what remains, so no single stray of ANY type (wall,
62388
+ // entrance, fixture, other) can hide the whole layout. Falls back to the full
62389
+ // union when there are too few objects to reason about statistically.
62390
+ getRobustLayoutBounds() {
62391
+ const entries = this.canvas
62379
62392
  .getObjects()
62380
- .filter((o) => o?.objType === "wall" || o?.objType === "entrance");
62381
- if (!objects.length)
62393
+ .map((o) => ({ o, r: o.getBoundingRect() }))
62394
+ .filter(({ r }) => Number.isFinite(r.left) &&
62395
+ Number.isFinite(r.top) &&
62396
+ Number.isFinite(r.width) &&
62397
+ Number.isFinite(r.height));
62398
+ if (!entries.length)
62382
62399
  return { left: 0, top: 0, width: 0, height: 0 };
62383
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
62384
- for (const obj of objects) {
62385
- const rect = obj.getBoundingRect();
62386
- minX = Math.min(minX, rect.left);
62387
- minY = Math.min(minY, rect.top);
62388
- maxX = Math.max(maxX, rect.left + rect.width);
62389
- maxY = Math.max(maxY, rect.top + rect.height);
62390
- }
62391
- return {
62392
- left: minX,
62393
- top: minY,
62394
- width: maxX - minX,
62395
- height: maxY - minY,
62400
+ const union = (list) => {
62401
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
62402
+ for (const { r } of list) {
62403
+ minX = Math.min(minX, r.left);
62404
+ minY = Math.min(minY, r.top);
62405
+ maxX = Math.max(maxX, r.left + r.width);
62406
+ maxY = Math.max(maxY, r.top + r.height);
62407
+ }
62408
+ return { left: minX, top: minY, width: maxX - minX, height: maxY - minY };
62396
62409
  };
62410
+ if (entries.length < 4)
62411
+ return union(entries);
62412
+ const cxs = entries.map(({ r }) => r.left + r.width / 2);
62413
+ const cys = entries.map(({ r }) => r.top + r.height / 2);
62414
+ const medX = this.median(cxs);
62415
+ const medY = this.median(cys);
62416
+ const dists = entries.map((_, i) => Math.hypot(cxs[i] - medX, cys[i] - medY));
62417
+ const medDist = this.median(dists);
62418
+ const mad = this.median(dists.map((d) => Math.abs(d - medDist)));
62419
+ const spread = mad > 1 ? mad : medDist > 1 ? medDist : 1;
62420
+ const threshold = medDist + 6 * spread;
62421
+ const kept = entries.filter((_, i) => dists[i] <= threshold);
62422
+ const dropped = entries.filter((_, i) => dists[i] > threshold);
62423
+ if (dropped.length) {
62424
+ console.warn(`[fit] ignoring ${dropped.length} out-of-cluster object(s) while fitting`, dropped.map(({ o, r }) => ({
62425
+ type: o?.objType,
62426
+ id: o?.fixtureId ?? o?.data?._id ?? o?.data?.elementNumber,
62427
+ bounds: r,
62428
+ })));
62429
+ }
62430
+ return kept.length ? union(kept) : union(entries);
62431
+ }
62432
+ // Wall start-points (px) with gross outliers removed. A CAD orphan wall far
62433
+ // from the store must not define the perimeter that reclaim places elements
62434
+ // into, so strip it the same way the fit does before building the polygon.
62435
+ getRobustWallPolygon() {
62436
+ const pts = (this.floorData?.layoutPolygon ?? [])
62437
+ .filter((w) => w?.elementType === "wall")
62438
+ .map((w) => ({
62439
+ x: this.toPixels(w.relativePosition?.x ?? 0, w.relativePosition?.unit || "ft"),
62440
+ y: this.toPixels(w.relativePosition?.y ?? 0, w.relativePosition?.unit || "ft"),
62441
+ }))
62442
+ .filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y));
62443
+ if (pts.length < 4)
62444
+ return pts;
62445
+ const medX = this.median(pts.map((p) => p.x));
62446
+ const medY = this.median(pts.map((p) => p.y));
62447
+ const dists = pts.map((p) => Math.hypot(p.x - medX, p.y - medY));
62448
+ const medDist = this.median(dists);
62449
+ const mad = this.median(dists.map((d) => Math.abs(d - medDist)));
62450
+ const spread = mad > 1 ? mad : medDist > 1 ? medDist : 1;
62451
+ const threshold = medDist + 6 * spread;
62452
+ const kept = pts.filter((_, i) => dists[i] <= threshold);
62453
+ return kept.length >= 3 ? kept : pts;
62397
62454
  }
62398
62455
  // Reclaims elements whose stored position renders them outside the wall
62399
62456
  // perimeter — the bad-coordinate artifacts a CAD import can leave behind.
@@ -62406,7 +62463,10 @@ class OnboardStorePlanoComponent {
62406
62463
  const floor = this.floorData;
62407
62464
  if (!floor?.layoutPolygon?.length)
62408
62465
  return 0;
62409
- const wallPolygon = this.buildLayoutPolygonFromWalls();
62466
+ // Robust wall polygon so an orphan wall (bad coordinate) can't define the
62467
+ // perimeter — otherwise fixtures/other elements would all look "inside" the
62468
+ // inflated frame and never get reclaimed.
62469
+ const wallPolygon = this.getRobustWallPolygon();
62410
62470
  if (wallPolygon.length < 3)
62411
62471
  return 0;
62412
62472
  const bounds = this.polygonBounds(wallPolygon);
@@ -62627,15 +62687,12 @@ class OnboardStorePlanoComponent {
62627
62687
  return;
62628
62688
  if (this.layoutForm.enabled)
62629
62689
  return;
62630
- // Fit to the wall/entrance perimeter (the true store extent) rather than the
62631
- // union of every object. On CAD-imported stores a few fixtures can land far
62632
- // outside the layout, and using the full bounds would blow up the frame and
62633
- // collapse the zoom to near-zero, leaving the layout invisibly small. Fall
62634
- // back to full bounds only when there is no wall/entrance to anchor on.
62635
- const perimeter = this.getWallPerimeterBounds();
62636
- const bounds = perimeter.width && perimeter.height
62637
- ? perimeter
62638
- : this.getFullLayoutBounds();
62690
+ // Fit to the cluster of objects with gross outliers removed, rather than the
62691
+ // union of everything. A single element with a bad far coordinate e.g. a
62692
+ // CAD orphan wall stranded at the origin while the real store sits thousands
62693
+ // of feet away would otherwise blow up the frame and collapse the zoom to
62694
+ // an invisible speck. getRobustLayoutBounds() drops such strays of any type.
62695
+ const bounds = this.getRobustLayoutBounds();
62639
62696
  if (!bounds.width || !bounds.height)
62640
62697
  return;
62641
62698
  const canvasW = this.canvas.getWidth();
@@ -69087,6 +69144,7 @@ class FixtureConformanceComponent {
69087
69144
  case 'ok': return 'Conforms';
69088
69145
  case 'shelf-count-mismatch': return 'Shelf count mismatch';
69089
69146
  case 'zone-mismatch': return 'Zone mismatch';
69147
+ case 'type-mismatch': return 'Shelf type mismatch';
69090
69148
  case 'no-library': return 'No library match';
69091
69149
  case 'ambiguous': return 'Ambiguous library';
69092
69150
  case 'no-shelves': return 'No shelves';
@@ -69097,7 +69155,8 @@ class FixtureConformanceComponent {
69097
69155
  switch (reason) {
69098
69156
  case 'ok': return 'badge-ok';
69099
69157
  case 'shelf-count-mismatch':
69100
- case 'zone-mismatch': return 'badge-warn';
69158
+ case 'zone-mismatch':
69159
+ case 'type-mismatch': return 'badge-warn';
69101
69160
  case 'no-library':
69102
69161
  case 'ambiguous':
69103
69162
  case 'no-shelves': return 'badge-muted';
@@ -69114,11 +69173,11 @@ class FixtureConformanceComponent {
69114
69173
  this.destroy$.complete();
69115
69174
  }
69116
69175
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FixtureConformanceComponent, deps: [{ token: StoreBuilderService }, { token: i2$1.GlobalStateService }], target: i0.ɵɵFactoryTarget.Component });
69117
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: FixtureConformanceComponent, selector: "lib-fixture-conformance", ngImport: i0, template: "<section class=\"fixture-conformance p-3\">\n <lib-tool-page-header\n title=\"Sync Fixtures to Library\"\n description=\"Check a store's fixtures against the library, then sync shelf count and zones for the ones that don't match.\"\n ></lib-tool-page-header>\n\n <div class=\"card p-4\">\n <!-- Store Select -->\n <div class=\"mb-3\" style=\"max-width: 600px;\">\n <label class=\"form-label\">Store</label>\n <lib-store-select\n [options]=\"storeOptions\"\n [singleSelect]=\"true\"\n (selectedChange)=\"onStoresSelected($event)\">\n </lib-store-select>\n </div>\n\n <!-- Fixture List -->\n @if (selectedStoreName) {\n @if (isLoadingFixtures) {\n <div class=\"text-muted\">Loading fixtures...</div>\n } @else if (fixtures.length === 0) {\n <div class=\"text-muted\">No fixtures found for this store.</div>\n } @else {\n <div class=\"d-flex align-items-center justify-content-between mb-2\">\n <h6 class=\"section-heading mb-0\">\n Fixtures\n <span class=\"text-muted small\">({{ selectedFixtureIds.length }} of {{ fixtures.length }} selected)</span>\n </h6>\n <button\n class=\"btn btn-primary btn-sm\"\n (click)=\"onAudit()\"\n [disabled]=\"selectedFixtureIds.length === 0 || isAuditing\">\n {{ isAuditing ? 'Auditing...' : 'Audit Selected' }}\n </button>\n </div>\n\n <div class=\"table-responsive mb-2\">\n <table class=\"table table-sm align-middle fc-table\">\n <thead>\n <tr>\n <th style=\"width: 42px;\">\n <input type=\"checkbox\"\n [checked]=\"allFixturesSelected\"\n (change)=\"toggleAllFixtures($any($event.target).checked)\">\n </th>\n <th style=\"width: 90px;\">Fixture #</th>\n <th>Header</th>\n <th>Fixture ID</th>\n <th>Category</th>\n <th style=\"width: 90px;\">Width</th>\n </tr>\n </thead>\n <tbody>\n @for (f of fixtures; track f._id) {\n <tr (click)=\"f.selected = !f.selected\" class=\"selectable-row\">\n <td (click)=\"$event.stopPropagation()\">\n <input type=\"checkbox\" [(ngModel)]=\"f.selected\">\n </td>\n <td>{{ f.fixtureNumber ?? '\u2014' }}</td>\n <td>{{ f.headerLabel || '\u2014' }}</td>\n <td class=\"fixture-id\">{{ f._id }}</td>\n <td>{{ f.fixtureCategory || '\u2014' }}</td>\n <td>{{ f.width || '\u2014' }}</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n }\n\n <!-- Audit Results -->\n @if (hasAudited) {\n <hr class=\"my-4\" />\n <div class=\"d-flex align-items-center justify-content-between mb-2\">\n <h6 class=\"section-heading mb-0\">\n Audit Result\n <span class=\"text-muted small\">({{ fixableRows.length }} fixable, {{ selectedAuditIds.length }} selected)</span>\n </h6>\n <button\n class=\"btn btn-success btn-sm\"\n (click)=\"onApplyFix()\"\n [disabled]=\"selectedAuditIds.length === 0 || isApplying\">\n {{ isApplying ? 'Applying...' : 'Apply Fix' }}\n </button>\n </div>\n\n @if (auditRows.length === 0) {\n <div class=\"text-muted\">No results.</div>\n } @else {\n <div class=\"table-responsive\">\n <table class=\"table table-sm align-middle fc-table\">\n <thead>\n <tr>\n <th style=\"width: 42px;\">\n <input type=\"checkbox\"\n [checked]=\"allFixableSelected\"\n [disabled]=\"fixableRows.length === 0\"\n (change)=\"toggleAllFixable($any($event.target).checked)\">\n </th>\n <th style=\"width: 90px;\">Fixture #</th>\n <th>Header</th>\n <th>Category</th>\n <th style=\"width: 70px;\">Width</th>\n <th style=\"width: 110px;\">Shelves</th>\n <th>Current Zones</th>\n <th>Library Zones</th>\n <th style=\"width: 160px;\">Status</th>\n </tr>\n </thead>\n <tbody>\n @for (r of auditRows; track r.fixtureId) {\n <tr [class.row-fixable]=\"r.fixable\">\n <td>\n <input type=\"checkbox\" [(ngModel)]=\"r.selected\" [disabled]=\"!r.fixable\">\n </td>\n <td>{{ r.fixtureNumber ?? '\u2014' }}</td>\n <td>{{ r.header || '\u2014' }}</td>\n <td>{{ r.category || '\u2014' }}</td>\n <td>{{ r.width || '\u2014' }}</td>\n <td>\n @if (r.libShelfCount != null) {\n <span [class.text-danger]=\"r.curShelfCount !== r.libShelfCount\">\n {{ r.curShelfCount }} \u2192 {{ r.libShelfCount }}\n </span>\n } @else {\n {{ r.curShelfCount }}\n }\n </td>\n <td class=\"zones\">{{ r.currentZones || '\u2014' }}</td>\n <td class=\"zones\">{{ r.libZones || '\u2014' }}</td>\n <td>\n <span class=\"status-badge\" [ngClass]=\"reasonClass(r.reason)\">\n {{ reasonLabel(r.reason) }}\n </span>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n }\n\n <!-- Apply summary per fixture -->\n @if (applyResults.length > 0) {\n <hr class=\"my-4\" />\n <h6 class=\"section-heading mb-2\">Apply Summary</h6>\n <div class=\"table-responsive\">\n <table class=\"table table-sm align-middle fc-table\">\n <thead>\n <tr>\n <th style=\"width: 90px;\">Fixture #</th>\n <th style=\"width: 140px;\">Status</th>\n <th>Shelves</th>\n <th>Compliance Synced</th>\n <th>Note</th>\n </tr>\n </thead>\n <tbody>\n @for (ar of applyResults; track ar.fixtureId) {\n <tr>\n <td>{{ ar.fixtureNumber ?? '\u2014' }}</td>\n <td>\n <span class=\"status-badge\"\n [class.badge-ok]=\"ar.status === 'fixed' || ar.status === 'already-conform'\"\n [class.badge-warn]=\"ar.status === 'skipped'\"\n [class.badge-danger]=\"ar.status === 'failed'\">\n {{ ar.status }}\n </span>\n </td>\n <td>\n @if (ar.shelvesFrom != null) {\n {{ ar.shelvesFrom }} \u2192 {{ ar.shelvesTo }}\n } @else { \u2014 }\n </td>\n <td>{{ ar.complianceSynced ?? 0 }}</td>\n <td class=\"text-muted small\">{{ ar.error || ar.reason || '' }}</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n <!-- Message -->\n @if (message) {\n <div class=\"mt-3 alert\" [class.alert-success]=\"messageType === 'success'\"\n [class.alert-danger]=\"messageType === 'error'\">\n {{ message }}\n </div>\n }\n </div>\n</section>\n", styles: [".fixture-conformance .section-heading{color:#344054;font-weight:600}.fixture-conformance .fc-table th{background-color:#f9fafb;font-weight:600;font-size:13px;color:#475467;white-space:nowrap}.fixture-conformance .fc-table td{font-size:13px;vertical-align:middle}.fixture-conformance .fc-table .zones{font-family:Courier New,monospace;font-size:12px;color:#475467;word-break:break-word}.fixture-conformance .fc-table .fixture-id{font-family:Courier New,monospace;font-size:12px;color:#667085}.fixture-conformance .fc-table .selectable-row{cursor:pointer}.fixture-conformance .fc-table .selectable-row:hover{background-color:#f9fafb}.fixture-conformance .fc-table .row-fixable{background-color:#fffaf0}.fixture-conformance .status-badge{display:inline-block;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:600;white-space:nowrap}.fixture-conformance .status-badge.badge-ok{background-color:#ecfdf3;color:#027a48}.fixture-conformance .status-badge.badge-warn{background-color:#fffaeb;color:#b54708}.fixture-conformance .status-badge.badge-muted{background-color:#f2f4f7;color:#475467}.fixture-conformance .status-badge.badge-danger{background-color:#fef3f2;color:#b42318}\n"], dependencies: [{ kind: "directive", type: i5.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { 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"] }, { kind: "component", type: ToolPageHeaderComponent, selector: "lib-tool-page-header", inputs: ["title", "description", "backLink"] }] });
69176
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: FixtureConformanceComponent, selector: "lib-fixture-conformance", ngImport: i0, template: "<section class=\"fixture-conformance p-3\">\n <lib-tool-page-header\n title=\"Sync Fixtures to Library\"\n description=\"Check a store's fixtures against the library, then sync shelf count and zones for the ones that don't match.\"\n ></lib-tool-page-header>\n\n <div class=\"card p-4\">\n <!-- Store Select -->\n <div class=\"mb-3\" style=\"max-width: 600px;\">\n <label class=\"form-label\">Store</label>\n <lib-store-select\n [options]=\"storeOptions\"\n [singleSelect]=\"true\"\n (selectedChange)=\"onStoresSelected($event)\">\n </lib-store-select>\n </div>\n\n <!-- Fixture List -->\n @if (selectedStoreName) {\n @if (isLoadingFixtures) {\n <div class=\"text-muted\">Loading fixtures...</div>\n } @else if (fixtures.length === 0) {\n <div class=\"text-muted\">No fixtures found for this store.</div>\n } @else {\n <div class=\"d-flex align-items-center justify-content-between mb-2\">\n <h6 class=\"section-heading mb-0\">\n Fixtures\n <span class=\"text-muted small\">({{ selectedFixtureIds.length }} of {{ fixtures.length }} selected)</span>\n </h6>\n <button\n class=\"btn btn-primary btn-sm\"\n (click)=\"onAudit()\"\n [disabled]=\"selectedFixtureIds.length === 0 || isAuditing\">\n {{ isAuditing ? 'Auditing...' : 'Audit Selected' }}\n </button>\n </div>\n\n <div class=\"table-responsive mb-2\">\n <table class=\"table table-sm align-middle fc-table\">\n <thead>\n <tr>\n <th style=\"width: 42px;\">\n <input type=\"checkbox\"\n [checked]=\"allFixturesSelected\"\n (change)=\"toggleAllFixtures($any($event.target).checked)\">\n </th>\n <th style=\"width: 90px;\">Fixture #</th>\n <th>Header</th>\n <th>Fixture ID</th>\n <th>Category</th>\n <th style=\"width: 90px;\">Width</th>\n </tr>\n </thead>\n <tbody>\n @for (f of fixtures; track f._id) {\n <tr (click)=\"f.selected = !f.selected\" class=\"selectable-row\">\n <td (click)=\"$event.stopPropagation()\">\n <input type=\"checkbox\" [(ngModel)]=\"f.selected\">\n </td>\n <td>{{ f.fixtureNumber ?? '\u2014' }}</td>\n <td>{{ f.headerLabel || '\u2014' }}</td>\n <td class=\"fixture-id\">{{ f._id }}</td>\n <td>{{ f.fixtureCategory || '\u2014' }}</td>\n <td>{{ f.width || '\u2014' }}</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n }\n\n <!-- Audit Results -->\n @if (hasAudited) {\n <hr class=\"my-4\" />\n <div class=\"d-flex align-items-center justify-content-between mb-2\">\n <h6 class=\"section-heading mb-0\">\n Audit Result\n <span class=\"text-muted small\">({{ fixableRows.length }} fixable, {{ selectedAuditIds.length }} selected)</span>\n </h6>\n <button\n class=\"btn btn-success btn-sm\"\n (click)=\"onApplyFix()\"\n [disabled]=\"selectedAuditIds.length === 0 || isApplying\">\n {{ isApplying ? 'Applying...' : 'Apply Fix' }}\n </button>\n </div>\n\n @if (auditRows.length === 0) {\n <div class=\"text-muted\">No results.</div>\n } @else {\n <div class=\"table-responsive\">\n <table class=\"table table-sm align-middle fc-table\">\n <thead>\n <tr>\n <th style=\"width: 42px;\">\n <input type=\"checkbox\"\n [checked]=\"allFixableSelected\"\n [disabled]=\"fixableRows.length === 0\"\n (change)=\"toggleAllFixable($any($event.target).checked)\">\n </th>\n <th style=\"width: 90px;\">Fixture #</th>\n <th>Header</th>\n <th>Category</th>\n <th style=\"width: 70px;\">Width</th>\n <th style=\"width: 110px;\">Shelves</th>\n <th>Current Zones</th>\n <th>Library Zones</th>\n <th>Current Types</th>\n <th>Library Types</th>\n <th style=\"width: 160px;\">Status</th>\n </tr>\n </thead>\n <tbody>\n @for (r of auditRows; track r.fixtureId) {\n <tr [class.row-fixable]=\"r.fixable\">\n <td>\n <input type=\"checkbox\" [(ngModel)]=\"r.selected\" [disabled]=\"!r.fixable\">\n </td>\n <td>{{ r.fixtureNumber ?? '\u2014' }}</td>\n <td>{{ r.header || '\u2014' }}</td>\n <td>{{ r.category || '\u2014' }}</td>\n <td>{{ r.width || '\u2014' }}</td>\n <td>\n @if (r.libShelfCount != null) {\n <span [class.text-danger]=\"r.curShelfCount !== r.libShelfCount\">\n {{ r.curShelfCount }} \u2192 {{ r.libShelfCount }}\n </span>\n } @else {\n {{ r.curShelfCount }}\n }\n </td>\n <td class=\"zones\">{{ r.currentZones || '\u2014' }}</td>\n <td class=\"zones\">{{ r.libZones || '\u2014' }}</td>\n <td class=\"zones\" [class.text-danger]=\"r.reason === 'type-mismatch'\">{{ r.currentTypes || '\u2014' }}</td>\n <td class=\"zones\">{{ r.libTypes || '\u2014' }}</td>\n <td>\n <span class=\"status-badge\" [ngClass]=\"reasonClass(r.reason)\">\n {{ reasonLabel(r.reason) }}\n </span>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n }\n\n <!-- Apply summary per fixture -->\n @if (applyResults.length > 0) {\n <hr class=\"my-4\" />\n <h6 class=\"section-heading mb-2\">Apply Summary</h6>\n <div class=\"table-responsive\">\n <table class=\"table table-sm align-middle fc-table\">\n <thead>\n <tr>\n <th style=\"width: 90px;\">Fixture #</th>\n <th style=\"width: 140px;\">Status</th>\n <th>Shelves</th>\n <th>Compliance Synced</th>\n <th>Note</th>\n </tr>\n </thead>\n <tbody>\n @for (ar of applyResults; track ar.fixtureId) {\n <tr>\n <td>{{ ar.fixtureNumber ?? '\u2014' }}</td>\n <td>\n <span class=\"status-badge\"\n [class.badge-ok]=\"ar.status === 'fixed' || ar.status === 'already-conform'\"\n [class.badge-warn]=\"ar.status === 'skipped'\"\n [class.badge-danger]=\"ar.status === 'failed'\">\n {{ ar.status }}\n </span>\n </td>\n <td>\n @if (ar.shelvesFrom != null) {\n {{ ar.shelvesFrom }} \u2192 {{ ar.shelvesTo }}\n } @else { \u2014 }\n </td>\n <td>{{ ar.complianceSynced ?? 0 }}</td>\n <td class=\"text-muted small\">{{ ar.error || ar.reason || '' }}</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n <!-- Message -->\n @if (message) {\n <div class=\"mt-3 alert\" [class.alert-success]=\"messageType === 'success'\"\n [class.alert-danger]=\"messageType === 'error'\">\n {{ message }}\n </div>\n }\n </div>\n</section>\n", styles: [".fixture-conformance .section-heading{color:#344054;font-weight:600}.fixture-conformance .fc-table th{background-color:#f9fafb;font-weight:600;font-size:13px;color:#475467;white-space:nowrap}.fixture-conformance .fc-table td{font-size:13px;vertical-align:middle}.fixture-conformance .fc-table .zones{font-family:Courier New,monospace;font-size:12px;color:#475467;word-break:break-word}.fixture-conformance .fc-table .fixture-id{font-family:Courier New,monospace;font-size:12px;color:#667085}.fixture-conformance .fc-table .selectable-row{cursor:pointer}.fixture-conformance .fc-table .selectable-row:hover{background-color:#f9fafb}.fixture-conformance .fc-table .row-fixable{background-color:#fffaf0}.fixture-conformance .status-badge{display:inline-block;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:600;white-space:nowrap}.fixture-conformance .status-badge.badge-ok{background-color:#ecfdf3;color:#027a48}.fixture-conformance .status-badge.badge-warn{background-color:#fffaeb;color:#b54708}.fixture-conformance .status-badge.badge-muted{background-color:#f2f4f7;color:#475467}.fixture-conformance .status-badge.badge-danger{background-color:#fef3f2;color:#b42318}\n"], dependencies: [{ kind: "directive", type: i5.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { 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"] }, { kind: "component", type: ToolPageHeaderComponent, selector: "lib-tool-page-header", inputs: ["title", "description", "backLink"] }] });
69118
69177
  }
69119
69178
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FixtureConformanceComponent, decorators: [{
69120
69179
  type: Component,
69121
- args: [{ selector: 'lib-fixture-conformance', template: "<section class=\"fixture-conformance p-3\">\n <lib-tool-page-header\n title=\"Sync Fixtures to Library\"\n description=\"Check a store's fixtures against the library, then sync shelf count and zones for the ones that don't match.\"\n ></lib-tool-page-header>\n\n <div class=\"card p-4\">\n <!-- Store Select -->\n <div class=\"mb-3\" style=\"max-width: 600px;\">\n <label class=\"form-label\">Store</label>\n <lib-store-select\n [options]=\"storeOptions\"\n [singleSelect]=\"true\"\n (selectedChange)=\"onStoresSelected($event)\">\n </lib-store-select>\n </div>\n\n <!-- Fixture List -->\n @if (selectedStoreName) {\n @if (isLoadingFixtures) {\n <div class=\"text-muted\">Loading fixtures...</div>\n } @else if (fixtures.length === 0) {\n <div class=\"text-muted\">No fixtures found for this store.</div>\n } @else {\n <div class=\"d-flex align-items-center justify-content-between mb-2\">\n <h6 class=\"section-heading mb-0\">\n Fixtures\n <span class=\"text-muted small\">({{ selectedFixtureIds.length }} of {{ fixtures.length }} selected)</span>\n </h6>\n <button\n class=\"btn btn-primary btn-sm\"\n (click)=\"onAudit()\"\n [disabled]=\"selectedFixtureIds.length === 0 || isAuditing\">\n {{ isAuditing ? 'Auditing...' : 'Audit Selected' }}\n </button>\n </div>\n\n <div class=\"table-responsive mb-2\">\n <table class=\"table table-sm align-middle fc-table\">\n <thead>\n <tr>\n <th style=\"width: 42px;\">\n <input type=\"checkbox\"\n [checked]=\"allFixturesSelected\"\n (change)=\"toggleAllFixtures($any($event.target).checked)\">\n </th>\n <th style=\"width: 90px;\">Fixture #</th>\n <th>Header</th>\n <th>Fixture ID</th>\n <th>Category</th>\n <th style=\"width: 90px;\">Width</th>\n </tr>\n </thead>\n <tbody>\n @for (f of fixtures; track f._id) {\n <tr (click)=\"f.selected = !f.selected\" class=\"selectable-row\">\n <td (click)=\"$event.stopPropagation()\">\n <input type=\"checkbox\" [(ngModel)]=\"f.selected\">\n </td>\n <td>{{ f.fixtureNumber ?? '\u2014' }}</td>\n <td>{{ f.headerLabel || '\u2014' }}</td>\n <td class=\"fixture-id\">{{ f._id }}</td>\n <td>{{ f.fixtureCategory || '\u2014' }}</td>\n <td>{{ f.width || '\u2014' }}</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n }\n\n <!-- Audit Results -->\n @if (hasAudited) {\n <hr class=\"my-4\" />\n <div class=\"d-flex align-items-center justify-content-between mb-2\">\n <h6 class=\"section-heading mb-0\">\n Audit Result\n <span class=\"text-muted small\">({{ fixableRows.length }} fixable, {{ selectedAuditIds.length }} selected)</span>\n </h6>\n <button\n class=\"btn btn-success btn-sm\"\n (click)=\"onApplyFix()\"\n [disabled]=\"selectedAuditIds.length === 0 || isApplying\">\n {{ isApplying ? 'Applying...' : 'Apply Fix' }}\n </button>\n </div>\n\n @if (auditRows.length === 0) {\n <div class=\"text-muted\">No results.</div>\n } @else {\n <div class=\"table-responsive\">\n <table class=\"table table-sm align-middle fc-table\">\n <thead>\n <tr>\n <th style=\"width: 42px;\">\n <input type=\"checkbox\"\n [checked]=\"allFixableSelected\"\n [disabled]=\"fixableRows.length === 0\"\n (change)=\"toggleAllFixable($any($event.target).checked)\">\n </th>\n <th style=\"width: 90px;\">Fixture #</th>\n <th>Header</th>\n <th>Category</th>\n <th style=\"width: 70px;\">Width</th>\n <th style=\"width: 110px;\">Shelves</th>\n <th>Current Zones</th>\n <th>Library Zones</th>\n <th style=\"width: 160px;\">Status</th>\n </tr>\n </thead>\n <tbody>\n @for (r of auditRows; track r.fixtureId) {\n <tr [class.row-fixable]=\"r.fixable\">\n <td>\n <input type=\"checkbox\" [(ngModel)]=\"r.selected\" [disabled]=\"!r.fixable\">\n </td>\n <td>{{ r.fixtureNumber ?? '\u2014' }}</td>\n <td>{{ r.header || '\u2014' }}</td>\n <td>{{ r.category || '\u2014' }}</td>\n <td>{{ r.width || '\u2014' }}</td>\n <td>\n @if (r.libShelfCount != null) {\n <span [class.text-danger]=\"r.curShelfCount !== r.libShelfCount\">\n {{ r.curShelfCount }} \u2192 {{ r.libShelfCount }}\n </span>\n } @else {\n {{ r.curShelfCount }}\n }\n </td>\n <td class=\"zones\">{{ r.currentZones || '\u2014' }}</td>\n <td class=\"zones\">{{ r.libZones || '\u2014' }}</td>\n <td>\n <span class=\"status-badge\" [ngClass]=\"reasonClass(r.reason)\">\n {{ reasonLabel(r.reason) }}\n </span>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n }\n\n <!-- Apply summary per fixture -->\n @if (applyResults.length > 0) {\n <hr class=\"my-4\" />\n <h6 class=\"section-heading mb-2\">Apply Summary</h6>\n <div class=\"table-responsive\">\n <table class=\"table table-sm align-middle fc-table\">\n <thead>\n <tr>\n <th style=\"width: 90px;\">Fixture #</th>\n <th style=\"width: 140px;\">Status</th>\n <th>Shelves</th>\n <th>Compliance Synced</th>\n <th>Note</th>\n </tr>\n </thead>\n <tbody>\n @for (ar of applyResults; track ar.fixtureId) {\n <tr>\n <td>{{ ar.fixtureNumber ?? '\u2014' }}</td>\n <td>\n <span class=\"status-badge\"\n [class.badge-ok]=\"ar.status === 'fixed' || ar.status === 'already-conform'\"\n [class.badge-warn]=\"ar.status === 'skipped'\"\n [class.badge-danger]=\"ar.status === 'failed'\">\n {{ ar.status }}\n </span>\n </td>\n <td>\n @if (ar.shelvesFrom != null) {\n {{ ar.shelvesFrom }} \u2192 {{ ar.shelvesTo }}\n } @else { \u2014 }\n </td>\n <td>{{ ar.complianceSynced ?? 0 }}</td>\n <td class=\"text-muted small\">{{ ar.error || ar.reason || '' }}</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n <!-- Message -->\n @if (message) {\n <div class=\"mt-3 alert\" [class.alert-success]=\"messageType === 'success'\"\n [class.alert-danger]=\"messageType === 'error'\">\n {{ message }}\n </div>\n }\n </div>\n</section>\n", styles: [".fixture-conformance .section-heading{color:#344054;font-weight:600}.fixture-conformance .fc-table th{background-color:#f9fafb;font-weight:600;font-size:13px;color:#475467;white-space:nowrap}.fixture-conformance .fc-table td{font-size:13px;vertical-align:middle}.fixture-conformance .fc-table .zones{font-family:Courier New,monospace;font-size:12px;color:#475467;word-break:break-word}.fixture-conformance .fc-table .fixture-id{font-family:Courier New,monospace;font-size:12px;color:#667085}.fixture-conformance .fc-table .selectable-row{cursor:pointer}.fixture-conformance .fc-table .selectable-row:hover{background-color:#f9fafb}.fixture-conformance .fc-table .row-fixable{background-color:#fffaf0}.fixture-conformance .status-badge{display:inline-block;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:600;white-space:nowrap}.fixture-conformance .status-badge.badge-ok{background-color:#ecfdf3;color:#027a48}.fixture-conformance .status-badge.badge-warn{background-color:#fffaeb;color:#b54708}.fixture-conformance .status-badge.badge-muted{background-color:#f2f4f7;color:#475467}.fixture-conformance .status-badge.badge-danger{background-color:#fef3f2;color:#b42318}\n"] }]
69180
+ args: [{ selector: 'lib-fixture-conformance', template: "<section class=\"fixture-conformance p-3\">\n <lib-tool-page-header\n title=\"Sync Fixtures to Library\"\n description=\"Check a store's fixtures against the library, then sync shelf count and zones for the ones that don't match.\"\n ></lib-tool-page-header>\n\n <div class=\"card p-4\">\n <!-- Store Select -->\n <div class=\"mb-3\" style=\"max-width: 600px;\">\n <label class=\"form-label\">Store</label>\n <lib-store-select\n [options]=\"storeOptions\"\n [singleSelect]=\"true\"\n (selectedChange)=\"onStoresSelected($event)\">\n </lib-store-select>\n </div>\n\n <!-- Fixture List -->\n @if (selectedStoreName) {\n @if (isLoadingFixtures) {\n <div class=\"text-muted\">Loading fixtures...</div>\n } @else if (fixtures.length === 0) {\n <div class=\"text-muted\">No fixtures found for this store.</div>\n } @else {\n <div class=\"d-flex align-items-center justify-content-between mb-2\">\n <h6 class=\"section-heading mb-0\">\n Fixtures\n <span class=\"text-muted small\">({{ selectedFixtureIds.length }} of {{ fixtures.length }} selected)</span>\n </h6>\n <button\n class=\"btn btn-primary btn-sm\"\n (click)=\"onAudit()\"\n [disabled]=\"selectedFixtureIds.length === 0 || isAuditing\">\n {{ isAuditing ? 'Auditing...' : 'Audit Selected' }}\n </button>\n </div>\n\n <div class=\"table-responsive mb-2\">\n <table class=\"table table-sm align-middle fc-table\">\n <thead>\n <tr>\n <th style=\"width: 42px;\">\n <input type=\"checkbox\"\n [checked]=\"allFixturesSelected\"\n (change)=\"toggleAllFixtures($any($event.target).checked)\">\n </th>\n <th style=\"width: 90px;\">Fixture #</th>\n <th>Header</th>\n <th>Fixture ID</th>\n <th>Category</th>\n <th style=\"width: 90px;\">Width</th>\n </tr>\n </thead>\n <tbody>\n @for (f of fixtures; track f._id) {\n <tr (click)=\"f.selected = !f.selected\" class=\"selectable-row\">\n <td (click)=\"$event.stopPropagation()\">\n <input type=\"checkbox\" [(ngModel)]=\"f.selected\">\n </td>\n <td>{{ f.fixtureNumber ?? '\u2014' }}</td>\n <td>{{ f.headerLabel || '\u2014' }}</td>\n <td class=\"fixture-id\">{{ f._id }}</td>\n <td>{{ f.fixtureCategory || '\u2014' }}</td>\n <td>{{ f.width || '\u2014' }}</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n }\n\n <!-- Audit Results -->\n @if (hasAudited) {\n <hr class=\"my-4\" />\n <div class=\"d-flex align-items-center justify-content-between mb-2\">\n <h6 class=\"section-heading mb-0\">\n Audit Result\n <span class=\"text-muted small\">({{ fixableRows.length }} fixable, {{ selectedAuditIds.length }} selected)</span>\n </h6>\n <button\n class=\"btn btn-success btn-sm\"\n (click)=\"onApplyFix()\"\n [disabled]=\"selectedAuditIds.length === 0 || isApplying\">\n {{ isApplying ? 'Applying...' : 'Apply Fix' }}\n </button>\n </div>\n\n @if (auditRows.length === 0) {\n <div class=\"text-muted\">No results.</div>\n } @else {\n <div class=\"table-responsive\">\n <table class=\"table table-sm align-middle fc-table\">\n <thead>\n <tr>\n <th style=\"width: 42px;\">\n <input type=\"checkbox\"\n [checked]=\"allFixableSelected\"\n [disabled]=\"fixableRows.length === 0\"\n (change)=\"toggleAllFixable($any($event.target).checked)\">\n </th>\n <th style=\"width: 90px;\">Fixture #</th>\n <th>Header</th>\n <th>Category</th>\n <th style=\"width: 70px;\">Width</th>\n <th style=\"width: 110px;\">Shelves</th>\n <th>Current Zones</th>\n <th>Library Zones</th>\n <th>Current Types</th>\n <th>Library Types</th>\n <th style=\"width: 160px;\">Status</th>\n </tr>\n </thead>\n <tbody>\n @for (r of auditRows; track r.fixtureId) {\n <tr [class.row-fixable]=\"r.fixable\">\n <td>\n <input type=\"checkbox\" [(ngModel)]=\"r.selected\" [disabled]=\"!r.fixable\">\n </td>\n <td>{{ r.fixtureNumber ?? '\u2014' }}</td>\n <td>{{ r.header || '\u2014' }}</td>\n <td>{{ r.category || '\u2014' }}</td>\n <td>{{ r.width || '\u2014' }}</td>\n <td>\n @if (r.libShelfCount != null) {\n <span [class.text-danger]=\"r.curShelfCount !== r.libShelfCount\">\n {{ r.curShelfCount }} \u2192 {{ r.libShelfCount }}\n </span>\n } @else {\n {{ r.curShelfCount }}\n }\n </td>\n <td class=\"zones\">{{ r.currentZones || '\u2014' }}</td>\n <td class=\"zones\">{{ r.libZones || '\u2014' }}</td>\n <td class=\"zones\" [class.text-danger]=\"r.reason === 'type-mismatch'\">{{ r.currentTypes || '\u2014' }}</td>\n <td class=\"zones\">{{ r.libTypes || '\u2014' }}</td>\n <td>\n <span class=\"status-badge\" [ngClass]=\"reasonClass(r.reason)\">\n {{ reasonLabel(r.reason) }}\n </span>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n }\n\n <!-- Apply summary per fixture -->\n @if (applyResults.length > 0) {\n <hr class=\"my-4\" />\n <h6 class=\"section-heading mb-2\">Apply Summary</h6>\n <div class=\"table-responsive\">\n <table class=\"table table-sm align-middle fc-table\">\n <thead>\n <tr>\n <th style=\"width: 90px;\">Fixture #</th>\n <th style=\"width: 140px;\">Status</th>\n <th>Shelves</th>\n <th>Compliance Synced</th>\n <th>Note</th>\n </tr>\n </thead>\n <tbody>\n @for (ar of applyResults; track ar.fixtureId) {\n <tr>\n <td>{{ ar.fixtureNumber ?? '\u2014' }}</td>\n <td>\n <span class=\"status-badge\"\n [class.badge-ok]=\"ar.status === 'fixed' || ar.status === 'already-conform'\"\n [class.badge-warn]=\"ar.status === 'skipped'\"\n [class.badge-danger]=\"ar.status === 'failed'\">\n {{ ar.status }}\n </span>\n </td>\n <td>\n @if (ar.shelvesFrom != null) {\n {{ ar.shelvesFrom }} \u2192 {{ ar.shelvesTo }}\n } @else { \u2014 }\n </td>\n <td>{{ ar.complianceSynced ?? 0 }}</td>\n <td class=\"text-muted small\">{{ ar.error || ar.reason || '' }}</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n <!-- Message -->\n @if (message) {\n <div class=\"mt-3 alert\" [class.alert-success]=\"messageType === 'success'\"\n [class.alert-danger]=\"messageType === 'error'\">\n {{ message }}\n </div>\n }\n </div>\n</section>\n", styles: [".fixture-conformance .section-heading{color:#344054;font-weight:600}.fixture-conformance .fc-table th{background-color:#f9fafb;font-weight:600;font-size:13px;color:#475467;white-space:nowrap}.fixture-conformance .fc-table td{font-size:13px;vertical-align:middle}.fixture-conformance .fc-table .zones{font-family:Courier New,monospace;font-size:12px;color:#475467;word-break:break-word}.fixture-conformance .fc-table .fixture-id{font-family:Courier New,monospace;font-size:12px;color:#667085}.fixture-conformance .fc-table .selectable-row{cursor:pointer}.fixture-conformance .fc-table .selectable-row:hover{background-color:#f9fafb}.fixture-conformance .fc-table .row-fixable{background-color:#fffaf0}.fixture-conformance .status-badge{display:inline-block;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:600;white-space:nowrap}.fixture-conformance .status-badge.badge-ok{background-color:#ecfdf3;color:#027a48}.fixture-conformance .status-badge.badge-warn{background-color:#fffaeb;color:#b54708}.fixture-conformance .status-badge.badge-muted{background-color:#f2f4f7;color:#475467}.fixture-conformance .status-badge.badge-danger{background-color:#fef3f2;color:#b42318}\n"] }]
69122
69181
  }], ctorParameters: () => [{ type: StoreBuilderService }, { type: i2$1.GlobalStateService }] });
69123
69182
 
69124
69183
  class SwapTemplateComponent {
@@ -80271,4 +80330,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
80271
80330
  */
80272
80331
 
80273
80332
  export { StoreBuilderService as S, TangoStoreBuilderModule as T, TangoStorePlanoModule as a };
80274
- //# sourceMappingURL=tango-app-ui-store-builder-tango-app-ui-store-builder-UJrKxjTl.mjs.map
80333
+ //# sourceMappingURL=tango-app-ui-store-builder-tango-app-ui-store-builder-CXw6OsFW.mjs.map