tin-spa 20.14.14 → 20.14.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, InjectionToken, makeEnvironmentProviders, Optional, Inject, Component, EventEmitter, Output, Input, inject, ViewChild, Pipe, ChangeDetectionStrategy, ViewEncapsulation, forwardRef, HostListener, Directive, TemplateRef, ContentChildren, ViewChildren, SecurityContext, ContentChild, NgModule } from '@angular/core';
2
+ import { Injectable, InjectionToken, makeEnvironmentProviders, Optional, Inject, Component, EventEmitter, Output, Input, inject, ViewChild, Pipe, ChangeDetectionStrategy, ViewEncapsulation, forwardRef, HostListener, Directive, ViewChildren, SecurityContext, ContentChild, NgModule } from '@angular/core';
3
3
  import * as i4 from '@angular/material/dialog';
4
4
  import { MAT_DIALOG_DATA, MatDialogModule, MAT_DIALOG_DEFAULT_OPTIONS, MatDialog, MatDialogRef } from '@angular/material/dialog';
5
5
  import { BehaviorSubject, Subject, from, throwError, of, timeout, firstValueFrom, tap as tap$1, catchError as catchError$1, finalize, share, Observable, filter as filter$1, combineLatest, switchMap, startWith as startWith$1, map as map$1, interval } from 'rxjs';
@@ -34,9 +34,9 @@ import * as i19 from '@angular/material/card';
34
34
  import { MatCardModule } from '@angular/material/card';
35
35
  import * as i5 from '@angular/material/progress-bar';
36
36
  import { MatProgressBarModule } from '@angular/material/progress-bar';
37
- import * as i14$1 from '@angular/material/table';
37
+ import * as i14 from '@angular/material/table';
38
38
  import { MatTableDataSource, MatTableModule } from '@angular/material/table';
39
- import * as i15 from '@angular/material/paginator';
39
+ import * as i15$1 from '@angular/material/paginator';
40
40
  import { MatPaginatorModule } from '@angular/material/paginator';
41
41
  import * as i7 from '@angular/material/tooltip';
42
42
  import { MatTooltipModule } from '@angular/material/tooltip';
@@ -48,7 +48,7 @@ import * as i4$3 from '@angular/material/checkbox';
48
48
  import { MatCheckboxModule } from '@angular/material/checkbox';
49
49
  import * as i7$1 from '@angular/material/select';
50
50
  import { MatSelect, MatSelectModule } from '@angular/material/select';
51
- import * as i12 from '@angular/material/autocomplete';
51
+ import * as i13 from '@angular/material/autocomplete';
52
52
  import { MatAutocompleteModule } from '@angular/material/autocomplete';
53
53
  import * as i5$1 from '@angular/material/datepicker';
54
54
  import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -58,13 +58,13 @@ import * as i17 from '@angular/material/list';
58
58
  import { MatListModule } from '@angular/material/list';
59
59
  import * as i10 from 'ng2-charts';
60
60
  import { BaseChartDirective, provideCharts, withDefaultRegisterables } from 'ng2-charts';
61
- import * as i7$2 from '@angular/material/menu';
61
+ import * as i4$4 from '@angular/material/menu';
62
62
  import { MatMenuModule } from '@angular/material/menu';
63
- import * as i14 from '@angular/cdk/drag-drop';
63
+ import * as i15 from '@angular/cdk/drag-drop';
64
64
  import { moveItemInArray, transferArrayItem, DragDropModule } from '@angular/cdk/drag-drop';
65
65
  import imageCompression from 'browser-image-compression';
66
66
  import { trigger, transition, style, animate } from '@angular/animations';
67
- import * as i4$4 from '@angular/material/badge';
67
+ import * as i4$5 from '@angular/material/badge';
68
68
  import { MatBadgeModule } from '@angular/material/badge';
69
69
  import { MatNativeDateModule, MAT_DATE_LOCALE } from '@angular/material/core';
70
70
  import * as i20$1 from '@angular/material/progress-spinner';
@@ -72,9 +72,9 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
72
72
  import { MatRadioModule } from '@angular/material/radio';
73
73
  import { MatSliderModule } from '@angular/material/slider';
74
74
  import { MatSortModule } from '@angular/material/sort';
75
- import * as i4$5 from '@angular/material/stepper';
75
+ import * as i5$3 from '@angular/material/stepper';
76
76
  import { MatStepperModule } from '@angular/material/stepper';
77
- import * as i5$3 from '@angular/material/tabs';
77
+ import * as i5$4 from '@angular/material/tabs';
78
78
  import { MatTabsModule } from '@angular/material/tabs';
79
79
  import * as i19$1 from '@angular/material/sidenav';
80
80
  import { MatSidenavModule } from '@angular/material/sidenav';
@@ -2075,13 +2075,24 @@ class OfflineService {
2075
2075
  const basePrefixed = this.readPrefixes.find(p => base.startsWith(p.prefix));
2076
2076
  return basePrefixed ? basePrefixed.entry : null;
2077
2077
  }
2078
+ // Changed (O1): the grammar now accepts path segments before `?action=` — `transactions/dto?action=create`
2079
+ // was failing the old `[^?/]+` on the slash alone, which made the WRITE half of the library's own
2080
+ // offline-enabled transactions grid a silent no-op (accounting.service.ts:762 promises queueing it never did).
2081
+ // The entity is the FIRST path segment, the same convention register() derives it from loadAction.url, so a
2082
+ // bare `customers?action=create` resolves exactly as before. This deliberately does NOT admit the other
2083
+ // grammar casualties: `checkactions?action=yes|no` (verbs stay create|edit|delete — mapping yes/no onto queue
2084
+ // semantics is a design decision, not a parse), `pigmovements/move` / `pigtransactions/*` (no action grammar
2085
+ // at all — queueing them needs a config surface declaring each custom endpoint's CRUD semantics), and the
2086
+ // `<entity>/form?action=` family, which is refused one level up (CallApi's isFormData guard) because file
2087
+ // uploads cannot queue. For those, a match here only ever yields the explicit 'File uploads require a
2088
+ // connection' message instead of a raw connection error — nothing queues.
2078
2089
  matchMutation(url) {
2079
2090
  if (!this.hasAny)
2080
2091
  return null;
2081
- const match = url.match(/^([^?/]+)\?action=(create|edit|delete)$/);
2092
+ const match = url.match(/^([^?]+)\?action=(create|edit|delete)$/); // Changed: [^?/]+ -> [^?]+ — allow slashes in the path
2082
2093
  if (!match)
2083
2094
  return null;
2084
- const entry = this.entries.get(match[1]);
2095
+ const entry = this.entries.get(match[1].split('/')[0]); // Changed: entity = first path segment (unchanged for slash-free urls)
2085
2096
  if (!entry)
2086
2097
  return null;
2087
2098
  return entry.config.queue[match[2]] ? entry : null;
@@ -12204,12 +12215,14 @@ class FieldLoadIndicator {
12204
12215
  class TextSingleComponent {
12205
12216
  constructor(dataService, dialogService, // Changed: Added DialogService injection
12206
12217
  buttonService, // Changed: Added ButtonService injection
12207
- messageService // Changed: Added MessageService injection
12218
+ messageService, // Changed: Added MessageService injection
12219
+ apiErrorService // Added: so a failed option load is no longer silent
12208
12220
  ) {
12209
12221
  this.dataService = dataService;
12210
12222
  this.dialogService = dialogService;
12211
12223
  this.buttonService = buttonService;
12212
12224
  this.messageService = messageService;
12225
+ this.apiErrorService = apiErrorService;
12213
12226
  this.multiDimension = false;
12214
12227
  this.appearance = 'outline';
12215
12228
  this.readonly = false;
@@ -12465,6 +12478,9 @@ class TextSingleComponent {
12465
12478
  }
12466
12479
  this.initFilter();
12467
12480
  }
12481
+ else {
12482
+ this.apiErrorService.presentAppFailure(apiResponse, 'load', action.url); // Added: was a silent no-op — an autocomplete that failed to load its options looked identical to one with none; 'load' dedupes with the page's other loads
12483
+ }
12468
12484
  });
12469
12485
  }
12470
12486
  getValidators() {
@@ -12561,13 +12577,13 @@ class TextSingleComponent {
12561
12577
  this.value = createdID;
12562
12578
  this.valueChange.emit(this.value);
12563
12579
  }
12564
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TextSingleComponent, deps: [{ token: DataServiceLib }, { token: DialogService }, { token: ButtonService }, { token: MessageService }], target: i0.ɵɵFactoryTarget.Component }); }
12565
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TextSingleComponent, isStandalone: false, selector: "spa-text-single", inputs: { appearance: "appearance", readonly: "readonly", hint: "hint", display: "display", placeholder: "placeholder", value: "value", width: "width", copyContent: "copyContent", clearContent: "clearContent", options: "options", optionDisplay: "optionDisplay", optionValue: "optionValue", loadAction: "loadAction", required: "required", min: "min", max: "max", regex: "regex", suffix: "suffix", infoMessage: "infoMessage", field: "field", data: "data", detailsConfig: "detailsConfig", masterField: "masterField" }, outputs: { valueChange: "valueChange", leave: "leave", enterPress: "enterPress" }, usesOnChanges: true, ngImport: i0, template: "<!-- Autocomplete text input -->\n<mat-form-field [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [hintLabel]=\"hint\" [appearance]=\"appearance ?? 'fill'\" [ngStyle]=\"{'width':width ?? '100%'}\" style=\"margin-right: 5px;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <input [placeholder]=\"placeholder\" [formControl]=\"myControl\" matInput [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\" [required]=\"required\" [readonly]=\"readonly\">\n <mat-error *ngIf=\"myControl.invalid && (myControl.dirty || myControl.touched)\">{{validate(myControl)}}</mat-error>\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"onOptionSelected($event)\"> <!-- Changed: route through onOptionSelected so the \"add new\" option opens the create dialog -->\n <!-- Added (dropdown quick-add): always-first create option \u2014 outside the filter, so it shows even when nothing matches -->\n <mat-option *ngIf=\"addOptionLabel\" [value]=\"ADD_NEW_OPTION\" class=\"spa-add-new-option\">\n <mat-icon class=\"tinyIcon spa-add-new-icon\">add</mat-icon>{{addOptionLabel}}\n </mat-option>\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"multiDimension ? option[optionDisplay] : option\">\n {{multiDimension ? option[optionDisplay] : option}}\n </mat-option>\n </mat-autocomplete>\n <div matSuffix class=\"suffix-icons\">\n <!-- Changed: Added Add button for creating new items via detailsConfig -->\n <button mat-icon-button *ngIf=\"detailsConfig && canCreate() && isHovered\" (click)=\"onPeekClick($event, 'create')\" matTooltip=\"Add\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" style=\"color: green;\">add</mat-icon>\n </button>\n <!-- Changed: Added View button for viewing selected item via detailsConfig -->\n <button mat-icon-button *ngIf=\"detailsConfig && canView() && isHovered && value\" (click)=\"onPeekClick($event, 'view')\" matTooltip=\"View\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" color=\"primary\">launch</mat-icon>\n </button>\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [".spa-add-new-option{border-bottom:1px solid rgba(0,0,0,.12);font-weight:500;color:green}.spa-add-new-icon{color:green;vertical-align:middle;margin-right:4px}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i2$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: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3$1.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4$2.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i7$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i12.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "directive", type: i12.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }] }); }
12580
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TextSingleComponent, deps: [{ token: DataServiceLib }, { token: DialogService }, { token: ButtonService }, { token: MessageService }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
12581
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TextSingleComponent, isStandalone: false, selector: "spa-text-single", inputs: { appearance: "appearance", readonly: "readonly", hint: "hint", display: "display", placeholder: "placeholder", value: "value", width: "width", copyContent: "copyContent", clearContent: "clearContent", options: "options", optionDisplay: "optionDisplay", optionValue: "optionValue", loadAction: "loadAction", required: "required", min: "min", max: "max", regex: "regex", suffix: "suffix", infoMessage: "infoMessage", field: "field", data: "data", detailsConfig: "detailsConfig", masterField: "masterField" }, outputs: { valueChange: "valueChange", leave: "leave", enterPress: "enterPress" }, usesOnChanges: true, ngImport: i0, template: "<!-- Autocomplete text input -->\n<mat-form-field [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [hintLabel]=\"hint\" [appearance]=\"appearance ?? 'fill'\" [ngStyle]=\"{'width':width ?? '100%'}\" style=\"margin-right: 5px;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <input [placeholder]=\"placeholder\" [formControl]=\"myControl\" matInput [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\" [required]=\"required\" [readonly]=\"readonly\">\n <mat-error *ngIf=\"myControl.invalid && (myControl.dirty || myControl.touched)\">{{validate(myControl)}}</mat-error>\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"onOptionSelected($event)\"> <!-- Changed: route through onOptionSelected so the \"add new\" option opens the create dialog -->\n <!-- Added (dropdown quick-add): always-first create option \u2014 outside the filter, so it shows even when nothing matches -->\n <mat-option *ngIf=\"addOptionLabel\" [value]=\"ADD_NEW_OPTION\" class=\"spa-add-new-option\">\n <mat-icon class=\"tinyIcon spa-add-new-icon\">add</mat-icon>{{addOptionLabel}}\n </mat-option>\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"multiDimension ? option[optionDisplay] : option\">\n {{multiDimension ? option[optionDisplay] : option}}\n </mat-option>\n </mat-autocomplete>\n <div matSuffix class=\"suffix-icons\">\n <!-- Changed: Added Add button for creating new items via detailsConfig -->\n <button mat-icon-button *ngIf=\"detailsConfig && canCreate() && isHovered\" (click)=\"onPeekClick($event, 'create')\" matTooltip=\"Add\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" style=\"color: green;\">add</mat-icon>\n </button>\n <!-- Changed: Added View button for viewing selected item via detailsConfig -->\n <button mat-icon-button *ngIf=\"detailsConfig && canView() && isHovered && value\" (click)=\"onPeekClick($event, 'view')\" matTooltip=\"View\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" color=\"primary\">launch</mat-icon>\n </button>\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [".spa-add-new-option{border-bottom:1px solid rgba(0,0,0,.12);font-weight:500;color:green}.spa-add-new-icon{color:green;vertical-align:middle;margin-right:4px}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i2$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: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3$1.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4$2.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i7$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i13.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "directive", type: i13.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }] }); }
12566
12582
  }
12567
12583
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TextSingleComponent, decorators: [{
12568
12584
  type: Component,
12569
12585
  args: [{ selector: 'spa-text-single', standalone: false, template: "<!-- Autocomplete text input -->\n<mat-form-field [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [hintLabel]=\"hint\" [appearance]=\"appearance ?? 'fill'\" [ngStyle]=\"{'width':width ?? '100%'}\" style=\"margin-right: 5px;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <input [placeholder]=\"placeholder\" [formControl]=\"myControl\" matInput [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\" [required]=\"required\" [readonly]=\"readonly\">\n <mat-error *ngIf=\"myControl.invalid && (myControl.dirty || myControl.touched)\">{{validate(myControl)}}</mat-error>\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"onOptionSelected($event)\"> <!-- Changed: route through onOptionSelected so the \"add new\" option opens the create dialog -->\n <!-- Added (dropdown quick-add): always-first create option \u2014 outside the filter, so it shows even when nothing matches -->\n <mat-option *ngIf=\"addOptionLabel\" [value]=\"ADD_NEW_OPTION\" class=\"spa-add-new-option\">\n <mat-icon class=\"tinyIcon spa-add-new-icon\">add</mat-icon>{{addOptionLabel}}\n </mat-option>\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"multiDimension ? option[optionDisplay] : option\">\n {{multiDimension ? option[optionDisplay] : option}}\n </mat-option>\n </mat-autocomplete>\n <div matSuffix class=\"suffix-icons\">\n <!-- Changed: Added Add button for creating new items via detailsConfig -->\n <button mat-icon-button *ngIf=\"detailsConfig && canCreate() && isHovered\" (click)=\"onPeekClick($event, 'create')\" matTooltip=\"Add\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" style=\"color: green;\">add</mat-icon>\n </button>\n <!-- Changed: Added View button for viewing selected item via detailsConfig -->\n <button mat-icon-button *ngIf=\"detailsConfig && canView() && isHovered && value\" (click)=\"onPeekClick($event, 'view')\" matTooltip=\"View\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" color=\"primary\">launch</mat-icon>\n </button>\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [".spa-add-new-option{border-bottom:1px solid rgba(0,0,0,.12);font-weight:500;color:green}.spa-add-new-icon{color:green;vertical-align:middle;margin-right:4px}\n"] }]
12570
- }], ctorParameters: () => [{ type: DataServiceLib }, { type: DialogService }, { type: ButtonService }, { type: MessageService }], propDecorators: { appearance: [{
12586
+ }], ctorParameters: () => [{ type: DataServiceLib }, { type: DialogService }, { type: ButtonService }, { type: MessageService }, { type: ApiErrorService }], propDecorators: { appearance: [{
12571
12587
  type: Input
12572
12588
  }], readonly: [{
12573
12589
  type: Input
@@ -12726,9 +12742,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
12726
12742
  }] } });
12727
12743
 
12728
12744
  class SelectCommonComponent {
12729
- constructor(messageService, dataService) {
12745
+ constructor(messageService, dataService, apiErrorService) {
12730
12746
  this.messageService = messageService;
12731
12747
  this.dataService = dataService;
12748
+ this.apiErrorService = apiErrorService;
12732
12749
  this.isHovered = false;
12733
12750
  this.selectedOptionHint = "";
12734
12751
  // Added: drives the suffix refresh icon — held visible and spinning while the loadAction is in flight
@@ -12752,7 +12769,7 @@ class SelectCommonComponent {
12752
12769
  this.valueChange = new EventEmitter();
12753
12770
  this.hoverChange = new EventEmitter();
12754
12771
  this.addNew = new EventEmitter(); // Added (dropdown quick-add): fired when the "add new" option is picked
12755
- }
12772
+ } // Changed: injected ApiErrorService so a failed option load is no longer silent
12756
12773
  ngOnInit() {
12757
12774
  if (this.placeholder == "") {
12758
12775
  this.placeholder = "Select " + this.display;
@@ -12936,6 +12953,9 @@ class SelectCommonComponent {
12936
12953
  }
12937
12954
  this.updateSelectedOptionHint();
12938
12955
  }
12956
+ else {
12957
+ this.apiErrorService.presentAppFailure(apiResponse, 'load', transformedAction.url); // Added: was a silent no-op — a dropdown that failed to load looked identical to a dropdown with no options; 'load' dedupes with the page's other loads so a broken backend yields one dialog
12958
+ }
12939
12959
  });
12940
12960
  }
12941
12961
  ngOnDestroy() {
@@ -12944,13 +12964,13 @@ class SelectCommonComponent {
12944
12964
  }
12945
12965
  this.loadIndicator.destroy(); // Added: clear any pending delay/hold timer on a field torn down mid-read
12946
12966
  }
12947
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SelectCommonComponent, deps: [{ token: MessageService }, { token: DataServiceLib }], target: i0.ɵɵFactoryTarget.Component }); }
12967
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SelectCommonComponent, deps: [{ token: MessageService }, { token: DataServiceLib }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
12948
12968
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SelectCommonComponent, isStandalone: false, selector: "spa-select-common", inputs: { width: "width", readonly: "readonly", required: "required", defaultFirstValue: "defaultFirstValue", readonlyMode: "readonlyMode", hint: "hint", placeholder: "placeholder", display: "display", value: "value", options: "options", masterOptions: "masterOptions", masterField: "masterField", optionValue: "optionValue", optionDisplay: "optionDisplay", optionDisplayExtra: "optionDisplayExtra", nullable: "nullable", infoMessage: "infoMessage", copyContent: "copyContent", loadAction: "loadAction", loadIDField: "loadIDField", field: "field", data: "data", addOptionLabel: "addOptionLabel" }, outputs: { valueChange: "valueChange", hoverChange: "hoverChange", addNew: "addNew" }, viewQueries: [{ propertyName: "matSelect", first: true, predicate: MatSelect, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<mat-form-field *ngIf=\"readonlyMode==''\" floatLabel=\"always\" [hintLabel]=\"computedHint\" [ngStyle]=\"{'width':width ?? '100%'}\" [hideRequiredMarker]=\"false\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n\n <mat-label>{{display}}</mat-label>\n <!-- Removed: [multiple]=\"multiple\" binding - component now only supports single selection -->\n <mat-select [(value)]=\"value\" (selectionChange)=\"changed()\" [disabled]=\"readonly\" [placeholder]=\"placeholder\" [required]=\"required\">\n <!-- Added (dropdown quick-add): always-first create option \u2014 shown even when the list is empty; picking it opens the create dialog -->\n <mat-option *ngIf=\"addOptionLabel\" [value]=\"ADD_NEW_OPTION\" class=\"spa-add-new-option\">\n <mat-icon class=\"tinyIcon spa-add-new-icon\">add</mat-icon>{{addOptionLabel}}\n </mat-option>\n <mat-option *ngFor=\"let row of options\" [value]=\"row[optionValue]\">\n {{row[optionDisplay]}} <label *ngIf=\"optionDisplayExtra!='' && row[optionDisplayExtra] && row[optionDisplayExtra] != ''\">({{row[optionDisplayExtra]}})</label>\n </mat-option>\n </mat-select>\n\n <div matSuffix class=\"suffix-icons\">\n <ng-content select=\"[additionalButtons]\"></ng-content>\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning) && !masterField\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n\n</mat-form-field>\n", styles: [".spa-add-new-option{border-bottom:1px solid rgba(0,0,0,.12);font-weight:500;color:green}.spa-add-new-icon{color:green;vertical-align:middle;margin-right:4px}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i7$1.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }] }); }
12949
12969
  }
12950
12970
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SelectCommonComponent, decorators: [{
12951
12971
  type: Component,
12952
12972
  args: [{ selector: 'spa-select-common', standalone: false, template: "<mat-form-field *ngIf=\"readonlyMode==''\" floatLabel=\"always\" [hintLabel]=\"computedHint\" [ngStyle]=\"{'width':width ?? '100%'}\" [hideRequiredMarker]=\"false\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n\n <mat-label>{{display}}</mat-label>\n <!-- Removed: [multiple]=\"multiple\" binding - component now only supports single selection -->\n <mat-select [(value)]=\"value\" (selectionChange)=\"changed()\" [disabled]=\"readonly\" [placeholder]=\"placeholder\" [required]=\"required\">\n <!-- Added (dropdown quick-add): always-first create option \u2014 shown even when the list is empty; picking it opens the create dialog -->\n <mat-option *ngIf=\"addOptionLabel\" [value]=\"ADD_NEW_OPTION\" class=\"spa-add-new-option\">\n <mat-icon class=\"tinyIcon spa-add-new-icon\">add</mat-icon>{{addOptionLabel}}\n </mat-option>\n <mat-option *ngFor=\"let row of options\" [value]=\"row[optionValue]\">\n {{row[optionDisplay]}} <label *ngIf=\"optionDisplayExtra!='' && row[optionDisplayExtra] && row[optionDisplayExtra] != ''\">({{row[optionDisplayExtra]}})</label>\n </mat-option>\n </mat-select>\n\n <div matSuffix class=\"suffix-icons\">\n <ng-content select=\"[additionalButtons]\"></ng-content>\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning) && !masterField\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n\n</mat-form-field>\n", styles: [".spa-add-new-option{border-bottom:1px solid rgba(0,0,0,.12);font-weight:500;color:green}.spa-add-new-icon{color:green;vertical-align:middle;margin-right:4px}\n"] }]
12953
- }], ctorParameters: () => [{ type: MessageService }, { type: DataServiceLib }], propDecorators: { matSelect: [{
12973
+ }], ctorParameters: () => [{ type: MessageService }, { type: DataServiceLib }, { type: ApiErrorService }], propDecorators: { matSelect: [{
12954
12974
  type: ViewChild,
12955
12975
  args: [MatSelect]
12956
12976
  }], width: [{
@@ -13023,9 +13043,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
13023
13043
  }] });
13024
13044
 
13025
13045
  class TextMultiComponent {
13026
- constructor(messageService, dataService) {
13046
+ constructor(messageService, dataService, apiErrorService) {
13027
13047
  this.messageService = messageService;
13028
13048
  this.dataService = dataService;
13049
+ this.apiErrorService = apiErrorService;
13029
13050
  this.display = "";
13030
13051
  this.value = "";
13031
13052
  this.valueChange = new EventEmitter();
@@ -13046,7 +13067,7 @@ class TextMultiComponent {
13046
13067
  // Added: drives the suffix refresh icon — held visible and spinning while the loadAction is in flight
13047
13068
  this.loadIndicator = new FieldLoadIndicator();
13048
13069
  this.hoverChange = new EventEmitter();
13049
- }
13070
+ } // Changed: injected ApiErrorService so a failed option load is no longer silent
13050
13071
  ngOnInit() {
13051
13072
  this.values = this.value ? this.value.split(';').filter(val => val.trim() !== '') : [];
13052
13073
  this.setupAutoComplete();
@@ -13173,18 +13194,21 @@ class TextMultiComponent {
13173
13194
  this.options = apiResponse.data;
13174
13195
  this.setupAutoComplete();
13175
13196
  }
13197
+ else {
13198
+ this.apiErrorService.presentAppFailure(apiResponse, 'load', action.url); // Added: was a silent no-op — a chip autocomplete that failed to load looked identical to one with no options; 'load' dedupes with the page's other loads
13199
+ }
13176
13200
  });
13177
13201
  }
13178
13202
  ngOnDestroy() {
13179
13203
  this.loadIndicator.destroy(); // Added: clear any pending delay/hold timer on a field torn down mid-read
13180
13204
  }
13181
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TextMultiComponent, deps: [{ token: MessageService }, { token: DataServiceLib }], target: i0.ɵɵFactoryTarget.Component }); }
13182
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TextMultiComponent, isStandalone: false, selector: "spa-text-multi", inputs: { display: "display", value: "value", readonly: "readonly", required: "required", hint: "hint", strict: "strict", suffix: "suffix", infoMessage: "infoMessage", copyContent: "copyContent", clearContent: "clearContent", options: "options", optionDisplay: "optionDisplay", optionValue: "optionValue", loadAction: "loadAction" }, outputs: { valueChange: "valueChange", hoverChange: "hoverChange" }, viewQueries: [{ propertyName: "textInput", first: true, predicate: ["textInput"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<mat-form-field [class.spa-readonly]=\"readonly\" style=\"width: 100%;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <mat-chip-grid #chipList>\n <mat-chip-row *ngFor=\"let value of values\" [removable]=\"!readonly\" (removed)=\"remove(value)\">\n {{getDisplayValue(value)}}\n <button matChipRemove *ngIf=\"!readonly\"><mat-icon class=\"tinyIcon\">cancel</mat-icon></button>\n </mat-chip-row>\n <input #textInput autocomplete=\"off\" [placeholder]=\"readonly ? '' : display\" [matChipInputFor]=\"chipList\" [matChipInputSeparatorKeyCodes]=\"[13, 186]\" [matChipInputAddOnBlur]=\"true\" (matChipInputTokenEnd)=\"add($event)\" [formControl]=\"control\" [readonly]=\"readonly\" [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\">\n </mat-chip-grid>\n\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"optionSelected($event)\">\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"option\">\n {{option[optionDisplay]}}\n </mat-option>\n </mat-autocomplete>\n\n <mat-error *ngIf=\"errorState\">{{hint}}</mat-error>\n <mat-hint *ngIf=\"hint && !errorState\">{{hint}}</mat-hint>\n\n <div matSuffix class=\"suffix-icons\">\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"true\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$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: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3$1.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3$1.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i6.MatChipGrid, selector: "mat-chip-grid", inputs: ["disabled", "placeholder", "required", "value", "errorStateMatcher"], outputs: ["change", "valueChange"] }, { kind: "directive", type: i6.MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputFor", "matChipInputAddOnBlur", "matChipInputSeparatorKeyCodes", "placeholder", "id", "disabled", "readonly", "matChipInputDisabledInteractive"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { kind: "directive", type: i6.MatChipRemove, selector: "[matChipRemove]" }, { kind: "component", type: i6.MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "component", type: i7$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i12.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "directive", type: i12.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }] }); }
13205
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TextMultiComponent, deps: [{ token: MessageService }, { token: DataServiceLib }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
13206
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TextMultiComponent, isStandalone: false, selector: "spa-text-multi", inputs: { display: "display", value: "value", readonly: "readonly", required: "required", hint: "hint", strict: "strict", suffix: "suffix", infoMessage: "infoMessage", copyContent: "copyContent", clearContent: "clearContent", options: "options", optionDisplay: "optionDisplay", optionValue: "optionValue", loadAction: "loadAction" }, outputs: { valueChange: "valueChange", hoverChange: "hoverChange" }, viewQueries: [{ propertyName: "textInput", first: true, predicate: ["textInput"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<mat-form-field [class.spa-readonly]=\"readonly\" style=\"width: 100%;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <mat-chip-grid #chipList>\n <mat-chip-row *ngFor=\"let value of values\" [removable]=\"!readonly\" (removed)=\"remove(value)\">\n {{getDisplayValue(value)}}\n <button matChipRemove *ngIf=\"!readonly\"><mat-icon class=\"tinyIcon\">cancel</mat-icon></button>\n </mat-chip-row>\n <input #textInput autocomplete=\"off\" [placeholder]=\"readonly ? '' : display\" [matChipInputFor]=\"chipList\" [matChipInputSeparatorKeyCodes]=\"[13, 186]\" [matChipInputAddOnBlur]=\"true\" (matChipInputTokenEnd)=\"add($event)\" [formControl]=\"control\" [readonly]=\"readonly\" [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\">\n </mat-chip-grid>\n\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"optionSelected($event)\">\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"option\">\n {{option[optionDisplay]}}\n </mat-option>\n </mat-autocomplete>\n\n <mat-error *ngIf=\"errorState\">{{hint}}</mat-error>\n <mat-hint *ngIf=\"hint && !errorState\">{{hint}}</mat-hint>\n\n <div matSuffix class=\"suffix-icons\">\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"true\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$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: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3$1.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3$1.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i6.MatChipGrid, selector: "mat-chip-grid", inputs: ["disabled", "placeholder", "required", "value", "errorStateMatcher"], outputs: ["change", "valueChange"] }, { kind: "directive", type: i6.MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputFor", "matChipInputAddOnBlur", "matChipInputSeparatorKeyCodes", "placeholder", "id", "disabled", "readonly", "matChipInputDisabledInteractive"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { kind: "directive", type: i6.MatChipRemove, selector: "[matChipRemove]" }, { kind: "component", type: i6.MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "component", type: i7$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i13.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "directive", type: i13.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }] }); }
13183
13207
  }
13184
13208
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TextMultiComponent, decorators: [{
13185
13209
  type: Component,
13186
13210
  args: [{ selector: 'spa-text-multi', standalone: false, template: "<mat-form-field [class.spa-readonly]=\"readonly\" style=\"width: 100%;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <mat-chip-grid #chipList>\n <mat-chip-row *ngFor=\"let value of values\" [removable]=\"!readonly\" (removed)=\"remove(value)\">\n {{getDisplayValue(value)}}\n <button matChipRemove *ngIf=\"!readonly\"><mat-icon class=\"tinyIcon\">cancel</mat-icon></button>\n </mat-chip-row>\n <input #textInput autocomplete=\"off\" [placeholder]=\"readonly ? '' : display\" [matChipInputFor]=\"chipList\" [matChipInputSeparatorKeyCodes]=\"[13, 186]\" [matChipInputAddOnBlur]=\"true\" (matChipInputTokenEnd)=\"add($event)\" [formControl]=\"control\" [readonly]=\"readonly\" [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\">\n </mat-chip-grid>\n\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"optionSelected($event)\">\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"option\">\n {{option[optionDisplay]}}\n </mat-option>\n </mat-autocomplete>\n\n <mat-error *ngIf=\"errorState\">{{hint}}</mat-error>\n <mat-hint *ngIf=\"hint && !errorState\">{{hint}}</mat-hint>\n\n <div matSuffix class=\"suffix-icons\">\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"true\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n" }]
13187
- }], ctorParameters: () => [{ type: MessageService }, { type: DataServiceLib }], propDecorators: { textInput: [{
13211
+ }], ctorParameters: () => [{ type: MessageService }, { type: DataServiceLib }, { type: ApiErrorService }], propDecorators: { textInput: [{
13188
13212
  type: ViewChild,
13189
13213
  args: ['textInput']
13190
13214
  }], display: [{
@@ -13222,9 +13246,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
13222
13246
  }] } });
13223
13247
 
13224
13248
  class SelectMultiComponent {
13225
- constructor(messageService, dataService) {
13249
+ constructor(messageService, dataService, apiErrorService) {
13226
13250
  this.messageService = messageService;
13227
13251
  this.dataService = dataService;
13252
+ this.apiErrorService = apiErrorService;
13228
13253
  this.display = "";
13229
13254
  this.value = "";
13230
13255
  this.valueChange = new EventEmitter();
@@ -13246,7 +13271,7 @@ class SelectMultiComponent {
13246
13271
  // Added: drives the suffix refresh icon — held visible and spinning while the loadAction is in flight
13247
13272
  this.loadIndicator = new FieldLoadIndicator();
13248
13273
  this.hoverChange = new EventEmitter();
13249
- }
13274
+ } // Changed: injected ApiErrorService so a failed option load is no longer silent
13250
13275
  ngOnInit() {
13251
13276
  if (!this.placeholder) {
13252
13277
  this.placeholder = `Select ${this.display}`;
@@ -13348,18 +13373,21 @@ class SelectMultiComponent {
13348
13373
  this.options = apiResponse.data;
13349
13374
  this.initializeValues();
13350
13375
  }
13376
+ else {
13377
+ this.apiErrorService.presentAppFailure(apiResponse, 'load', action.url); // Added: was a silent no-op — a multi-select that failed to load looked identical to one with no options; 'load' dedupes with the page's other loads
13378
+ }
13351
13379
  });
13352
13380
  }
13353
13381
  ngOnDestroy() {
13354
13382
  this.loadIndicator.destroy(); // Added: clear any pending delay/hold timer on a field torn down mid-read
13355
13383
  }
13356
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SelectMultiComponent, deps: [{ token: MessageService }, { token: DataServiceLib }], target: i0.ɵɵFactoryTarget.Component }); }
13384
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SelectMultiComponent, deps: [{ token: MessageService }, { token: DataServiceLib }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
13357
13385
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SelectMultiComponent, isStandalone: false, selector: "spa-select-multi", inputs: { display: "display", value: "value", readonly: "readonly", required: "required", hint: "hint", options: "options", optionDisplay: "optionDisplay", optionValue: "optionValue", infoMessage: "infoMessage", copyContent: "copyContent", clearContent: "clearContent", nullable: "nullable", placeholder: "placeholder", width: "width", suffix: "suffix", loadAction: "loadAction", selectAll: "selectAll" }, outputs: { valueChange: "valueChange", hoverChange: "hoverChange" }, usesOnChanges: true, ngImport: i0, template: "<mat-form-field [ngStyle]=\"{'width':width ?? '100%'}\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <mat-select [formControl]=\"control\" [required]=\"required\" multiple [placeholder]=\"placeholder\" [compareWith]=\"compareWith\" (selectionChange)=\"selectionChange($event)\">\n <mat-option *ngIf=\"nullable\" [value]=\"null\">None</mat-option>\n <mat-option *ngFor=\"let option of options\" [value]=\"option[optionValue]\">\n {{option[optionDisplay]}}\n </mat-option>\n </mat-select>\n <mat-hint *ngIf=\"hint\">{{hint}}</mat-hint>\n <div matSuffix class=\"suffix-icons\">\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"true\" [(value)]=\"value\">\n </spa-suffix>\n </div>\n</mat-form-field>\n", dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3$1.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i7$1.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }] }); }
13358
13386
  }
13359
13387
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SelectMultiComponent, decorators: [{
13360
13388
  type: Component,
13361
13389
  args: [{ selector: 'spa-select-multi', standalone: false, template: "<mat-form-field [ngStyle]=\"{'width':width ?? '100%'}\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <mat-select [formControl]=\"control\" [required]=\"required\" multiple [placeholder]=\"placeholder\" [compareWith]=\"compareWith\" (selectionChange)=\"selectionChange($event)\">\n <mat-option *ngIf=\"nullable\" [value]=\"null\">None</mat-option>\n <mat-option *ngFor=\"let option of options\" [value]=\"option[optionValue]\">\n {{option[optionDisplay]}}\n </mat-option>\n </mat-select>\n <mat-hint *ngIf=\"hint\">{{hint}}</mat-hint>\n <div matSuffix class=\"suffix-icons\">\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"true\" [(value)]=\"value\">\n </spa-suffix>\n </div>\n</mat-form-field>\n" }]
13362
- }], ctorParameters: () => [{ type: MessageService }, { type: DataServiceLib }], propDecorators: { display: [{
13390
+ }], ctorParameters: () => [{ type: MessageService }, { type: DataServiceLib }, { type: ApiErrorService }], propDecorators: { display: [{
13363
13391
  type: Input
13364
13392
  }], value: [{
13365
13393
  type: Input
@@ -13767,6 +13795,16 @@ class TilesComponent {
13767
13795
  // TS-5: cache chart data/options/plugins so template bindings return STABLE references; without this each
13768
13796
  // change-detection cycle produced new objects, forcing BaseChartDirective to call chart.update() continuously.
13769
13797
  this.chartCache = new WeakMap();
13798
+ this.isAuto = true; // Changed: 'auto' is the DEFAULT again — the original stretch/shrink layout every existing page relies on
13799
+ this.isCarousel = false;
13800
+ this.tilesStyle = {};
13801
+ this.canScrollLeft = false;
13802
+ this.canScrollRight = false;
13803
+ // Added: one CACHED outlet context per tile. Building `{ $implicit: tile }` in the template would hand
13804
+ // NgTemplateOutlet a fresh object on every change-detection pass, re-creating every tile's view — the
13805
+ // documented cause of a real livelock in this library (and of editors being destroyed mid-typing).
13806
+ this.ctxCache = new WeakMap();
13807
+ this.tileBasis = ''; // Added: carousel-only fixed width, bound per card
13770
13808
  this.tileActionSelected = new EventEmitter();
13771
13809
  this.tileClick = new EventEmitter();
13772
13810
  this.tileUnClick = new EventEmitter();
@@ -13782,7 +13820,83 @@ class TilesComponent {
13782
13820
  get effQuietLoading() {
13783
13821
  return resolveQuietLoading(this.config, this.dataService.appConfig?.quietLoading);
13784
13822
  }
13823
+ // ---------------------------------------------------------------------------
13824
+ // Added: tile row layout. These are FIELDS, not template getters, on purpose — a getter that builds a new
13825
+ // object every change-detection pass is what caused a real livelock in this library before (see the
13826
+ // no-template-bound-allocating-getters rule). They are recomputed only when the config actually changes.
13827
+ // ---------------------------------------------------------------------------
13828
+ static { this.DEFAULT_MIN_TILE_WIDTH = 200; } // px — below this a tile's label starts wrapping to three lines
13829
+ static { this.TILE_GAP = 10; } // px — must match the CSS gap or maxPerRow arithmetic drifts
13830
+ ctx(tile) {
13831
+ let c = this.ctxCache.get(tile);
13832
+ if (!c) {
13833
+ c = { $implicit: tile };
13834
+ this.ctxCache.set(tile, c);
13835
+ }
13836
+ return c;
13837
+ }
13838
+ // Added: derives the grid track / carousel basis from minTileWidth + maxPerRow.
13839
+ // wrap -> repeat(auto-fit, minmax(<floor>, 1fr)). auto-fit collapses unused tracks, so a few tiles still
13840
+ // spread across the full width, while many tiles wrap onto the next row all at the SAME width.
13841
+ // maxPerRow raises the floor to 1/N of the row, which caps how many can fit.
13842
+ // carousel -> a fixed flex-basis; fixed width is precisely what makes it scroll instead of squeeze.
13843
+ applyLayout() {
13844
+ const cfg = this.config;
13845
+ const min = cfg?.minTileWidth ?? TilesComponent.DEFAULT_MIN_TILE_WIDTH;
13846
+ // Changed: absent or 'auto' => the ORIGINAL layout. Restored as the default on the owner's instruction
13847
+ // 2026-08-08: making the grid the default regressed every existing page on a phone, where a 200px track
13848
+ // floor does not fit twice in a ~380px viewport and every tile dropped onto its own row.
13849
+ this.isCarousel = cfg?.layout === 'carousel';
13850
+ this.isAuto = !this.isCarousel && cfg?.layout !== 'wrap';
13851
+ if (this.isAuto) {
13852
+ this.tilesStyle = {};
13853
+ this.tileBasis = '';
13854
+ return;
13855
+ }
13856
+ if (this.isCarousel) {
13857
+ this.tilesStyle = { 'grid-template-columns': '' };
13858
+ this.tileBasis = min + 'px';
13859
+ return;
13860
+ }
13861
+ const n = cfg?.maxPerRow;
13862
+ const floor = n && n > 0
13863
+ ? `max(${min}px, calc((100% - ${(n - 1) * TilesComponent.TILE_GAP}px) / ${n}))`
13864
+ : `${min}px`;
13865
+ this.tilesStyle = { 'grid-template-columns': `repeat(auto-fit, minmax(${floor}, 1fr))` };
13866
+ this.tileBasis = '';
13867
+ }
13868
+ ngAfterViewInit() {
13869
+ if (this.isCarousel)
13870
+ this.updateScrollState();
13871
+ }
13872
+ // Added: only offer an arrow when there is genuinely somewhere to scroll — an arrow that does nothing is
13873
+ // worse than no arrow. The 2px tolerance absorbs sub-pixel rounding at fractional zoom levels.
13874
+ updateScrollState() {
13875
+ const el = this.tileScroller?.nativeElement;
13876
+ if (!el)
13877
+ return;
13878
+ const left = el.scrollLeft > 2;
13879
+ const right = el.scrollLeft + el.clientWidth < el.scrollWidth - 2;
13880
+ if (left !== this.canScrollLeft || right !== this.canScrollRight) {
13881
+ this.canScrollLeft = left;
13882
+ this.canScrollRight = right;
13883
+ this.cdr.markForCheck(); // OnPush — the arrows would otherwise never appear or never leave
13884
+ }
13885
+ }
13886
+ onTilesScroll() {
13887
+ if (this.isCarousel)
13888
+ this.updateScrollState();
13889
+ }
13890
+ // Added: page by ~80% of the visible width rather than a fixed tile count, so it behaves the same on a
13891
+ // phone showing one tile and a monitor showing six, and always leaves a sliver of context on screen.
13892
+ scrollTiles(direction) {
13893
+ const el = this.tileScroller?.nativeElement;
13894
+ if (!el)
13895
+ return;
13896
+ el.scrollBy({ left: direction * Math.max(el.clientWidth * 0.8, 160), behavior: 'smooth' });
13897
+ }
13785
13898
  ngOnInit() {
13899
+ this.applyLayout(); // Added: resolve the layout before the first render
13786
13900
  if (this.config) {
13787
13901
  this.tiles = this.config.tiles;
13788
13902
  if (this.config?.loadAction && this.config.loadInit) {
@@ -13808,6 +13922,9 @@ class TilesComponent {
13808
13922
  // happening as soon as the host stops churning its bindings.
13809
13923
  if (this.config && this.config.tiles !== this.tiles)
13810
13924
  this.tiles = this.config.tiles;
13925
+ this.applyLayout(); // Added: a host that swaps its TileConfig may also be changing layout/minTileWidth/maxPerRow
13926
+ if (this.isCarousel)
13927
+ setTimeout(() => this.updateScrollState()); // after the new tiles have laid out
13811
13928
  }
13812
13929
  ngOnDestroy() {
13813
13930
  this.subs.forEach(s => s.unsubscribe()); // TS-8
@@ -14140,12 +14257,15 @@ class TilesComponent {
14140
14257
  return [];
14141
14258
  }
14142
14259
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TilesComponent, deps: [{ token: DataServiceLib }, { token: MessageService }, { token: i0.ChangeDetectorRef }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
14143
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TilesComponent, isStandalone: false, selector: "spa-tiles", inputs: { config: "config", lastSearch: "lastSearch", data: "data", reload: "reload" }, outputs: { tileActionSelected: "tileActionSelected", tileClick: "tileClick", tileUnClick: "tileUnClick" }, usesOnChanges: true, ngImport: i0, template: "<!-- Changed: align-items-center vertically centers tiles of different heights for clean alignment -->\n<div class=\"d-flex row align-items-center justify-content-between\">\n <ng-container *ngFor=\"let tile of tiles\">\n <!-- Changed: tile-clickable class gives pointer + hover lift only on tiles that actually respond to clicks -->\n <mat-card *ngIf=\"!isHidden(tile)\" class=\"col\" [class.tile-clickable]=\"isClickable(tile)\" [class.selected-tile]=\"tile.name === selectedTile\" style=\"margin-left: 5px;margin-right: 5px; padding: 10px 16px ; min-width: 150px; margin-top: 5px;\" (click)=\"clicked(tile)\">\n\n <!-- Changed: Chart-style tile \u2014 header, prominent chart, optional value, footer -->\n <ng-container *ngIf=\"tile.chart; else standardTile\">\n <!-- Changed: Header with tile name \u2014 left-aligned for better hierarchy -->\n <div class=\"tile-chart-header\">\n <span>{{tile.alias ?? tile.name | camelToWords}}</span>\n </div>\n\n <!-- Changed: Chart fills tile \u2014 uses helper method for reliable data detection -->\n <div class=\"tile-chart\" *ngIf=\"hasTileChartData(tile)\" [style.height.px]=\"tile.chart.height ?? 120\">\n <canvas baseChart\n [type]=\"tile.chart.type\"\n [data]=\"getTileMiniChartData(tile)\"\n [options]=\"getMiniChartOptions(tile)\"\n [plugins]=\"getTileChartPlugins(tile)\">\n </canvas>\n </div>\n\n <!-- Changed: Optional value display below chart \u2014 only show primitive values, skip chart data objects -->\n <div class=\"tile-chart-value\" *ngIf=\"tile.name && isTileValuePrimitive(tile)\">\n <span class=\"tile-chart-value-text\" [style.color]=\"tile.color\">{{tile.prefix ?? ''}}{{data?.[tile.name]}}<span *ngIf=\"tile.suffix\"> {{tile.suffix}}</span></span>\n </div>\n\n <!-- Changed: Footer with divider for chart tiles -->\n <div class=\"tile-footer\" *ngIf=\"tile.footer || tile.info\">\n <mat-divider></mat-divider>\n <div class=\"d-flex align-items-center\" style=\"gap: 4px; color: #9a9a9a; font-size: 12px; margin-top: 6px;\">\n <mat-icon *ngIf=\"tile.footerIcon\" style=\"font-size: 16px; width: 16px; height: 16px;\">{{tile.footerIcon}}</mat-icon>\n <mat-icon *ngIf=\"!tile.footerIcon && tile.info\" style=\"font-size: 16px; width: 16px; height: 16px; color: steelblue;\">info</mat-icon>\n <span>{{tile.footer ?? tile.info}}</span>\n </div>\n </div>\n </ng-container>\n\n <!-- Changed: Standard tile \u2014 Paper Dashboard style: icon left, label+value right, optional footer -->\n <ng-template #standardTile>\n <!-- Changed: Icon-style tile \u2014 icon left, label top-right, large value below -->\n <ng-container *ngIf=\"tile.icon; else basicTile\">\n <div class=\"tile-icon-row\">\n <div class=\"tile-icon-wrap\" [style.color]=\"tile.color ?? '#2196f3'\">\n <mat-icon>{{tile.icon}}</mat-icon>\n </div>\n <div class=\"tile-icon-content\">\n <div class=\"tile-icon-label\">{{tile.alias ?? tile.name | camelToWords}}</div>\n <!-- Changed (Quiet Loading D5): while the value is genuinely unknown the slot shows a shimmer chip instead of the old lying `0`; icon, label and footer render normally so the card never resizes. On a reload the previous number stays put and simply updates. -->\n <div class=\"tile-icon-value\" [style.color]=\"tile.color\" [attr.aria-busy]=\"showValueGhost(tile) ? 'true' : null\">\n <span *ngIf=\"tile.prefix\">{{tile.prefix}}</span><span *ngIf=\"showValueGhost(tile); else iconTileValue\" class=\"tin-skel tin-skel-text tile-value-ghost\" aria-hidden=\"true\"></span><ng-template #iconTileValue><span [class.tile-value-in]=\"effQuietLoading\">{{data?.[tile.name] ?? 0}}</span></ng-template><span *ngIf=\"tile.suffix\"> {{tile.suffix}}</span>\n <span *ngIf=\"tile.badge && data?.[tile.badge]\" class=\"tile-badge\" [style.backgroundColor]=\"tile.badgeColor ?? '#4caf50'\">{{data?.[tile.badge]}}</span>\n </div>\n </div>\n </div>\n <!-- Changed: Footer with divider \u2014 info tooltip or custom footer text -->\n <div class=\"tile-icon-footer\" *ngIf=\"tile.info || tile.footer\">\n <mat-divider></mat-divider>\n <div class=\"tile-icon-footer-content\">\n <mat-icon *ngIf=\"tile.footerIcon\" class=\"tile-icon-footer-icon\">{{tile.footerIcon}}</mat-icon>\n <mat-icon *ngIf=\"!tile.footerIcon && tile.info\" class=\"tile-icon-footer-icon\" style=\"color: steelblue;\">info</mat-icon>\n <span>{{tile.footer ?? tile.info}}</span>\n </div>\n </div>\n </ng-container>\n\n <!-- Basic tile fallback \u2014 centered number display (no icon) -->\n <ng-template #basicTile>\n <div class=\"row d-flex justify-content-center align-items-center\">\n <div style=\"text-align: center;font-size: 30px;\">\n <mat-label style=\"font-weight:bold;\" *ngIf=\"tile.prefix\" >{{tile.prefix}}</mat-label> &nbsp;\n <!-- Changed (Quiet Loading D5): same value ghost as the icon tile \u2014 chip while unknown, real number otherwise -->\n <mat-label style=\"font-weight:bold; text-align: center;\" [ngStyle]=\"{'color':tile.color }\" [attr.aria-busy]=\"showValueGhost(tile) ? 'true' : null\"><span *ngIf=\"showValueGhost(tile); else basicTileValue\" class=\"tin-skel tin-skel-text tile-value-ghost\" aria-hidden=\"true\"></span><ng-template #basicTileValue><span [class.tile-value-in]=\"effQuietLoading\">{{data?.[tile.name] ?? 0}}</span></ng-template></mat-label>&nbsp;\n <mat-label style=\"font-weight:bold;\" *ngIf=\"tile.suffix\">{{tile.suffix}}</mat-label>\n <span *ngIf=\"tile.badge && data?.[tile.badge]\" class=\"tile-badge\" [style.backgroundColor]=\"tile.badgeColor ?? '#4caf50'\">{{data?.[tile.badge]}}</span>\n </div>\n </div>\n <div class=\"row d-flex justify-content-center align-items-center\">\n <div class=\"d-flex justify-content-center align-items-center\" style=\"text-align: center;\">\n <mat-label style=\"padding-left:5px;padding-right:5px; text-align: center;font-size: 14px;\">{{tile.alias ?? tile.name | camelToWords}}</mat-label>\n <mat-icon *ngIf=\"tile.info\" [matTooltip]=\"tile.info\" matTooltipPosition=\"above\" style=\"font-size: 20px; color:steelblue;\">info</mat-icon>\n </div>\n </div>\n </ng-template>\n </ng-template>\n\n </mat-card>\n </ng-container>\n</div>\n", styles: [".card{min-width:180px;flex:1;display:flex;flex-direction:column;align-items:center;padding:5px 10px}.tiles{gap:1;row-gap:5px}.col{transition:all .2s ease}.tile-clickable{cursor:pointer}.tile-clickable:hover{transform:translateY(-2px);box-shadow:0 4px 10px #00000021;background-color:#2196f312}.selected-tile{background-color:#e0e0e0;box-shadow:0 4px 8px #0003;transform:translateY(-2px);border:2px solid #3f51b5}.selected-tile mat-label{font-weight:700}.selected-tile:hover{background-color:#e0e0e0}.tile-chart-header{display:flex;justify-content:flex-start;font-size:11px;font-weight:500;color:#999;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}.tile-chart{width:100%;position:relative;margin-top:4px;display:flex;align-items:center;justify-content:center}.tile-chart canvas{width:100%!important;height:100%!important;max-height:inherit}.tile-chart-value{text-align:center;margin-top:6px}.tile-chart-value-text{font-size:22px;font-weight:600;letter-spacing:-.5px}.tile-badge{display:inline-block;font-size:12px;font-weight:500;color:#fff;padding:2px 8px;border-radius:12px;vertical-align:middle;margin-left:4px}.tile-footer{margin-top:8px}.tile-icon-row{display:flex;align-items:flex-start;gap:12px;padding:4px 0}.tile-icon-wrap{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:48px;height:48px}.tile-icon-wrap mat-icon{font-size:36px;width:36px;height:36px;opacity:.85}.tile-icon-content{flex:1;text-align:right;min-width:0}.tile-icon-label{font-size:12px;color:#999;text-transform:uppercase;letter-spacing:.3px;line-height:1.4}.tile-icon-value{font-size:26px;font-weight:600;line-height:1.2;letter-spacing:-.5px}.tile-icon-footer{margin-top:8px}.tile-icon-footer mat-divider{margin-bottom:6px}.tile-icon-footer-content{display:flex;align-items:center;gap:4px;font-size:12px;color:#999}.tile-icon-footer-icon{font-size:16px;width:16px;height:16px;color:#bbb}.tile-value-ghost{width:2.5ch;height:.72em;vertical-align:middle}.tile-value-in{animation:tile-value-in .15s ease-out both}@keyframes tile-value-in{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.tile-value-in{animation:none}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: i17.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: i10.BaseChartDirective, selector: "canvas[baseChart]", inputs: ["type", "legend", "data", "options", "plugins", "labels", "datasets"], outputs: ["chartClick", "chartHover"], exportAs: ["base-chart"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
14260
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TilesComponent, isStandalone: false, selector: "spa-tiles", inputs: { config: "config", lastSearch: "lastSearch", data: "data", reload: "reload" }, outputs: { tileActionSelected: "tileActionSelected", tileClick: "tileClick", tileUnClick: "tileUnClick" }, viewQueries: [{ propertyName: "tileScroller", first: true, predicate: ["tileScroller"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!-- Tile row layout. THREE modes, and the DEFAULT is the original.\n 'auto' (default, and what every existing page gets): the original bootstrap .row/.col \u2014 tiles stretch to\n fill and shrink to a 150px floor, wrapping when they run out of room. This is byte-for-byte the markup\n that shipped before, restored on the owner's instruction 2026-08-08: the grid I briefly made the default\n forced ONE tile per row on a phone, because a 200px track floor does not fit twice in a ~380px viewport.\n 'wrap': a grid of equal tracks \u2014 opt in where uniform tile widths matter more than filling the row.\n 'carousel': one row, horizontal scroll. The Day Book opts into this.\n The tile body itself is ONE template shared by all three, so the modes can never drift apart. -->\n\n<!-- AUTO \u2014 the original layout, unchanged -->\n<div *ngIf=\"isAuto\" class=\"d-flex row align-items-center justify-content-between\">\n <ng-container *ngFor=\"let tile of tiles\">\n <!-- Changed: tile-clickable class gives pointer + hover lift only on tiles that actually respond to clicks -->\n <mat-card *ngIf=\"!isHidden(tile)\" class=\"col\" [class.tile-clickable]=\"isClickable(tile)\" [class.selected-tile]=\"tile.name === selectedTile\" style=\"margin-left: 5px;margin-right: 5px; padding: 10px 16px ; min-width: 150px; margin-top: 5px;\" (click)=\"clicked(tile)\">\n <ng-container *ngTemplateOutlet=\"tileBody; context: ctx(tile)\"></ng-container>\n </mat-card>\n </ng-container>\n</div>\n\n<!-- WRAP / CAROUSEL \u2014 opt-in only -->\n<div *ngIf=\"!isAuto\" class=\"spa-tiles-outer\">\n\n <!-- Carousel only: scroll affordances, shown solely when there is something to scroll to -->\n <button type=\"button\" class=\"tiles-nav\" *ngIf=\"isCarousel && canScrollLeft\" (click)=\"scrollTiles(-1)\" aria-label=\"Scroll tiles left\">\n <mat-icon>chevron_left</mat-icon>\n </button>\n\n <div #tileScroller class=\"spa-tiles\" [class.tiles-wrap]=\"!isCarousel\" [class.tiles-carousel]=\"isCarousel\" [ngStyle]=\"tilesStyle\" (scroll)=\"onTilesScroll()\">\n <ng-container *ngFor=\"let tile of tiles\">\n <mat-card *ngIf=\"!isHidden(tile)\" class=\"tile-card\" [class.tile-clickable]=\"isClickable(tile)\" [class.selected-tile]=\"tile.name === selectedTile\" [style.flex]=\"tileBasis ? '0 0 ' + tileBasis : null\" (click)=\"clicked(tile)\">\n <ng-container *ngTemplateOutlet=\"tileBody; context: ctx(tile)\"></ng-container>\n </mat-card>\n </ng-container>\n </div>\n\n <button type=\"button\" class=\"tiles-nav\" *ngIf=\"isCarousel && canScrollRight\" (click)=\"scrollTiles(1)\" aria-label=\"Scroll tiles right\">\n <mat-icon>chevron_right</mat-icon>\n </button>\n\n</div>\n\n<!-- The tile body, shared by all three modes. ctx() returns a CACHED context object per tile \u2014 building\n `{ $implicit: tile }` inline would hand NgTemplateOutlet a new object every change-detection pass and\n re-create every tile's view, which is the documented cause of a real livelock in this library. -->\n<ng-template #tileBody let-tile>\n\n <!-- Changed: Chart-style tile \u2014 header, prominent chart, optional value, footer -->\n <ng-container *ngIf=\"tile.chart; else standardTile\">\n <!-- Changed: Header with tile name \u2014 left-aligned for better hierarchy -->\n <div class=\"tile-chart-header\">\n <span>{{tile.alias ?? tile.name | camelToWords}}</span>\n </div>\n\n <!-- Changed: Chart fills tile \u2014 uses helper method for reliable data detection -->\n <div class=\"tile-chart\" *ngIf=\"hasTileChartData(tile)\" [style.height.px]=\"tile.chart.height ?? 120\">\n <canvas baseChart\n [type]=\"tile.chart.type\"\n [data]=\"getTileMiniChartData(tile)\"\n [options]=\"getMiniChartOptions(tile)\"\n [plugins]=\"getTileChartPlugins(tile)\">\n </canvas>\n </div>\n\n <!-- Changed: Optional value display below chart \u2014 only show primitive values, skip chart data objects -->\n <div class=\"tile-chart-value\" *ngIf=\"tile.name && isTileValuePrimitive(tile)\">\n <span class=\"tile-chart-value-text\" [style.color]=\"tile.color\">{{tile.prefix ?? ''}}{{data?.[tile.name]}}<span *ngIf=\"tile.suffix\"> {{tile.suffix}}</span></span>\n </div>\n\n <!-- Changed: Footer with divider for chart tiles -->\n <div class=\"tile-footer\" *ngIf=\"tile.footer || tile.info\">\n <mat-divider></mat-divider>\n <div class=\"d-flex align-items-center\" style=\"gap: 4px; color: #9a9a9a; font-size: 12px; margin-top: 6px;\">\n <mat-icon *ngIf=\"tile.footerIcon\" style=\"font-size: 16px; width: 16px; height: 16px;\">{{tile.footerIcon}}</mat-icon>\n <mat-icon *ngIf=\"!tile.footerIcon && tile.info\" style=\"font-size: 16px; width: 16px; height: 16px; color: steelblue;\">info</mat-icon>\n <span>{{tile.footer ?? tile.info}}</span>\n </div>\n </div>\n </ng-container>\n\n <!-- Changed: Standard tile \u2014 Paper Dashboard style: icon left, label+value right, optional footer -->\n <ng-template #standardTile>\n <!-- Changed: Icon-style tile \u2014 icon left, label top-right, large value below -->\n <ng-container *ngIf=\"tile.icon; else basicTile\">\n <div class=\"tile-icon-row\">\n <div class=\"tile-icon-wrap\" [style.color]=\"tile.color ?? '#2196f3'\">\n <mat-icon>{{tile.icon}}</mat-icon>\n </div>\n <div class=\"tile-icon-content\">\n <div class=\"tile-icon-label\">{{tile.alias ?? tile.name | camelToWords}}</div>\n <!-- Changed (Quiet Loading D5): while the value is genuinely unknown the slot shows a shimmer chip instead of the old lying `0`; icon, label and footer render normally so the card never resizes. On a reload the previous number stays put and simply updates. -->\n <div class=\"tile-icon-value\" [style.color]=\"tile.color\" [attr.aria-busy]=\"showValueGhost(tile) ? 'true' : null\">\n <span *ngIf=\"tile.prefix\">{{tile.prefix}}</span><span *ngIf=\"showValueGhost(tile); else iconTileValue\" class=\"tin-skel tin-skel-text tile-value-ghost\" aria-hidden=\"true\"></span><ng-template #iconTileValue><span [class.tile-value-in]=\"effQuietLoading\">{{data?.[tile.name] ?? 0}}</span></ng-template><span *ngIf=\"tile.suffix\"> {{tile.suffix}}</span>\n <span *ngIf=\"tile.badge && data?.[tile.badge]\" class=\"tile-badge\" [style.backgroundColor]=\"tile.badgeColor ?? '#4caf50'\">{{data?.[tile.badge]}}</span>\n </div>\n </div>\n </div>\n <!-- Changed: Footer with divider \u2014 info tooltip or custom footer text -->\n <div class=\"tile-icon-footer\" *ngIf=\"tile.info || tile.footer\">\n <mat-divider></mat-divider>\n <div class=\"tile-icon-footer-content\">\n <mat-icon *ngIf=\"tile.footerIcon\" class=\"tile-icon-footer-icon\">{{tile.footerIcon}}</mat-icon>\n <mat-icon *ngIf=\"!tile.footerIcon && tile.info\" class=\"tile-icon-footer-icon\" style=\"color: steelblue;\">info</mat-icon>\n <span>{{tile.footer ?? tile.info}}</span>\n </div>\n </div>\n </ng-container>\n\n <!-- Basic tile fallback \u2014 centered number display (no icon) -->\n <ng-template #basicTile>\n <div class=\"row d-flex justify-content-center align-items-center\">\n <div style=\"text-align: center;font-size: 30px;\">\n <mat-label style=\"font-weight:bold;\" *ngIf=\"tile.prefix\" >{{tile.prefix}}</mat-label> &nbsp;\n <!-- Changed (Quiet Loading D5): same value ghost as the icon tile \u2014 chip while unknown, real number otherwise -->\n <mat-label style=\"font-weight:bold; text-align: center;\" [ngStyle]=\"{'color':tile.color }\" [attr.aria-busy]=\"showValueGhost(tile) ? 'true' : null\"><span *ngIf=\"showValueGhost(tile); else basicTileValue\" class=\"tin-skel tin-skel-text tile-value-ghost\" aria-hidden=\"true\"></span><ng-template #basicTileValue><span [class.tile-value-in]=\"effQuietLoading\">{{data?.[tile.name] ?? 0}}</span></ng-template></mat-label>&nbsp;\n <mat-label style=\"font-weight:bold;\" *ngIf=\"tile.suffix\">{{tile.suffix}}</mat-label>\n <span *ngIf=\"tile.badge && data?.[tile.badge]\" class=\"tile-badge\" [style.backgroundColor]=\"tile.badgeColor ?? '#4caf50'\">{{data?.[tile.badge]}}</span>\n </div>\n </div>\n <div class=\"row d-flex justify-content-center align-items-center\">\n <div class=\"d-flex justify-content-center align-items-center\" style=\"text-align: center;\">\n <mat-label style=\"padding-left:5px;padding-right:5px; text-align: center;font-size: 14px;\">{{tile.alias ?? tile.name | camelToWords}}</mat-label>\n <mat-icon *ngIf=\"tile.info\" [matTooltip]=\"tile.info\" matTooltipPosition=\"above\" style=\"font-size: 20px; color:steelblue;\">info</mat-icon>\n </div>\n </div>\n </ng-template>\n </ng-template>\n\n</ng-template>\n", styles: [".card{min-width:180px;flex:1;display:flex;flex-direction:column;align-items:center;padding:5px 10px}.tiles{gap:1;row-gap:5px}.spa-tiles-outer{display:flex;align-items:center;gap:4px}.spa-tiles{flex:1;min-width:0}.tiles-wrap{display:grid;gap:10px;align-items:stretch}.tiles-carousel{display:flex;gap:10px;overflow-x:auto;scroll-behavior:smooth;scroll-snap-type:x proximity;padding-bottom:6px}.tiles-carousel>.tile-card{scroll-snap-align:start}.tile-card{padding:10px 16px;margin:0}.tiles-nav{flex:0 0 auto;display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:50%;background:#0000000a;color:#666;cursor:pointer;transition:background-color .2s ease,color .2s ease}.tiles-nav:hover{background:#2196f31f;color:#2196f3}.tiles-nav mat-icon{font-size:20px;width:20px;height:20px}.col{transition:all .2s ease}.tile-clickable{cursor:pointer}.tile-clickable:hover{transform:translateY(-2px);box-shadow:0 4px 10px #00000021;background-color:#2196f312}.selected-tile{background-color:#e0e0e0;box-shadow:0 4px 8px #0003;transform:translateY(-2px);border:2px solid #3f51b5}.selected-tile mat-label{font-weight:700}.selected-tile:hover{background-color:#e0e0e0}.tile-chart-header{display:flex;justify-content:flex-start;font-size:11px;font-weight:500;color:#999;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}.tile-chart{width:100%;position:relative;margin-top:4px;display:flex;align-items:center;justify-content:center}.tile-chart canvas{width:100%!important;height:100%!important;max-height:inherit}.tile-chart-value{text-align:center;margin-top:6px}.tile-chart-value-text{font-size:22px;font-weight:600;letter-spacing:-.5px}.tile-badge{display:inline-block;font-size:12px;font-weight:500;color:#fff;padding:2px 8px;border-radius:12px;vertical-align:middle;margin-left:4px}.tile-footer{margin-top:8px}.tile-icon-row{display:flex;align-items:flex-start;gap:12px;padding:4px 0}.tile-icon-wrap{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:48px;height:48px}.tile-icon-wrap mat-icon{font-size:36px;width:36px;height:36px;opacity:.85}.tile-icon-content{flex:1;text-align:right;min-width:0}.tile-icon-label{font-size:12px;color:#999;text-transform:uppercase;letter-spacing:.3px;line-height:1.4}.tile-icon-value{font-size:26px;font-weight:600;line-height:1.2;letter-spacing:-.5px}.tile-icon-footer{margin-top:8px}.tile-icon-footer mat-divider{margin-bottom:6px}.tile-icon-footer-content{display:flex;align-items:center;gap:4px;font-size:12px;color:#999}.tile-icon-footer-icon{font-size:16px;width:16px;height:16px;color:#bbb}.tile-value-ghost{width:2.5ch;height:.72em;vertical-align:middle}.tile-value-in{animation:tile-value-in .15s ease-out both}@keyframes tile-value-in{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.tile-value-in{animation:none}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: i17.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: i10.BaseChartDirective, selector: "canvas[baseChart]", inputs: ["type", "legend", "data", "options", "plugins", "labels", "datasets"], outputs: ["chartClick", "chartHover"], exportAs: ["base-chart"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
14144
14261
  }
14145
14262
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TilesComponent, decorators: [{
14146
14263
  type: Component,
14147
- args: [{ selector: 'spa-tiles', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<!-- Changed: align-items-center vertically centers tiles of different heights for clean alignment -->\n<div class=\"d-flex row align-items-center justify-content-between\">\n <ng-container *ngFor=\"let tile of tiles\">\n <!-- Changed: tile-clickable class gives pointer + hover lift only on tiles that actually respond to clicks -->\n <mat-card *ngIf=\"!isHidden(tile)\" class=\"col\" [class.tile-clickable]=\"isClickable(tile)\" [class.selected-tile]=\"tile.name === selectedTile\" style=\"margin-left: 5px;margin-right: 5px; padding: 10px 16px ; min-width: 150px; margin-top: 5px;\" (click)=\"clicked(tile)\">\n\n <!-- Changed: Chart-style tile \u2014 header, prominent chart, optional value, footer -->\n <ng-container *ngIf=\"tile.chart; else standardTile\">\n <!-- Changed: Header with tile name \u2014 left-aligned for better hierarchy -->\n <div class=\"tile-chart-header\">\n <span>{{tile.alias ?? tile.name | camelToWords}}</span>\n </div>\n\n <!-- Changed: Chart fills tile \u2014 uses helper method for reliable data detection -->\n <div class=\"tile-chart\" *ngIf=\"hasTileChartData(tile)\" [style.height.px]=\"tile.chart.height ?? 120\">\n <canvas baseChart\n [type]=\"tile.chart.type\"\n [data]=\"getTileMiniChartData(tile)\"\n [options]=\"getMiniChartOptions(tile)\"\n [plugins]=\"getTileChartPlugins(tile)\">\n </canvas>\n </div>\n\n <!-- Changed: Optional value display below chart \u2014 only show primitive values, skip chart data objects -->\n <div class=\"tile-chart-value\" *ngIf=\"tile.name && isTileValuePrimitive(tile)\">\n <span class=\"tile-chart-value-text\" [style.color]=\"tile.color\">{{tile.prefix ?? ''}}{{data?.[tile.name]}}<span *ngIf=\"tile.suffix\"> {{tile.suffix}}</span></span>\n </div>\n\n <!-- Changed: Footer with divider for chart tiles -->\n <div class=\"tile-footer\" *ngIf=\"tile.footer || tile.info\">\n <mat-divider></mat-divider>\n <div class=\"d-flex align-items-center\" style=\"gap: 4px; color: #9a9a9a; font-size: 12px; margin-top: 6px;\">\n <mat-icon *ngIf=\"tile.footerIcon\" style=\"font-size: 16px; width: 16px; height: 16px;\">{{tile.footerIcon}}</mat-icon>\n <mat-icon *ngIf=\"!tile.footerIcon && tile.info\" style=\"font-size: 16px; width: 16px; height: 16px; color: steelblue;\">info</mat-icon>\n <span>{{tile.footer ?? tile.info}}</span>\n </div>\n </div>\n </ng-container>\n\n <!-- Changed: Standard tile \u2014 Paper Dashboard style: icon left, label+value right, optional footer -->\n <ng-template #standardTile>\n <!-- Changed: Icon-style tile \u2014 icon left, label top-right, large value below -->\n <ng-container *ngIf=\"tile.icon; else basicTile\">\n <div class=\"tile-icon-row\">\n <div class=\"tile-icon-wrap\" [style.color]=\"tile.color ?? '#2196f3'\">\n <mat-icon>{{tile.icon}}</mat-icon>\n </div>\n <div class=\"tile-icon-content\">\n <div class=\"tile-icon-label\">{{tile.alias ?? tile.name | camelToWords}}</div>\n <!-- Changed (Quiet Loading D5): while the value is genuinely unknown the slot shows a shimmer chip instead of the old lying `0`; icon, label and footer render normally so the card never resizes. On a reload the previous number stays put and simply updates. -->\n <div class=\"tile-icon-value\" [style.color]=\"tile.color\" [attr.aria-busy]=\"showValueGhost(tile) ? 'true' : null\">\n <span *ngIf=\"tile.prefix\">{{tile.prefix}}</span><span *ngIf=\"showValueGhost(tile); else iconTileValue\" class=\"tin-skel tin-skel-text tile-value-ghost\" aria-hidden=\"true\"></span><ng-template #iconTileValue><span [class.tile-value-in]=\"effQuietLoading\">{{data?.[tile.name] ?? 0}}</span></ng-template><span *ngIf=\"tile.suffix\"> {{tile.suffix}}</span>\n <span *ngIf=\"tile.badge && data?.[tile.badge]\" class=\"tile-badge\" [style.backgroundColor]=\"tile.badgeColor ?? '#4caf50'\">{{data?.[tile.badge]}}</span>\n </div>\n </div>\n </div>\n <!-- Changed: Footer with divider \u2014 info tooltip or custom footer text -->\n <div class=\"tile-icon-footer\" *ngIf=\"tile.info || tile.footer\">\n <mat-divider></mat-divider>\n <div class=\"tile-icon-footer-content\">\n <mat-icon *ngIf=\"tile.footerIcon\" class=\"tile-icon-footer-icon\">{{tile.footerIcon}}</mat-icon>\n <mat-icon *ngIf=\"!tile.footerIcon && tile.info\" class=\"tile-icon-footer-icon\" style=\"color: steelblue;\">info</mat-icon>\n <span>{{tile.footer ?? tile.info}}</span>\n </div>\n </div>\n </ng-container>\n\n <!-- Basic tile fallback \u2014 centered number display (no icon) -->\n <ng-template #basicTile>\n <div class=\"row d-flex justify-content-center align-items-center\">\n <div style=\"text-align: center;font-size: 30px;\">\n <mat-label style=\"font-weight:bold;\" *ngIf=\"tile.prefix\" >{{tile.prefix}}</mat-label> &nbsp;\n <!-- Changed (Quiet Loading D5): same value ghost as the icon tile \u2014 chip while unknown, real number otherwise -->\n <mat-label style=\"font-weight:bold; text-align: center;\" [ngStyle]=\"{'color':tile.color }\" [attr.aria-busy]=\"showValueGhost(tile) ? 'true' : null\"><span *ngIf=\"showValueGhost(tile); else basicTileValue\" class=\"tin-skel tin-skel-text tile-value-ghost\" aria-hidden=\"true\"></span><ng-template #basicTileValue><span [class.tile-value-in]=\"effQuietLoading\">{{data?.[tile.name] ?? 0}}</span></ng-template></mat-label>&nbsp;\n <mat-label style=\"font-weight:bold;\" *ngIf=\"tile.suffix\">{{tile.suffix}}</mat-label>\n <span *ngIf=\"tile.badge && data?.[tile.badge]\" class=\"tile-badge\" [style.backgroundColor]=\"tile.badgeColor ?? '#4caf50'\">{{data?.[tile.badge]}}</span>\n </div>\n </div>\n <div class=\"row d-flex justify-content-center align-items-center\">\n <div class=\"d-flex justify-content-center align-items-center\" style=\"text-align: center;\">\n <mat-label style=\"padding-left:5px;padding-right:5px; text-align: center;font-size: 14px;\">{{tile.alias ?? tile.name | camelToWords}}</mat-label>\n <mat-icon *ngIf=\"tile.info\" [matTooltip]=\"tile.info\" matTooltipPosition=\"above\" style=\"font-size: 20px; color:steelblue;\">info</mat-icon>\n </div>\n </div>\n </ng-template>\n </ng-template>\n\n </mat-card>\n </ng-container>\n</div>\n", styles: [".card{min-width:180px;flex:1;display:flex;flex-direction:column;align-items:center;padding:5px 10px}.tiles{gap:1;row-gap:5px}.col{transition:all .2s ease}.tile-clickable{cursor:pointer}.tile-clickable:hover{transform:translateY(-2px);box-shadow:0 4px 10px #00000021;background-color:#2196f312}.selected-tile{background-color:#e0e0e0;box-shadow:0 4px 8px #0003;transform:translateY(-2px);border:2px solid #3f51b5}.selected-tile mat-label{font-weight:700}.selected-tile:hover{background-color:#e0e0e0}.tile-chart-header{display:flex;justify-content:flex-start;font-size:11px;font-weight:500;color:#999;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}.tile-chart{width:100%;position:relative;margin-top:4px;display:flex;align-items:center;justify-content:center}.tile-chart canvas{width:100%!important;height:100%!important;max-height:inherit}.tile-chart-value{text-align:center;margin-top:6px}.tile-chart-value-text{font-size:22px;font-weight:600;letter-spacing:-.5px}.tile-badge{display:inline-block;font-size:12px;font-weight:500;color:#fff;padding:2px 8px;border-radius:12px;vertical-align:middle;margin-left:4px}.tile-footer{margin-top:8px}.tile-icon-row{display:flex;align-items:flex-start;gap:12px;padding:4px 0}.tile-icon-wrap{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:48px;height:48px}.tile-icon-wrap mat-icon{font-size:36px;width:36px;height:36px;opacity:.85}.tile-icon-content{flex:1;text-align:right;min-width:0}.tile-icon-label{font-size:12px;color:#999;text-transform:uppercase;letter-spacing:.3px;line-height:1.4}.tile-icon-value{font-size:26px;font-weight:600;line-height:1.2;letter-spacing:-.5px}.tile-icon-footer{margin-top:8px}.tile-icon-footer mat-divider{margin-bottom:6px}.tile-icon-footer-content{display:flex;align-items:center;gap:4px;font-size:12px;color:#999}.tile-icon-footer-icon{font-size:16px;width:16px;height:16px;color:#bbb}.tile-value-ghost{width:2.5ch;height:.72em;vertical-align:middle}.tile-value-in{animation:tile-value-in .15s ease-out both}@keyframes tile-value-in{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.tile-value-in{animation:none}}\n"] }]
14148
- }], ctorParameters: () => [{ type: DataServiceLib }, { type: MessageService }, { type: i0.ChangeDetectorRef }, { type: ApiErrorService }], propDecorators: { config: [{
14264
+ args: [{ selector: 'spa-tiles', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<!-- Tile row layout. THREE modes, and the DEFAULT is the original.\n 'auto' (default, and what every existing page gets): the original bootstrap .row/.col \u2014 tiles stretch to\n fill and shrink to a 150px floor, wrapping when they run out of room. This is byte-for-byte the markup\n that shipped before, restored on the owner's instruction 2026-08-08: the grid I briefly made the default\n forced ONE tile per row on a phone, because a 200px track floor does not fit twice in a ~380px viewport.\n 'wrap': a grid of equal tracks \u2014 opt in where uniform tile widths matter more than filling the row.\n 'carousel': one row, horizontal scroll. The Day Book opts into this.\n The tile body itself is ONE template shared by all three, so the modes can never drift apart. -->\n\n<!-- AUTO \u2014 the original layout, unchanged -->\n<div *ngIf=\"isAuto\" class=\"d-flex row align-items-center justify-content-between\">\n <ng-container *ngFor=\"let tile of tiles\">\n <!-- Changed: tile-clickable class gives pointer + hover lift only on tiles that actually respond to clicks -->\n <mat-card *ngIf=\"!isHidden(tile)\" class=\"col\" [class.tile-clickable]=\"isClickable(tile)\" [class.selected-tile]=\"tile.name === selectedTile\" style=\"margin-left: 5px;margin-right: 5px; padding: 10px 16px ; min-width: 150px; margin-top: 5px;\" (click)=\"clicked(tile)\">\n <ng-container *ngTemplateOutlet=\"tileBody; context: ctx(tile)\"></ng-container>\n </mat-card>\n </ng-container>\n</div>\n\n<!-- WRAP / CAROUSEL \u2014 opt-in only -->\n<div *ngIf=\"!isAuto\" class=\"spa-tiles-outer\">\n\n <!-- Carousel only: scroll affordances, shown solely when there is something to scroll to -->\n <button type=\"button\" class=\"tiles-nav\" *ngIf=\"isCarousel && canScrollLeft\" (click)=\"scrollTiles(-1)\" aria-label=\"Scroll tiles left\">\n <mat-icon>chevron_left</mat-icon>\n </button>\n\n <div #tileScroller class=\"spa-tiles\" [class.tiles-wrap]=\"!isCarousel\" [class.tiles-carousel]=\"isCarousel\" [ngStyle]=\"tilesStyle\" (scroll)=\"onTilesScroll()\">\n <ng-container *ngFor=\"let tile of tiles\">\n <mat-card *ngIf=\"!isHidden(tile)\" class=\"tile-card\" [class.tile-clickable]=\"isClickable(tile)\" [class.selected-tile]=\"tile.name === selectedTile\" [style.flex]=\"tileBasis ? '0 0 ' + tileBasis : null\" (click)=\"clicked(tile)\">\n <ng-container *ngTemplateOutlet=\"tileBody; context: ctx(tile)\"></ng-container>\n </mat-card>\n </ng-container>\n </div>\n\n <button type=\"button\" class=\"tiles-nav\" *ngIf=\"isCarousel && canScrollRight\" (click)=\"scrollTiles(1)\" aria-label=\"Scroll tiles right\">\n <mat-icon>chevron_right</mat-icon>\n </button>\n\n</div>\n\n<!-- The tile body, shared by all three modes. ctx() returns a CACHED context object per tile \u2014 building\n `{ $implicit: tile }` inline would hand NgTemplateOutlet a new object every change-detection pass and\n re-create every tile's view, which is the documented cause of a real livelock in this library. -->\n<ng-template #tileBody let-tile>\n\n <!-- Changed: Chart-style tile \u2014 header, prominent chart, optional value, footer -->\n <ng-container *ngIf=\"tile.chart; else standardTile\">\n <!-- Changed: Header with tile name \u2014 left-aligned for better hierarchy -->\n <div class=\"tile-chart-header\">\n <span>{{tile.alias ?? tile.name | camelToWords}}</span>\n </div>\n\n <!-- Changed: Chart fills tile \u2014 uses helper method for reliable data detection -->\n <div class=\"tile-chart\" *ngIf=\"hasTileChartData(tile)\" [style.height.px]=\"tile.chart.height ?? 120\">\n <canvas baseChart\n [type]=\"tile.chart.type\"\n [data]=\"getTileMiniChartData(tile)\"\n [options]=\"getMiniChartOptions(tile)\"\n [plugins]=\"getTileChartPlugins(tile)\">\n </canvas>\n </div>\n\n <!-- Changed: Optional value display below chart \u2014 only show primitive values, skip chart data objects -->\n <div class=\"tile-chart-value\" *ngIf=\"tile.name && isTileValuePrimitive(tile)\">\n <span class=\"tile-chart-value-text\" [style.color]=\"tile.color\">{{tile.prefix ?? ''}}{{data?.[tile.name]}}<span *ngIf=\"tile.suffix\"> {{tile.suffix}}</span></span>\n </div>\n\n <!-- Changed: Footer with divider for chart tiles -->\n <div class=\"tile-footer\" *ngIf=\"tile.footer || tile.info\">\n <mat-divider></mat-divider>\n <div class=\"d-flex align-items-center\" style=\"gap: 4px; color: #9a9a9a; font-size: 12px; margin-top: 6px;\">\n <mat-icon *ngIf=\"tile.footerIcon\" style=\"font-size: 16px; width: 16px; height: 16px;\">{{tile.footerIcon}}</mat-icon>\n <mat-icon *ngIf=\"!tile.footerIcon && tile.info\" style=\"font-size: 16px; width: 16px; height: 16px; color: steelblue;\">info</mat-icon>\n <span>{{tile.footer ?? tile.info}}</span>\n </div>\n </div>\n </ng-container>\n\n <!-- Changed: Standard tile \u2014 Paper Dashboard style: icon left, label+value right, optional footer -->\n <ng-template #standardTile>\n <!-- Changed: Icon-style tile \u2014 icon left, label top-right, large value below -->\n <ng-container *ngIf=\"tile.icon; else basicTile\">\n <div class=\"tile-icon-row\">\n <div class=\"tile-icon-wrap\" [style.color]=\"tile.color ?? '#2196f3'\">\n <mat-icon>{{tile.icon}}</mat-icon>\n </div>\n <div class=\"tile-icon-content\">\n <div class=\"tile-icon-label\">{{tile.alias ?? tile.name | camelToWords}}</div>\n <!-- Changed (Quiet Loading D5): while the value is genuinely unknown the slot shows a shimmer chip instead of the old lying `0`; icon, label and footer render normally so the card never resizes. On a reload the previous number stays put and simply updates. -->\n <div class=\"tile-icon-value\" [style.color]=\"tile.color\" [attr.aria-busy]=\"showValueGhost(tile) ? 'true' : null\">\n <span *ngIf=\"tile.prefix\">{{tile.prefix}}</span><span *ngIf=\"showValueGhost(tile); else iconTileValue\" class=\"tin-skel tin-skel-text tile-value-ghost\" aria-hidden=\"true\"></span><ng-template #iconTileValue><span [class.tile-value-in]=\"effQuietLoading\">{{data?.[tile.name] ?? 0}}</span></ng-template><span *ngIf=\"tile.suffix\"> {{tile.suffix}}</span>\n <span *ngIf=\"tile.badge && data?.[tile.badge]\" class=\"tile-badge\" [style.backgroundColor]=\"tile.badgeColor ?? '#4caf50'\">{{data?.[tile.badge]}}</span>\n </div>\n </div>\n </div>\n <!-- Changed: Footer with divider \u2014 info tooltip or custom footer text -->\n <div class=\"tile-icon-footer\" *ngIf=\"tile.info || tile.footer\">\n <mat-divider></mat-divider>\n <div class=\"tile-icon-footer-content\">\n <mat-icon *ngIf=\"tile.footerIcon\" class=\"tile-icon-footer-icon\">{{tile.footerIcon}}</mat-icon>\n <mat-icon *ngIf=\"!tile.footerIcon && tile.info\" class=\"tile-icon-footer-icon\" style=\"color: steelblue;\">info</mat-icon>\n <span>{{tile.footer ?? tile.info}}</span>\n </div>\n </div>\n </ng-container>\n\n <!-- Basic tile fallback \u2014 centered number display (no icon) -->\n <ng-template #basicTile>\n <div class=\"row d-flex justify-content-center align-items-center\">\n <div style=\"text-align: center;font-size: 30px;\">\n <mat-label style=\"font-weight:bold;\" *ngIf=\"tile.prefix\" >{{tile.prefix}}</mat-label> &nbsp;\n <!-- Changed (Quiet Loading D5): same value ghost as the icon tile \u2014 chip while unknown, real number otherwise -->\n <mat-label style=\"font-weight:bold; text-align: center;\" [ngStyle]=\"{'color':tile.color }\" [attr.aria-busy]=\"showValueGhost(tile) ? 'true' : null\"><span *ngIf=\"showValueGhost(tile); else basicTileValue\" class=\"tin-skel tin-skel-text tile-value-ghost\" aria-hidden=\"true\"></span><ng-template #basicTileValue><span [class.tile-value-in]=\"effQuietLoading\">{{data?.[tile.name] ?? 0}}</span></ng-template></mat-label>&nbsp;\n <mat-label style=\"font-weight:bold;\" *ngIf=\"tile.suffix\">{{tile.suffix}}</mat-label>\n <span *ngIf=\"tile.badge && data?.[tile.badge]\" class=\"tile-badge\" [style.backgroundColor]=\"tile.badgeColor ?? '#4caf50'\">{{data?.[tile.badge]}}</span>\n </div>\n </div>\n <div class=\"row d-flex justify-content-center align-items-center\">\n <div class=\"d-flex justify-content-center align-items-center\" style=\"text-align: center;\">\n <mat-label style=\"padding-left:5px;padding-right:5px; text-align: center;font-size: 14px;\">{{tile.alias ?? tile.name | camelToWords}}</mat-label>\n <mat-icon *ngIf=\"tile.info\" [matTooltip]=\"tile.info\" matTooltipPosition=\"above\" style=\"font-size: 20px; color:steelblue;\">info</mat-icon>\n </div>\n </div>\n </ng-template>\n </ng-template>\n\n</ng-template>\n", styles: [".card{min-width:180px;flex:1;display:flex;flex-direction:column;align-items:center;padding:5px 10px}.tiles{gap:1;row-gap:5px}.spa-tiles-outer{display:flex;align-items:center;gap:4px}.spa-tiles{flex:1;min-width:0}.tiles-wrap{display:grid;gap:10px;align-items:stretch}.tiles-carousel{display:flex;gap:10px;overflow-x:auto;scroll-behavior:smooth;scroll-snap-type:x proximity;padding-bottom:6px}.tiles-carousel>.tile-card{scroll-snap-align:start}.tile-card{padding:10px 16px;margin:0}.tiles-nav{flex:0 0 auto;display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:50%;background:#0000000a;color:#666;cursor:pointer;transition:background-color .2s ease,color .2s ease}.tiles-nav:hover{background:#2196f31f;color:#2196f3}.tiles-nav mat-icon{font-size:20px;width:20px;height:20px}.col{transition:all .2s ease}.tile-clickable{cursor:pointer}.tile-clickable:hover{transform:translateY(-2px);box-shadow:0 4px 10px #00000021;background-color:#2196f312}.selected-tile{background-color:#e0e0e0;box-shadow:0 4px 8px #0003;transform:translateY(-2px);border:2px solid #3f51b5}.selected-tile mat-label{font-weight:700}.selected-tile:hover{background-color:#e0e0e0}.tile-chart-header{display:flex;justify-content:flex-start;font-size:11px;font-weight:500;color:#999;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}.tile-chart{width:100%;position:relative;margin-top:4px;display:flex;align-items:center;justify-content:center}.tile-chart canvas{width:100%!important;height:100%!important;max-height:inherit}.tile-chart-value{text-align:center;margin-top:6px}.tile-chart-value-text{font-size:22px;font-weight:600;letter-spacing:-.5px}.tile-badge{display:inline-block;font-size:12px;font-weight:500;color:#fff;padding:2px 8px;border-radius:12px;vertical-align:middle;margin-left:4px}.tile-footer{margin-top:8px}.tile-icon-row{display:flex;align-items:flex-start;gap:12px;padding:4px 0}.tile-icon-wrap{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:48px;height:48px}.tile-icon-wrap mat-icon{font-size:36px;width:36px;height:36px;opacity:.85}.tile-icon-content{flex:1;text-align:right;min-width:0}.tile-icon-label{font-size:12px;color:#999;text-transform:uppercase;letter-spacing:.3px;line-height:1.4}.tile-icon-value{font-size:26px;font-weight:600;line-height:1.2;letter-spacing:-.5px}.tile-icon-footer{margin-top:8px}.tile-icon-footer mat-divider{margin-bottom:6px}.tile-icon-footer-content{display:flex;align-items:center;gap:4px;font-size:12px;color:#999}.tile-icon-footer-icon{font-size:16px;width:16px;height:16px;color:#bbb}.tile-value-ghost{width:2.5ch;height:.72em;vertical-align:middle}.tile-value-in{animation:tile-value-in .15s ease-out both}@keyframes tile-value-in{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.tile-value-in{animation:none}}\n"] }]
14265
+ }], ctorParameters: () => [{ type: DataServiceLib }, { type: MessageService }, { type: i0.ChangeDetectorRef }, { type: ApiErrorService }], propDecorators: { tileScroller: [{
14266
+ type: ViewChild,
14267
+ args: ['tileScroller']
14268
+ }], config: [{
14149
14269
  type: Input
14150
14270
  }], tileActionSelected: [{
14151
14271
  type: Output
@@ -14616,7 +14736,7 @@ class TableActionComponent {
14616
14736
  : [];
14617
14737
  }
14618
14738
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableActionComponent, deps: [{ token: ButtonService }], target: i0.ɵɵFactoryTarget.Component }); }
14619
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TableActionComponent, isStandalone: false, selector: "app-table-action", inputs: { displayedButtons: "displayedButtons", config: "config", row: "row", smallScreen: "smallScreen" }, outputs: { actionClick: "actionClick" }, usesOnChanges: true, ngImport: i0, template: "<!-- TS-2: iterate precomputed view-models; no per-cell method calls or getters during change detection -->\n<ng-container *ngFor=\"let vm of visibleVMs\">\n\n <ng-container *ngIf=\"vm.name != 'create' && vm.visible\">\n <button mat-icon-button [matTooltip]=\"vm.button.tip ?? vm.name | titlecase\" matTooltipPosition=\"above\" style=\"margin-right:5px\" [disabled]=\"vm.disabled\" (click)=\"onActionClick(vm.name, row)\">\n <mat-icon [style.color]=\"vm.color\">{{vm.icon}}</mat-icon>\n </button>\n </ng-container>\n\n <ng-container *ngIf=\"vm.name != 'create' && !vm.visible && config?.collapseButtons === false\">\n <label style=\"margin-right: 35px;\"></label>\n </ng-container>\n\n</ng-container>\n\n<button *ngIf=\"overflowVMs.length > 0\" mat-icon-button [matMenuTriggerFor]=\"menu\" [matTooltip]=\"'More actions'\" matTooltipPosition=\"above\">\n <mat-icon>more_vert</mat-icon>\n</button>\n\n<mat-menu #menu=\"matMenu\">\n <ng-container *ngFor=\"let vm of overflowVMs\">\n <button *ngIf=\"vm.visible\" mat-menu-item [disabled]=\"vm.disabled\" (click)=\"onActionClick(vm.name, row)\">\n <mat-icon [style.color]=\"vm.color\">\n {{vm.icon}}\n </mat-icon>\n <span>{{vm.button.tip ?? vm.name | titlecase}}</span>\n </button>\n </ng-container>\n</mat-menu>\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#e5e5e5}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i7$2.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7$2.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7$2.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "pipe", type: i1$2.TitleCasePipe, name: "titlecase" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
14739
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TableActionComponent, isStandalone: false, selector: "app-table-action", inputs: { displayedButtons: "displayedButtons", config: "config", row: "row", smallScreen: "smallScreen" }, outputs: { actionClick: "actionClick" }, usesOnChanges: true, ngImport: i0, template: "<!-- TS-2: iterate precomputed view-models; no per-cell method calls or getters during change detection -->\n<ng-container *ngFor=\"let vm of visibleVMs\">\n\n <ng-container *ngIf=\"vm.name != 'create' && vm.visible\">\n <button mat-icon-button [matTooltip]=\"vm.button.tip ?? vm.name | titlecase\" matTooltipPosition=\"above\" style=\"margin-right:5px\" [disabled]=\"vm.disabled\" (click)=\"onActionClick(vm.name, row)\">\n <mat-icon [style.color]=\"vm.color\">{{vm.icon}}</mat-icon>\n </button>\n </ng-container>\n\n <ng-container *ngIf=\"vm.name != 'create' && !vm.visible && config?.collapseButtons === false\">\n <label style=\"margin-right: 35px;\"></label>\n </ng-container>\n\n</ng-container>\n\n<button *ngIf=\"overflowVMs.length > 0\" mat-icon-button [matMenuTriggerFor]=\"menu\" [matTooltip]=\"'More actions'\" matTooltipPosition=\"above\">\n <mat-icon>more_vert</mat-icon>\n</button>\n\n<mat-menu #menu=\"matMenu\">\n <ng-container *ngFor=\"let vm of overflowVMs\">\n <button *ngIf=\"vm.visible\" mat-menu-item [disabled]=\"vm.disabled\" (click)=\"onActionClick(vm.name, row)\">\n <mat-icon [style.color]=\"vm.color\">\n {{vm.icon}}\n </mat-icon>\n <span>{{vm.button.tip ?? vm.name | titlecase}}</span>\n </button>\n </ng-container>\n</mat-menu>\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#e5e5e5}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i4$4.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i4$4.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i4$4.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "pipe", type: i1$2.TitleCasePipe, name: "titlecase" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
14620
14740
  }
14621
14741
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableActionComponent, decorators: [{
14622
14742
  type: Component,
@@ -14857,8 +14977,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
14857
14977
  }] } });
14858
14978
 
14859
14979
  class SelectComponent extends SelectCommonComponent {
14860
- constructor(messageService, dataService, dialogService, buttonService) {
14861
- super(messageService, dataService);
14980
+ constructor(messageService, dataService, dialogService, buttonService, apiErrorService) {
14981
+ super(messageService, dataService, apiErrorService); // Changed: base constructor gained apiErrorService
14862
14982
  this.messageService = messageService;
14863
14983
  this.dataService = dataService;
14864
14984
  this.dialogService = dialogService;
@@ -14960,13 +15080,13 @@ class SelectComponent extends SelectCommonComponent {
14960
15080
  this.value = createdID;
14961
15081
  this.valueChange.emit(this.value);
14962
15082
  }
14963
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SelectComponent, deps: [{ token: MessageService }, { token: DataServiceLib }, { token: DialogService }, { token: ButtonService }], target: i0.ɵɵFactoryTarget.Component }); }
15083
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SelectComponent, deps: [{ token: MessageService }, { token: DataServiceLib }, { token: DialogService }, { token: ButtonService }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
14964
15084
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SelectComponent, isStandalone: false, selector: "spa-select", inputs: { detailsConfig: "detailsConfig" }, usesInheritance: true, usesOnChanges: true, ngImport: i0, template: "<spa-select-common [width]=\"width\" [readonly]=\"readonly\" [required]=\"required\" [defaultFirstValue]=\"defaultFirstValue\"\n [readonlyMode]=\"readonlyMode\" [hint]=\"hint\" [placeholder]=\"placeholder\"\n [display]=\"display\" [(value)]=\"value\" [options]=\"options\" [masterOptions]=\"masterOptions\" [masterField]=\"masterField\"\n [optionValue]=\"optionValue\" [optionDisplay]=\"optionDisplay\" [optionDisplayExtra]=\"optionDisplayExtra\"\n [nullable]=\"nullable\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [loadAction]=\"loadAction\" [loadIDField]=\"loadIDField\" [field]=\"field\" [data]=\"data\"\n [addOptionLabel]=\"addOptionLabel\"\n (valueChange)=\"valueChange.emit($event)\"\n (hoverChange)=\"onHoverChange($event)\"\n (addNew)=\"onPeekClick(null, 'create')\">\n <ng-container additionalButtons>\n <button mat-icon-button *ngIf=\"detailsConfig && canCreate() && isHovered\" (click)=\"onPeekClick($event, 'create')\" matTooltip=\"Add\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" style=\"color: green;\">add</mat-icon>\n </button>\n <button mat-icon-button *ngIf=\"detailsConfig && canView() && isHovered && value\" (click)=\"onPeekClick($event, 'view')\" matTooltip=\"View\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" color=\"primary\">launch</mat-icon>\n </button>\n\n </ng-container>\n</spa-select-common>\n", styles: [""], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SelectCommonComponent, selector: "spa-select-common", inputs: ["width", "readonly", "required", "defaultFirstValue", "readonlyMode", "hint", "placeholder", "display", "value", "options", "masterOptions", "masterField", "optionValue", "optionDisplay", "optionDisplayExtra", "nullable", "infoMessage", "copyContent", "loadAction", "loadIDField", "field", "data", "addOptionLabel"], outputs: ["valueChange", "hoverChange", "addNew"] }] }); }
14965
15085
  }
14966
15086
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SelectComponent, decorators: [{
14967
15087
  type: Component,
14968
15088
  args: [{ selector: 'spa-select', standalone: false, template: "<spa-select-common [width]=\"width\" [readonly]=\"readonly\" [required]=\"required\" [defaultFirstValue]=\"defaultFirstValue\"\n [readonlyMode]=\"readonlyMode\" [hint]=\"hint\" [placeholder]=\"placeholder\"\n [display]=\"display\" [(value)]=\"value\" [options]=\"options\" [masterOptions]=\"masterOptions\" [masterField]=\"masterField\"\n [optionValue]=\"optionValue\" [optionDisplay]=\"optionDisplay\" [optionDisplayExtra]=\"optionDisplayExtra\"\n [nullable]=\"nullable\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [loadAction]=\"loadAction\" [loadIDField]=\"loadIDField\" [field]=\"field\" [data]=\"data\"\n [addOptionLabel]=\"addOptionLabel\"\n (valueChange)=\"valueChange.emit($event)\"\n (hoverChange)=\"onHoverChange($event)\"\n (addNew)=\"onPeekClick(null, 'create')\">\n <ng-container additionalButtons>\n <button mat-icon-button *ngIf=\"detailsConfig && canCreate() && isHovered\" (click)=\"onPeekClick($event, 'create')\" matTooltip=\"Add\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" style=\"color: green;\">add</mat-icon>\n </button>\n <button mat-icon-button *ngIf=\"detailsConfig && canView() && isHovered && value\" (click)=\"onPeekClick($event, 'view')\" matTooltip=\"View\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" color=\"primary\">launch</mat-icon>\n </button>\n\n </ng-container>\n</spa-select-common>\n" }]
14969
- }], ctorParameters: () => [{ type: MessageService }, { type: DataServiceLib }, { type: DialogService }, { type: ButtonService }], propDecorators: { detailsConfig: [{
15089
+ }], ctorParameters: () => [{ type: MessageService }, { type: DataServiceLib }, { type: DialogService }, { type: ButtonService }, { type: ApiErrorService }], propDecorators: { detailsConfig: [{
14970
15090
  type: Input
14971
15091
  }] } });
14972
15092
 
@@ -15595,7 +15715,7 @@ class EmailComponent {
15595
15715
  });
15596
15716
  }
15597
15717
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: EmailComponent, deps: [{ token: MessageService }], target: i0.ɵɵFactoryTarget.Component }); }
15598
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: EmailComponent, isStandalone: false, selector: "spa-email", inputs: { display: "display", value: "value", readonly: "readonly", required: "required", hint: "hint", suffix: "suffix", infoMessage: "infoMessage", copyContent: "copyContent", clearContent: "clearContent", options: "options", optionValue: "optionValue" }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<mat-form-field [class.spa-readonly]=\"readonly\" style=\"width: 100%;\" subscriptSizing=\"dynamic\">\n <mat-label>{{display}}</mat-label>\n <mat-chip-grid #chipList>\n <mat-chip-row *ngFor=\"let email of emails\" [removable]=\"!readonly\" (removed)=\"removeEmail(email)\">\n {{email}}\n <button matChipRemove *ngIf=\"!readonly\"><mat-icon class=\"tinyIcon\">cancel</mat-icon></button>\n </mat-chip-row>\n <input autocomplete=\"off\" #emailInput [placeholder]=\"readonly ? '' : 'Enter email addresses'\" [matChipInputFor]=\"chipList\" [matChipInputSeparatorKeyCodes]=\"[13, 186]\" [matChipInputAddOnBlur]=\"true\" (matChipInputTokenEnd)=\"addEmail($event)\" [formControl]=\"emailControl\" (paste)=\"handlePaste($event)\" [readonly]=\"readonly\" [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\">\n </mat-chip-grid>\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"optionSelected($event)\">\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"optionValue ? option[optionValue] : option\">\n {{optionValue ? option[optionValue] : option}}\n </mat-option>\n </mat-autocomplete>\n <mat-error *ngIf=\"emailControl.invalid && emailControl.touched\">Invalid email format</mat-error>\n <mat-hint *ngIf=\"hint\">{{hint}}</mat-hint>\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clearEmails()\" [isHovered]=\"true\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [":host ::ng-deep .mat-form-field-flex{align-items:flex-start}:host ::ng-deep .mat-form-field-infix{max-height:170px;overflow-y:auto;padding-top:.25em;padding-bottom:.25em;overflow-x:hidden}:host ::ng-deep mat-chip{flex:0 0 auto}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$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: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3$1.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3$1.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i6.MatChipGrid, selector: "mat-chip-grid", inputs: ["disabled", "placeholder", "required", "value", "errorStateMatcher"], outputs: ["change", "valueChange"] }, { kind: "directive", type: i6.MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputFor", "matChipInputAddOnBlur", "matChipInputSeparatorKeyCodes", "placeholder", "id", "disabled", "readonly", "matChipInputDisabledInteractive"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { kind: "directive", type: i6.MatChipRemove, selector: "[matChipRemove]" }, { kind: "component", type: i6.MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "component", type: i7$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i12.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "directive", type: i12.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }] }); }
15718
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: EmailComponent, isStandalone: false, selector: "spa-email", inputs: { display: "display", value: "value", readonly: "readonly", required: "required", hint: "hint", suffix: "suffix", infoMessage: "infoMessage", copyContent: "copyContent", clearContent: "clearContent", options: "options", optionValue: "optionValue" }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<mat-form-field [class.spa-readonly]=\"readonly\" style=\"width: 100%;\" subscriptSizing=\"dynamic\">\n <mat-label>{{display}}</mat-label>\n <mat-chip-grid #chipList>\n <mat-chip-row *ngFor=\"let email of emails\" [removable]=\"!readonly\" (removed)=\"removeEmail(email)\">\n {{email}}\n <button matChipRemove *ngIf=\"!readonly\"><mat-icon class=\"tinyIcon\">cancel</mat-icon></button>\n </mat-chip-row>\n <input autocomplete=\"off\" #emailInput [placeholder]=\"readonly ? '' : 'Enter email addresses'\" [matChipInputFor]=\"chipList\" [matChipInputSeparatorKeyCodes]=\"[13, 186]\" [matChipInputAddOnBlur]=\"true\" (matChipInputTokenEnd)=\"addEmail($event)\" [formControl]=\"emailControl\" (paste)=\"handlePaste($event)\" [readonly]=\"readonly\" [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\">\n </mat-chip-grid>\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"optionSelected($event)\">\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"optionValue ? option[optionValue] : option\">\n {{optionValue ? option[optionValue] : option}}\n </mat-option>\n </mat-autocomplete>\n <mat-error *ngIf=\"emailControl.invalid && emailControl.touched\">Invalid email format</mat-error>\n <mat-hint *ngIf=\"hint\">{{hint}}</mat-hint>\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clearEmails()\" [isHovered]=\"true\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [":host ::ng-deep .mat-form-field-flex{align-items:flex-start}:host ::ng-deep .mat-form-field-infix{max-height:170px;overflow-y:auto;padding-top:.25em;padding-bottom:.25em;overflow-x:hidden}:host ::ng-deep mat-chip{flex:0 0 auto}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$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: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3$1.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3$1.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i6.MatChipGrid, selector: "mat-chip-grid", inputs: ["disabled", "placeholder", "required", "value", "errorStateMatcher"], outputs: ["change", "valueChange"] }, { kind: "directive", type: i6.MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputFor", "matChipInputAddOnBlur", "matChipInputSeparatorKeyCodes", "placeholder", "id", "disabled", "readonly", "matChipInputDisabledInteractive"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { kind: "directive", type: i6.MatChipRemove, selector: "[matChipRemove]" }, { kind: "component", type: i6.MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "component", type: i7$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i13.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "directive", type: i13.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }] }); }
15599
15719
  }
15600
15720
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: EmailComponent, decorators: [{
15601
15721
  type: Component,
@@ -15755,7 +15875,7 @@ class CapsulesComponent {
15755
15875
  return this.displayedButtons?.find(x => x.name === name) || null;
15756
15876
  }
15757
15877
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CapsulesComponent, deps: [{ token: ButtonService }, { token: ConditionService }], target: i0.ɵɵFactoryTarget.Component }); }
15758
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: CapsulesComponent, isStandalone: false, selector: "spa-capsules", inputs: { config: "config", dataSource: "dataSource", displayedButtons: "displayedButtons" }, outputs: { actionClick: "actionClick" }, usesOnChanges: true, ngImport: i0, template: "<mat-chip-set>\n <mat-chip *ngFor=\"let row of dataSource\" [matMenuTriggerFor]=\"itemMenu\" [matMenuTriggerData]=\"{item: row}\">\n <!-- Left icons -->\n <ng-container *ngFor=\"let icon of config.capsuleConfig?.leftIcons\">\n <mat-icon *ngIf=\"testIconCondition(row, icon)\" \n [ngStyle]=\"{'color': icon?.color}\" \n style=\"font-size: 18px; padding-top: 3px; margin-right: 5px;\">\n {{icon.name}}\n </mat-icon>\n </ng-container>\n \n <!-- Display text -->\n <label style=\"margin-right: 5px;\">{{getDisplayValue(row)}}</label>\n \n <!-- Right icons -->\n <ng-container *ngFor=\"let icon of config.capsuleConfig?.rightIcons\">\n <mat-icon *ngIf=\"testIconCondition(row, icon)\" \n [ngStyle]=\"{'color': icon?.color}\" \n style=\"font-size: 18px; padding-top: 3px; margin-left: 5px;\">\n {{icon.name}}\n </mat-icon>\n </ng-container>\n </mat-chip>\n</mat-chip-set>\n \n <!-- Context menu -->\n<mat-menu #itemMenu=\"matMenu\">\n <ng-template matMenuContent let-item=\"item\">\n <button mat-menu-item *ngFor=\"let button of getVisibleButtons(item)\"\n [disabled]=\"testDisabled(item, button.name)\"\n (click)=\"onActionClick(button.name, item)\">\n <mat-icon [ngStyle]=\"{'color': getButtonColor(button, item)}\">\n {{getIcon(button.name)}}\n </mat-icon>\n <span>{{button.display || (button.name | titlecase)}}</span>\n </button>\n </ng-template>\n</mat-menu>", styles: ["mat-chip{cursor:pointer;margin:4px}mat-chip:hover{opacity:.8}mat-icon{vertical-align:middle}label{vertical-align:middle;margin:0}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i7$2.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7$2.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7$2.MatMenuContent, selector: "ng-template[matMenuContent]" }, { kind: "directive", type: i7$2.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i6.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["role", "id", "aria-label", "aria-description", "value", "color", "removable", "highlighted", "disableRipple", "disabled"], outputs: ["removed", "destroyed"], exportAs: ["matChip"] }, { kind: "component", type: i6.MatChipSet, selector: "mat-chip-set", inputs: ["disabled", "role", "tabIndex"] }, { kind: "pipe", type: i1$2.TitleCasePipe, name: "titlecase" }] }); }
15878
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: CapsulesComponent, isStandalone: false, selector: "spa-capsules", inputs: { config: "config", dataSource: "dataSource", displayedButtons: "displayedButtons" }, outputs: { actionClick: "actionClick" }, usesOnChanges: true, ngImport: i0, template: "<mat-chip-set>\n <mat-chip *ngFor=\"let row of dataSource\" [matMenuTriggerFor]=\"itemMenu\" [matMenuTriggerData]=\"{item: row}\">\n <!-- Left icons -->\n <ng-container *ngFor=\"let icon of config.capsuleConfig?.leftIcons\">\n <mat-icon *ngIf=\"testIconCondition(row, icon)\" \n [ngStyle]=\"{'color': icon?.color}\" \n style=\"font-size: 18px; padding-top: 3px; margin-right: 5px;\">\n {{icon.name}}\n </mat-icon>\n </ng-container>\n \n <!-- Display text -->\n <label style=\"margin-right: 5px;\">{{getDisplayValue(row)}}</label>\n \n <!-- Right icons -->\n <ng-container *ngFor=\"let icon of config.capsuleConfig?.rightIcons\">\n <mat-icon *ngIf=\"testIconCondition(row, icon)\" \n [ngStyle]=\"{'color': icon?.color}\" \n style=\"font-size: 18px; padding-top: 3px; margin-left: 5px;\">\n {{icon.name}}\n </mat-icon>\n </ng-container>\n </mat-chip>\n</mat-chip-set>\n \n <!-- Context menu -->\n<mat-menu #itemMenu=\"matMenu\">\n <ng-template matMenuContent let-item=\"item\">\n <button mat-menu-item *ngFor=\"let button of getVisibleButtons(item)\"\n [disabled]=\"testDisabled(item, button.name)\"\n (click)=\"onActionClick(button.name, item)\">\n <mat-icon [ngStyle]=\"{'color': getButtonColor(button, item)}\">\n {{getIcon(button.name)}}\n </mat-icon>\n <span>{{button.display || (button.name | titlecase)}}</span>\n </button>\n </ng-template>\n</mat-menu>", styles: ["mat-chip{cursor:pointer;margin:4px}mat-chip:hover{opacity:.8}mat-icon{vertical-align:middle}label{vertical-align:middle;margin:0}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i4$4.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i4$4.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i4$4.MatMenuContent, selector: "ng-template[matMenuContent]" }, { kind: "directive", type: i4$4.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i6.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["role", "id", "aria-label", "aria-description", "value", "color", "removable", "highlighted", "disableRipple", "disabled"], outputs: ["removed", "destroyed"], exportAs: ["matChip"] }, { kind: "component", type: i6.MatChipSet, selector: "mat-chip-set", inputs: ["disabled", "role", "tabIndex"] }, { kind: "pipe", type: i1$2.TitleCasePipe, name: "titlecase" }] }); }
15759
15879
  }
15760
15880
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CapsulesComponent, decorators: [{
15761
15881
  type: Component,
@@ -15881,12 +16001,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
15881
16001
  // Displays data in grouped card format with chips for items
15882
16002
  class GroupsComponent {
15883
16003
  constructor(conditionService, buttonService, dataService, // Changed: Injected for drop action API calls
15884
- snackBar // Changed: Injected for error feedback on failed drops
16004
+ snackBar, // Changed: Injected for error feedback on failed drops (still used by the transport-error fallback below)
16005
+ apiErrorService // Added: classifies a failed drop's response.message — a leaked SQL fragment gets the friendly dialog, a business refusal stays a toast
15885
16006
  ) {
15886
16007
  this.conditionService = conditionService;
15887
16008
  this.buttonService = buttonService;
15888
16009
  this.dataService = dataService;
15889
16010
  this.snackBar = snackBar;
16011
+ this.apiErrorService = apiErrorService;
15890
16012
  // Added: stable-reference caches — all three of these are bound to *ngFor (per group and per item), and
15891
16013
  // each call re-filtered into a NEW array on every change-detection cycle.
15892
16014
  this.iconCache = new WeakMap();
@@ -16021,7 +16143,7 @@ class GroupsComponent {
16021
16143
  if (!response.success) {
16022
16144
  // Revert on failure
16023
16145
  transferArrayItem(event.container.data, event.previousContainer.data, event.currentIndex, event.previousIndex);
16024
- this.snackBar.open(response.message || 'Move failed', 'OK', { duration: 3000 });
16146
+ this.apiErrorService.presentAppFailure(response, 'action', dropAction.url); // Changed: was snackBar.open(response.message ...) raw server text on a user surface; presentAppFailure classifies it (business refusal -> toast, opaque server text -> friendly dialog) and dedupes. The drag revert above is unchanged
16025
16147
  }
16026
16148
  },
16027
16149
  error: () => {
@@ -16157,13 +16279,13 @@ class GroupsComponent {
16157
16279
  button: button
16158
16280
  });
16159
16281
  }
16160
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: GroupsComponent, deps: [{ token: ConditionService }, { token: ButtonService }, { token: DataServiceLib }, { token: i2$1.MatSnackBar }], target: i0.ɵɵFactoryTarget.Component }); }
16161
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: GroupsComponent, isStandalone: false, selector: "spa-groups", inputs: { config: "config", dataSource: "dataSource", displayedButtons: "displayedButtons", showOwnFilter: "showOwnFilter", filterText: "filterText" }, outputs: { actionClick: "actionClick" }, usesOnChanges: true, ngImport: i0, template: "<!-- Changed: this filter row was REMOVED from the grouped view's own markup and now lives in the table header,\n beside the action buttons, where every other table config puts it. It used to sit on a row of its own\n directly under the buttons row, leaving both rows half empty and visibly misaligned.\n The filtering logic here is untouched \u2014 only the input that drives it moved, and its text arrives via the\n filterText @Input relayed from the header. Kept as a fallback for a grouped view rendered WITHOUT the\n standard header (spa-groups can be used directly), so filtering is never simply lost. -->\n<div class=\"groups-filter\" *ngIf=\"showOwnFilter\">\n <mat-form-field appearance=\"outline\" class=\"filter-field\">\n <mat-icon matPrefix>search</mat-icon>\n <input matInput placeholder=\"Filter\" [(ngModel)]=\"filterText\" (ngModelChange)=\"applyFilter()\">\n <button *ngIf=\"filterText\" mat-icon-button matSuffix (click)=\"filterText = ''; applyFilter()\">\n <mat-icon>close</mat-icon>\n </button>\n </mat-form-field>\n</div>\n\n<div class=\"groups-container\" cdkDropListGroup>\n\n <mat-card *ngFor=\"let group of groupedData\" class=\"group-card\">\n\n <div class=\"group-header\">\n <div class=\"header-left\">\n <mat-icon *ngIf=\"group.icon\" [style.color]=\"group.color\" class=\"group-icon\">{{group.icon}}</mat-icon>\n <label class=\"group-title\">{{group.displayName}}</label>\n <label *ngIf=\"config.groupConfig.showGroupCount !== false\" class=\"group-count\">({{group.items.length}})</label>\n </div>\n\n <div class=\"header-right\">\n <button\n *ngFor=\"let button of getVisibleHeaderButtons(group)\"\n mat-icon-button\n [matTooltip]=\"getHeaderButtonTooltip(button)\"\n matTooltipPosition=\"above\"\n [style.color]=\"getHeaderButtonColor(button, group)\"\n [disabled]=\"isHeaderButtonDisabled(button, group)\"\n (click)=\"headerButtonClicked(button, group)\">\n <mat-icon>{{getHeaderButtonIcon(button)}}</mat-icon>\n </button>\n </div>\n </div>\n\n <hr class=\"group-divider\" />\n\n <!-- Empty state: show when no items and drag is disabled -->\n <div *ngIf=\"group.items.length === 0 && !config.groupConfig.dragEnabled\" class=\"empty-state\">\n <label>{{config.groupConfig.emptyGroupMessage ?? 'Empty'}}</label>\n </div>\n\n <!-- Changed: Replaced mat-chip-set with flex container of mat-stroked-button for cleaner button style -->\n <div\n *ngIf=\"group.items.length > 0 || config.groupConfig.dragEnabled\"\n cdkDropList\n [cdkDropListData]=\"group.items\"\n [cdkDropListDisabled]=\"!config.groupConfig.dragEnabled\"\n (cdkDropListDropped)=\"onDrop($event)\"\n (cdkDropListEntered)=\"onDropListEntered($event)\"\n (cdkDropListExited)=\"onDropListExited($event)\"\n [class.drop-highlight]=\"group === highlightedGroup\"\n class=\"drop-list items-container\">\n\n <!-- Empty state inside drop list when drag is enabled -->\n <div *ngIf=\"group.items.length === 0\" class=\"empty-state drop-empty\">\n <label>{{config.groupConfig.emptyGroupMessage ?? 'Empty'}}</label>\n </div>\n\n <button\n mat-stroked-button\n *ngFor=\"let item of group.items\"\n cdkDrag [cdkDragData]=\"item\"\n [cdkDragDisabled]=\"!config.groupConfig.dragEnabled\"\n [matMenuTriggerFor]=\"config.groupConfig.contextMenuEnabled !== false ? itemMenu : null\"\n [matMenuTriggerData]=\"{item: item, group: group}\"\n class=\"item-button\">\n\n <mat-icon\n *ngIf=\"getItemIcon(item)\"\n [style.color]=\"getItemIconColor(item)\"\n [matTooltip]=\"getItemIconTip(item)\"\n matTooltipPosition=\"above\"\n class=\"item-icon\">\n {{getItemIcon(item)}}\n </mat-icon>\n\n {{getItemText(item)}}\n\n <ng-container *ngFor=\"let additionalIcon of getVisibleAdditionalIcons(item)\">\n <mat-icon\n [style.color]=\"additionalIcon.color\"\n [matTooltip]=\"getIconTooltip(additionalIcon, item)\"\n matTooltipPosition=\"above\"\n class=\"item-additional-icon\">\n {{additionalIcon.name}}\n </mat-icon>\n </ng-container>\n\n </button>\n </div>\n\n </mat-card>\n\n</div>\n\n<mat-menu #itemMenu=\"matMenu\">\n <ng-template matMenuContent let-item=\"item\" let-group=\"group\">\n <button\n *ngFor=\"let button of getVisibleButtons(item)\"\n mat-menu-item\n [disabled]=\"isButtonDisabled(button, item)\"\n (click)=\"itemActionClicked(button.name, item, group)\">\n <mat-icon [style.color]=\"getButtonIconColor(button, item)\">{{getButtonIcon(button)}}</mat-icon>\n <span>{{button.display ?? button.tip ?? (button.name | titlecase)}}</span>\n </button>\n </ng-template>\n</mat-menu>\n", styles: [".groups-filter{display:flex;justify-content:flex-end;margin-bottom:8px;padding-right:10px}.filter-field{width:300px;font-size:13px}.filter-field ::ng-deep .mat-mdc-form-field-infix{padding-top:8px!important;padding-bottom:8px!important;min-height:36px}.groups-container{display:flex;flex-direction:column;gap:20px;padding-right:10px;margin-left:0}.group-card{width:100%;margin-bottom:0;padding:8px 16px 16px}.group-header{display:flex;justify-content:space-between;align-items:center;padding:0;min-height:32px}.header-left{display:flex;align-items:center;gap:5px}.header-right{display:flex;align-items:center;gap:4px}.group-icon{margin-right:5px;font-size:24px;width:24px;height:24px;line-height:24px}.group-title{font-size:24px;font-weight:300;line-height:1.2}.group-count{font-size:12px;font-weight:300;margin-left:5px;line-height:1.2}.group-divider{margin-top:8px;margin-bottom:10px}.empty-state{display:flex;justify-content:center;align-items:center;padding:10px}.empty-state label{color:#757575;font-style:italic}.items-container{display:flex;flex-wrap:wrap;gap:10px;padding:8px 0;align-items:center}.item-button{cursor:pointer;font-weight:400;letter-spacing:.01em;transition:box-shadow .2s ease,background-color .2s ease}.item-button:hover{box-shadow:0 1px 4px #00000026;background-color:#00000005}.item-icon{font-size:18px;width:18px;height:18px;margin-right:4px;vertical-align:middle}.item-additional-icon{font-size:16px;width:16px;height:16px;margin-left:6px;vertical-align:middle;opacity:.85}button[mat-icon-button]{width:32px;height:32px}button[mat-icon-button] mat-icon{font-size:18px;margin-top:-3px}.header-right button[mat-icon-button]{display:inline-flex;align-items:center;justify-content:center;padding:0;line-height:normal}.header-right button[mat-icon-button] mat-icon{margin:0;font-size:20px;width:20px;height:20px;line-height:20px;display:block}.header-right button[mat-icon-button] .mat-mdc-button-touch-target{display:none}.drop-list{min-height:40px}.drop-empty{padding:5px 10px}.cdk-drag-preview{box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;opacity:.9;border-radius:4px}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.drop-highlight{border:2px dashed #90caf9;border-radius:8px;background:#90caf90d}.cdk-drop-list-dragging .item-button:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$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: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i7$2.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7$2.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7$2.MatMenuContent, selector: "ng-template[matMenuContent]" }, { kind: "directive", type: i7$2.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }, { kind: "directive", type: i3$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4$2.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: i14.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i14.CdkDropListGroup, selector: "[cdkDropListGroup]", inputs: ["cdkDropListGroupDisabled"], exportAs: ["cdkDropListGroup"] }, { kind: "directive", type: i14.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "pipe", type: i1$2.TitleCasePipe, name: "titlecase" }] }); }
16282
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: GroupsComponent, deps: [{ token: ConditionService }, { token: ButtonService }, { token: DataServiceLib }, { token: i2$1.MatSnackBar }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
16283
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: GroupsComponent, isStandalone: false, selector: "spa-groups", inputs: { config: "config", dataSource: "dataSource", displayedButtons: "displayedButtons", showOwnFilter: "showOwnFilter", filterText: "filterText" }, outputs: { actionClick: "actionClick" }, usesOnChanges: true, ngImport: i0, template: "<!-- Changed: this filter row was REMOVED from the grouped view's own markup and now lives in the table header,\n beside the action buttons, where every other table config puts it. It used to sit on a row of its own\n directly under the buttons row, leaving both rows half empty and visibly misaligned.\n The filtering logic here is untouched \u2014 only the input that drives it moved, and its text arrives via the\n filterText @Input relayed from the header. Kept as a fallback for a grouped view rendered WITHOUT the\n standard header (spa-groups can be used directly), so filtering is never simply lost. -->\n<div class=\"groups-filter\" *ngIf=\"showOwnFilter\">\n <mat-form-field appearance=\"outline\" class=\"filter-field\">\n <mat-icon matPrefix>search</mat-icon>\n <input matInput placeholder=\"Filter\" [(ngModel)]=\"filterText\" (ngModelChange)=\"applyFilter()\">\n <button *ngIf=\"filterText\" mat-icon-button matSuffix (click)=\"filterText = ''; applyFilter()\">\n <mat-icon>close</mat-icon>\n </button>\n </mat-form-field>\n</div>\n\n<div class=\"groups-container\" cdkDropListGroup>\n\n <mat-card *ngFor=\"let group of groupedData\" class=\"group-card\">\n\n <div class=\"group-header\">\n <div class=\"header-left\">\n <mat-icon *ngIf=\"group.icon\" [style.color]=\"group.color\" class=\"group-icon\">{{group.icon}}</mat-icon>\n <label class=\"group-title\">{{group.displayName}}</label>\n <label *ngIf=\"config.groupConfig.showGroupCount !== false\" class=\"group-count\">({{group.items.length}})</label>\n </div>\n\n <div class=\"header-right\">\n <button\n *ngFor=\"let button of getVisibleHeaderButtons(group)\"\n mat-icon-button\n [matTooltip]=\"getHeaderButtonTooltip(button)\"\n matTooltipPosition=\"above\"\n [style.color]=\"getHeaderButtonColor(button, group)\"\n [disabled]=\"isHeaderButtonDisabled(button, group)\"\n (click)=\"headerButtonClicked(button, group)\">\n <mat-icon>{{getHeaderButtonIcon(button)}}</mat-icon>\n </button>\n </div>\n </div>\n\n <hr class=\"group-divider\" />\n\n <!-- Empty state: show when no items and drag is disabled -->\n <div *ngIf=\"group.items.length === 0 && !config.groupConfig.dragEnabled\" class=\"empty-state\">\n <label>{{config.groupConfig.emptyGroupMessage ?? 'Empty'}}</label>\n </div>\n\n <!-- Changed: Replaced mat-chip-set with flex container of mat-stroked-button for cleaner button style -->\n <div\n *ngIf=\"group.items.length > 0 || config.groupConfig.dragEnabled\"\n cdkDropList\n [cdkDropListData]=\"group.items\"\n [cdkDropListDisabled]=\"!config.groupConfig.dragEnabled\"\n (cdkDropListDropped)=\"onDrop($event)\"\n (cdkDropListEntered)=\"onDropListEntered($event)\"\n (cdkDropListExited)=\"onDropListExited($event)\"\n [class.drop-highlight]=\"group === highlightedGroup\"\n class=\"drop-list items-container\">\n\n <!-- Empty state inside drop list when drag is enabled -->\n <div *ngIf=\"group.items.length === 0\" class=\"empty-state drop-empty\">\n <label>{{config.groupConfig.emptyGroupMessage ?? 'Empty'}}</label>\n </div>\n\n <button\n mat-stroked-button\n *ngFor=\"let item of group.items\"\n cdkDrag [cdkDragData]=\"item\"\n [cdkDragDisabled]=\"!config.groupConfig.dragEnabled\"\n [matMenuTriggerFor]=\"config.groupConfig.contextMenuEnabled !== false ? itemMenu : null\"\n [matMenuTriggerData]=\"{item: item, group: group}\"\n class=\"item-button\">\n\n <mat-icon\n *ngIf=\"getItemIcon(item)\"\n [style.color]=\"getItemIconColor(item)\"\n [matTooltip]=\"getItemIconTip(item)\"\n matTooltipPosition=\"above\"\n class=\"item-icon\">\n {{getItemIcon(item)}}\n </mat-icon>\n\n {{getItemText(item)}}\n\n <ng-container *ngFor=\"let additionalIcon of getVisibleAdditionalIcons(item)\">\n <mat-icon\n [style.color]=\"additionalIcon.color\"\n [matTooltip]=\"getIconTooltip(additionalIcon, item)\"\n matTooltipPosition=\"above\"\n class=\"item-additional-icon\">\n {{additionalIcon.name}}\n </mat-icon>\n </ng-container>\n\n </button>\n </div>\n\n </mat-card>\n\n</div>\n\n<mat-menu #itemMenu=\"matMenu\">\n <ng-template matMenuContent let-item=\"item\" let-group=\"group\">\n <button\n *ngFor=\"let button of getVisibleButtons(item)\"\n mat-menu-item\n [disabled]=\"isButtonDisabled(button, item)\"\n (click)=\"itemActionClicked(button.name, item, group)\">\n <mat-icon [style.color]=\"getButtonIconColor(button, item)\">{{getButtonIcon(button)}}</mat-icon>\n <span>{{button.display ?? button.tip ?? (button.name | titlecase)}}</span>\n </button>\n </ng-template>\n</mat-menu>\n", styles: [".groups-filter{display:flex;justify-content:flex-end;margin-bottom:8px;padding-right:10px}.filter-field{width:300px;font-size:13px}.filter-field ::ng-deep .mat-mdc-form-field-infix{padding-top:8px!important;padding-bottom:8px!important;min-height:36px}.groups-container{display:flex;flex-direction:column;gap:20px;padding-right:10px;margin-left:0}.group-card{width:100%;margin-bottom:0;padding:8px 16px 16px}.group-header{display:flex;justify-content:space-between;align-items:center;padding:0;min-height:32px}.header-left{display:flex;align-items:center;gap:5px}.header-right{display:flex;align-items:center;gap:4px}.group-icon{margin-right:5px;font-size:24px;width:24px;height:24px;line-height:24px}.group-title{font-size:24px;font-weight:300;line-height:1.2}.group-count{font-size:12px;font-weight:300;margin-left:5px;line-height:1.2}.group-divider{margin-top:8px;margin-bottom:10px}.empty-state{display:flex;justify-content:center;align-items:center;padding:10px}.empty-state label{color:#757575;font-style:italic}.items-container{display:flex;flex-wrap:wrap;gap:10px;padding:8px 0;align-items:center}.item-button{cursor:pointer;font-weight:400;letter-spacing:.01em;transition:box-shadow .2s ease,background-color .2s ease}.item-button:hover{box-shadow:0 1px 4px #00000026;background-color:#00000005}.item-icon{font-size:18px;width:18px;height:18px;margin-right:4px;vertical-align:middle}.item-additional-icon{font-size:16px;width:16px;height:16px;margin-left:6px;vertical-align:middle;opacity:.85}button[mat-icon-button]{width:32px;height:32px}button[mat-icon-button] mat-icon{font-size:18px;margin-top:-3px}.header-right button[mat-icon-button]{display:inline-flex;align-items:center;justify-content:center;padding:0;line-height:normal}.header-right button[mat-icon-button] mat-icon{margin:0;font-size:20px;width:20px;height:20px;line-height:20px;display:block}.header-right button[mat-icon-button] .mat-mdc-button-touch-target{display:none}.drop-list{min-height:40px}.drop-empty{padding:5px 10px}.cdk-drag-preview{box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;opacity:.9;border-radius:4px}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.drop-highlight{border:2px dashed #90caf9;border-radius:8px;background:#90caf90d}.cdk-drop-list-dragging .item-button:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$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: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i4$4.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i4$4.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i4$4.MatMenuContent, selector: "ng-template[matMenuContent]" }, { kind: "directive", type: i4$4.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }, { kind: "directive", type: i3$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4$2.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: i15.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i15.CdkDropListGroup, selector: "[cdkDropListGroup]", inputs: ["cdkDropListGroupDisabled"], exportAs: ["cdkDropListGroup"] }, { kind: "directive", type: i15.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "pipe", type: i1$2.TitleCasePipe, name: "titlecase" }] }); }
16162
16284
  }
16163
16285
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: GroupsComponent, decorators: [{
16164
16286
  type: Component,
16165
16287
  args: [{ selector: 'spa-groups', standalone: false, template: "<!-- Changed: this filter row was REMOVED from the grouped view's own markup and now lives in the table header,\n beside the action buttons, where every other table config puts it. It used to sit on a row of its own\n directly under the buttons row, leaving both rows half empty and visibly misaligned.\n The filtering logic here is untouched \u2014 only the input that drives it moved, and its text arrives via the\n filterText @Input relayed from the header. Kept as a fallback for a grouped view rendered WITHOUT the\n standard header (spa-groups can be used directly), so filtering is never simply lost. -->\n<div class=\"groups-filter\" *ngIf=\"showOwnFilter\">\n <mat-form-field appearance=\"outline\" class=\"filter-field\">\n <mat-icon matPrefix>search</mat-icon>\n <input matInput placeholder=\"Filter\" [(ngModel)]=\"filterText\" (ngModelChange)=\"applyFilter()\">\n <button *ngIf=\"filterText\" mat-icon-button matSuffix (click)=\"filterText = ''; applyFilter()\">\n <mat-icon>close</mat-icon>\n </button>\n </mat-form-field>\n</div>\n\n<div class=\"groups-container\" cdkDropListGroup>\n\n <mat-card *ngFor=\"let group of groupedData\" class=\"group-card\">\n\n <div class=\"group-header\">\n <div class=\"header-left\">\n <mat-icon *ngIf=\"group.icon\" [style.color]=\"group.color\" class=\"group-icon\">{{group.icon}}</mat-icon>\n <label class=\"group-title\">{{group.displayName}}</label>\n <label *ngIf=\"config.groupConfig.showGroupCount !== false\" class=\"group-count\">({{group.items.length}})</label>\n </div>\n\n <div class=\"header-right\">\n <button\n *ngFor=\"let button of getVisibleHeaderButtons(group)\"\n mat-icon-button\n [matTooltip]=\"getHeaderButtonTooltip(button)\"\n matTooltipPosition=\"above\"\n [style.color]=\"getHeaderButtonColor(button, group)\"\n [disabled]=\"isHeaderButtonDisabled(button, group)\"\n (click)=\"headerButtonClicked(button, group)\">\n <mat-icon>{{getHeaderButtonIcon(button)}}</mat-icon>\n </button>\n </div>\n </div>\n\n <hr class=\"group-divider\" />\n\n <!-- Empty state: show when no items and drag is disabled -->\n <div *ngIf=\"group.items.length === 0 && !config.groupConfig.dragEnabled\" class=\"empty-state\">\n <label>{{config.groupConfig.emptyGroupMessage ?? 'Empty'}}</label>\n </div>\n\n <!-- Changed: Replaced mat-chip-set with flex container of mat-stroked-button for cleaner button style -->\n <div\n *ngIf=\"group.items.length > 0 || config.groupConfig.dragEnabled\"\n cdkDropList\n [cdkDropListData]=\"group.items\"\n [cdkDropListDisabled]=\"!config.groupConfig.dragEnabled\"\n (cdkDropListDropped)=\"onDrop($event)\"\n (cdkDropListEntered)=\"onDropListEntered($event)\"\n (cdkDropListExited)=\"onDropListExited($event)\"\n [class.drop-highlight]=\"group === highlightedGroup\"\n class=\"drop-list items-container\">\n\n <!-- Empty state inside drop list when drag is enabled -->\n <div *ngIf=\"group.items.length === 0\" class=\"empty-state drop-empty\">\n <label>{{config.groupConfig.emptyGroupMessage ?? 'Empty'}}</label>\n </div>\n\n <button\n mat-stroked-button\n *ngFor=\"let item of group.items\"\n cdkDrag [cdkDragData]=\"item\"\n [cdkDragDisabled]=\"!config.groupConfig.dragEnabled\"\n [matMenuTriggerFor]=\"config.groupConfig.contextMenuEnabled !== false ? itemMenu : null\"\n [matMenuTriggerData]=\"{item: item, group: group}\"\n class=\"item-button\">\n\n <mat-icon\n *ngIf=\"getItemIcon(item)\"\n [style.color]=\"getItemIconColor(item)\"\n [matTooltip]=\"getItemIconTip(item)\"\n matTooltipPosition=\"above\"\n class=\"item-icon\">\n {{getItemIcon(item)}}\n </mat-icon>\n\n {{getItemText(item)}}\n\n <ng-container *ngFor=\"let additionalIcon of getVisibleAdditionalIcons(item)\">\n <mat-icon\n [style.color]=\"additionalIcon.color\"\n [matTooltip]=\"getIconTooltip(additionalIcon, item)\"\n matTooltipPosition=\"above\"\n class=\"item-additional-icon\">\n {{additionalIcon.name}}\n </mat-icon>\n </ng-container>\n\n </button>\n </div>\n\n </mat-card>\n\n</div>\n\n<mat-menu #itemMenu=\"matMenu\">\n <ng-template matMenuContent let-item=\"item\" let-group=\"group\">\n <button\n *ngFor=\"let button of getVisibleButtons(item)\"\n mat-menu-item\n [disabled]=\"isButtonDisabled(button, item)\"\n (click)=\"itemActionClicked(button.name, item, group)\">\n <mat-icon [style.color]=\"getButtonIconColor(button, item)\">{{getButtonIcon(button)}}</mat-icon>\n <span>{{button.display ?? button.tip ?? (button.name | titlecase)}}</span>\n </button>\n </ng-template>\n</mat-menu>\n", styles: [".groups-filter{display:flex;justify-content:flex-end;margin-bottom:8px;padding-right:10px}.filter-field{width:300px;font-size:13px}.filter-field ::ng-deep .mat-mdc-form-field-infix{padding-top:8px!important;padding-bottom:8px!important;min-height:36px}.groups-container{display:flex;flex-direction:column;gap:20px;padding-right:10px;margin-left:0}.group-card{width:100%;margin-bottom:0;padding:8px 16px 16px}.group-header{display:flex;justify-content:space-between;align-items:center;padding:0;min-height:32px}.header-left{display:flex;align-items:center;gap:5px}.header-right{display:flex;align-items:center;gap:4px}.group-icon{margin-right:5px;font-size:24px;width:24px;height:24px;line-height:24px}.group-title{font-size:24px;font-weight:300;line-height:1.2}.group-count{font-size:12px;font-weight:300;margin-left:5px;line-height:1.2}.group-divider{margin-top:8px;margin-bottom:10px}.empty-state{display:flex;justify-content:center;align-items:center;padding:10px}.empty-state label{color:#757575;font-style:italic}.items-container{display:flex;flex-wrap:wrap;gap:10px;padding:8px 0;align-items:center}.item-button{cursor:pointer;font-weight:400;letter-spacing:.01em;transition:box-shadow .2s ease,background-color .2s ease}.item-button:hover{box-shadow:0 1px 4px #00000026;background-color:#00000005}.item-icon{font-size:18px;width:18px;height:18px;margin-right:4px;vertical-align:middle}.item-additional-icon{font-size:16px;width:16px;height:16px;margin-left:6px;vertical-align:middle;opacity:.85}button[mat-icon-button]{width:32px;height:32px}button[mat-icon-button] mat-icon{font-size:18px;margin-top:-3px}.header-right button[mat-icon-button]{display:inline-flex;align-items:center;justify-content:center;padding:0;line-height:normal}.header-right button[mat-icon-button] mat-icon{margin:0;font-size:20px;width:20px;height:20px;line-height:20px;display:block}.header-right button[mat-icon-button] .mat-mdc-button-touch-target{display:none}.drop-list{min-height:40px}.drop-empty{padding:5px 10px}.cdk-drag-preview{box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;opacity:.9;border-radius:4px}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.drop-highlight{border:2px dashed #90caf9;border-radius:8px;background:#90caf90d}.cdk-drop-list-dragging .item-button:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}\n"] }]
16166
- }], ctorParameters: () => [{ type: ConditionService }, { type: ButtonService }, { type: DataServiceLib }, { type: i2$1.MatSnackBar }], propDecorators: { config: [{
16288
+ }], ctorParameters: () => [{ type: ConditionService }, { type: ButtonService }, { type: DataServiceLib }, { type: i2$1.MatSnackBar }, { type: ApiErrorService }], propDecorators: { config: [{
16167
16289
  type: Input
16168
16290
  }], dataSource: [{
16169
16291
  type: Input
@@ -16333,9 +16455,26 @@ class TableComponent {
16333
16455
  }
16334
16456
  sectionChips() { return this.sectionChipList; } // Changed: returns the materialized field
16335
16457
  sectionButtons() { return this.sectionButtonList; } // Changed: returns the materialized field
16458
+ // Changed: a section-level button used to be routed straight at customModel() -> open(), which never reads
16459
+ // onClick and, for a non-dialog button, hands back { action:'doAction' } — and doAction then re-resolved the
16460
+ // name against config.buttons, where a sectionConfig button has never lived. So a section button that was not
16461
+ // also a row button silently did nothing, which is why two apps had to drop their batch actions. It now
16462
+ // follows the same three-way shape actionClicked already uses: onClick first, then a direct action, then the
16463
+ // dialog. Row buttons are untouched — this method is only ever called from the section header.
16336
16464
  sectionButtonClicked(button, event) {
16337
16465
  event.stopPropagation(); // header click collapses — the button must not
16338
- this.customModel(button.name, null, button); // route through the standard button pipeline (dialogs, actions, confirms)
16466
+ // A section button acts on the whole list, so the rows ARE its data there is no row to hand it
16467
+ const rows = this.dataSourceValue || [];
16468
+ if (button?.onClick) {
16469
+ button.onClick(rows);
16470
+ if (!button.dialog && !button.action && !button.detailsConfig)
16471
+ return; // a pure onClick button is fully handled
16472
+ }
16473
+ if (button?.action && !button.dialog && !button.detailsConfig) {
16474
+ this.doAction(button.name, rows, button); // pass the button itself — it is not in config.buttons to be found by name
16475
+ return;
16476
+ }
16477
+ this.customModel(button.name, null, button); // dialog / detailsConfig button — opens empty, as it always did
16339
16478
  }
16340
16479
  get dataSource() { return this.dataSourceValue; } // returns the stored reference unchanged — safe in templates
16341
16480
  set dataSource(rows) {
@@ -16575,7 +16714,8 @@ class TableComponent {
16575
16714
  else {
16576
16715
  this.apiErrorService.presentAppFailure(apiResponse, 'submit', editButton.action.url); // Changed: inline edit IS a write — opaque server text now raises the friendly "Not saved" dialog instead of a 5s toast the user misses before retrying
16577
16716
  }
16578
- }, (error) => { this.messageService.toast("Error: " + error); });
16717
+ }, (error) => { this.apiErrorService.present(error); } // Changed: transport failure on inline edit — was toast("Error: " + error), the last raw-text survivor on this path; present() classifies, dedupes and never shows raw error text
16718
+ );
16579
16719
  }
16580
16720
  updateTableConfiguration() {
16581
16721
  this.elevation = this.getElevationClass(this.config?.elevation);
@@ -17146,13 +17286,13 @@ class TableComponent {
17146
17286
  });
17147
17287
  }
17148
17288
  //Actions
17149
- doAction(buttonName, row) {
17150
- let b = this.getButton(buttonName);
17289
+ doAction(buttonName, row, button) {
17290
+ let b = button || this.getButton(buttonName);
17151
17291
  if (!b)
17152
17292
  return;
17153
17293
  if (!b.action)
17154
17294
  return;
17155
- if (b.setHeroField && this.config.heroValue) {
17295
+ if (b.setHeroField && row && this.config.heroValue) { // Changed: a section-level action has an array (or nothing) for a row — never index into it
17156
17296
  row[this.config.heroField] = this.config.heroValue;
17157
17297
  }
17158
17298
  if (b.confirm && b.name != "delete") {
@@ -17190,7 +17330,7 @@ class TableComponent {
17190
17330
  }
17191
17331
  this.actionResponse.emit({ name: button.name, data: apiResponse });
17192
17332
  }, (error) => {
17193
- this.messageService.toast("Error: " + error);
17333
+ this.apiErrorService.present(error); // Changed: transport failure on a row action — was toast("Error: " + error); present() classifies, dedupes with other transport failures and never shows raw error text
17194
17334
  });
17195
17335
  }
17196
17336
  //Misc
@@ -17467,11 +17607,11 @@ class TableComponent {
17467
17607
  this.setPaginator();
17468
17608
  }
17469
17609
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableComponent, deps: [{ token: DataServiceLib }, { token: MessageService }, { token: i1$4.BreakpointObserver }, { token: i4.MatDialog }, { token: ButtonService }, { token: DialogService }, { token: TableConfigService }, { token: ConditionService }, { token: AuthService }, { token: SignalRService }, { token: OfflineService }, { token: ApiErrorService }, { token: TIN_SPA_RUNTIME_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
17470
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TableComponent, isStandalone: false, selector: "spa-table", inputs: { data: "data", tileData: "tileData", config: "config", localMode: "localMode", parentDetails: "parentDetails", reload: "reload", activeTab: "activeTab", inTab: "inTab", nestingLevel: "nestingLevel" }, outputs: { dataLoad: "dataLoad", totalChange: "totalChange", actionSuccess: "actionSuccess", refreshClick: "refreshClick", searchClick: "searchClick", createClick: "createClick", actionClick: "actionClick", inputChange: "inputChange", actionResponse: "actionResponse" }, viewQueries: [{ propertyName: "tablePaginator", first: true, predicate: ["tablePaginator"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "\n<ng-container *ngIf=\"hasFormAccess && !sectionHidden\"> <!-- Changed: sectionConfig.hideWhenEmpty hides the whole table -->\n\n <!-- Added: collapsible flat section header (sectionConfig) \u2014 1px border, no elevation, whole row toggles -->\n <div class=\"tbl-section-header\" *ngIf=\"config.sectionConfig\" (click)=\"toggleSection()\" [attr.aria-expanded]=\"!sectionCollapsed\" [class.tbl-section-static]=\"config.sectionConfig.collapsible === false\" role=\"button\" tabindex=\"0\" (keydown.enter)=\"toggleSection()\">\n <mat-icon class=\"tbl-section-icon\" *ngIf=\"config.sectionConfig.icon\">{{ config.sectionConfig.icon }}</mat-icon>\n <span class=\"tbl-section-title\">{{ config.sectionConfig.title }}</span>\n <span class=\"tbl-section-count\" *ngIf=\"config.sectionConfig.showCount !== false\">{{ dataSource?.length || 0 }}</span>\n <span class=\"tbl-section-chip\" *ngFor=\"let chip of sectionChips()\" [style.color]=\"chip.color\">{{ chip.text }}</span>\n <span class=\"tbl-section-spacer\"></span>\n <button mat-stroked-button color=\"primary\" *ngFor=\"let btn of sectionButtons()\" (click)=\"sectionButtonClicked(btn, $event)\"><mat-icon *ngIf=\"btn.icon?.name\">{{ btn.icon.name }}</mat-icon>{{ btn.display || btn.name }}</button>\n <mat-icon class=\"tbl-section-chevron\" *ngIf=\"config.sectionConfig.collapsible !== false\">{{ sectionCollapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </div>\n\n <!-- Added: collapsed section affordance \u2014 mirrors the Day Book \"Show the N\" pattern -->\n <div class=\"tbl-section-more\" *ngIf=\"config.sectionConfig && sectionCollapsed && (dataSource?.length || 0) > 0\">\n <button type=\"button\" class=\"tbl-section-link\" (click)=\"toggleSection()\">Show the {{ dataSource.length }}</button>\n </div>\n\n <ng-container *ngIf=\"!config.sectionConfig || !sectionCollapsed\"> <!-- Added: section collapse hides the table body -->\n\n <!-- Search -->\n <spa-search\n *ngIf=\"config.searchConfig\" [config]=\"config.searchConfig\" [smallScreen]=\"smallScreen\" [tableDataSource]=\"tableDataSource\" style=\"margin-bottom: 20px;\" (searchClick)=\"searchClicked($event)\">\n </spa-search>\n\n <!-- Header -->\n <app-table-header\n [config]=\"config\" [data]=\"dataSource\" [tableDataSource]=\"tableDataSource\" [tileConfig]=\"config.tileConfig\" [tileData]=\"tileData\" [tileReload]=\"tileReload\" [lastSearch]=\"lastSearch\" [smallScreen]=\"smallScreen\"\n [showFilterButton]=\"showFilterButton\" [isRealTime]=\"config.realTime\" [isConnected]=\"isSignalRConnected\" [refreshing]=\"loadingStage === 'refresh'\"\n (createClick)=\"newModel()\" (customClick)=\"customModel($event,null)\"\n (refreshClick)=\"refreshClicked()\" (tileClick)=\"tileClicked($event)\" (tileUnClick)=\"tileUnClicked($event)\" (filterChange)=\"filterChanged($event)\">\n </app-table-header>\n\n <!-- Added (Quiet Loading D4): refresh with data already on screen \u2014 a 2px line flush under the header and the\n spinning refresh icon are the ONLY signals. Rows stay live, clickable and un-dimmed while they swap. -->\n <div *ngIf=\"loadingStage === 'refresh'\" class=\"tin-load-line\" aria-hidden=\"true\">\n <span class=\"tin-load-line-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n\n <!-- Added: paged-mode filter hint \u2014 the client filter only covers rows loaded so far -->\n <div *ngIf=\"pagedMode && filterActive && loadedRows.length < serverTotal\" class=\"paged-filter-hint\">\n <mat-icon>info</mat-icon>\n <span>Filtering only the {{loadedRows.length}} loaded rows of {{serverTotal}}. {{ config.searchConfig ? 'Use Search for complete results.' : 'Refine with search for complete results.' }}</span>\n </div>\n\n\n <!-- Table -->\n <div *ngIf=\"!config.viewType || config?.viewType === 'table'\">\n\n <p *ngIf=\"!config\"><em>Configure Table</em></p>\n <p *ngIf=\"!dataSource && !loadingStage\"><em>Loading...</em></p> <!-- Changed (Quiet Loading): the bare text is replaced by the stage below while a quiet load is on screen -->\n\n <!-- Added (Quiet Loading D3): first load, nothing on screen yet. The progress module is the hero (eased bar +\n counting percentage + caption) and the ghost rows hold the exact space the real rows will fill, so the\n table does not jump when data lands. Only ever rendered when quiet loading is on. -->\n <div *ngIf=\"loadingStage === 'initial'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div class=\"tin-load-ghosts\">\n <div class=\"tin-load-ghost-row\" *ngFor=\"let r of ghostRows\">\n <div class=\"tin-load-ghost-cell\" *ngFor=\"let c of ghostColumns; let i = index\">\n <span class=\"tin-skel\" [style.width.%]=\"ghostWidth(i, c)\" [style.animation-delay.ms]=\"r * 120\"></span> <!-- staggered sweep: each row starts 120ms after the one above -->\n </div>\n </div>\n </div>\n\n </div>\n\n <div *ngIf=\"dataSource && loadingStage !== 'initial' && (!smallScreen || (smallScreen && dataSource?.length > 0))\" [class.tin-load-in]=\"effQuietLoading\"> <!-- Changed (Quiet Loading): the empty header-only table is suppressed while the initial stage stands in for it, and the real rows fade in where the ghosts were (D3) -->\n\n <table mat-table [dataSource]=\"tableDataSource\" [trackBy]=\"trackByRow\" [ngClass]=\"elevation\" [class.tin-no-col-headers]=\"config.hideColumnHeaders\"> <!-- Changed: optional column-header suppression -->\n\n <ng-container *ngFor=\"let column of config.columns\" [matColumnDef]=\"column.name\">\n <th mat-header-cell *matHeaderCellDef >{{ column.alias ?? column.name | camelToWords }}</th>\n <td mat-cell *matCellDef=\"let row;\" class=\"right-padding\" >\n\n <!-- Added: inline edit \u2014 editable cells swap to their form-field editor while the row is in edit mode -->\n <app-inline-cell *ngIf=\"isRowEditing(row) && getInlineField(column); else displayCell\" [field]=\"getInlineField(column)\" [data]=\"editingModel\"></app-inline-cell>\n\n <!-- Rows -->\n <ng-template #displayCell>\n <app-table-row [column]=\"column\" [row]=\"row\" [config]=\"config\" [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked(column.name, row)\" (columnClick)=\"columnClicked(column, row)\" (showBannerEvent)=\"showBanner($event)\">\n </app-table-row>\n </ng-template>\n\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"action\">\n <th mat-header-cell *matHeaderCellDef> Action </th>\n <td mat-cell *matCellDef=\"let row\" [ngStyle]=\"{width:false ? '20px' : actionsWidth}\">\n <div class=\"action-buttons-container\">\n\n <!-- Added: inline edit \u2014 while a row edits in place, its actions collapse to submit/cancel -->\n <ng-container *ngIf=\"isRowEditing(row); else rowActions\">\n <button mat-icon-button matTooltip=\"Save\" matTooltipPosition=\"above\" (click)=\"submitInlineEdit()\"><mat-icon class=\"inline-save\">check</mat-icon></button> <!-- Changed: dropped color=\"primary\" \u2014 the icon now carries a green save cue -->\n <button mat-icon-button matTooltip=\"Cancel\" matTooltipPosition=\"above\" (click)=\"cancelInlineEdit()\"><mat-icon class=\"inline-cancel\">close</mat-icon></button> <!-- Changed: red cancel cue -->\n </ng-container>\n\n <!-- Actions -->\n <ng-template #rowActions>\n <app-table-action\n [displayedButtons]=\"displayedButtons\" [config]=\"config\" [smallScreen]=\"smallScreen\" [row]=\"row\" (actionClick)=\"actionClicked($event.name, $event.row)\">\n </app-table-action>\n </ng-template>\n\n </div>\n </td>\n </ng-container>\n\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\" [ngClass]=\"{'make-gray': (config.greyOut && config.greyOut(row)) || row.pendingApproval, 'row-editing': isRowEditing(row)}\"></tr> <!-- Changed: row-editing flags the row that is open for inline edit -->\n </table>\n\n </div>\n\n <!-- Changed: Removed *ngIf condition to keep paginator always in DOM and maintain ViewChild reference -->\n <!-- Changed: Added CSS class binding to hide when no data instead of conditional rendering -->\n <!-- Changed: Legacy paginator only renders in non-paged mode (pagedMode is constant per instance, set before first render) -->\n <mat-paginator *ngIf=\"!pagedMode\"\n #tablePaginator\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial'}\"\n showFirstLastButtons>\n </mat-paginator>\n\n <!-- Added: manual paginator for server-side paged mode \u2014 fully state-bound, never attached to MatTableDataSource. Always visible: when filtering it pages the in-memory filtered subset (length = filtered count); otherwise the server window (length = true total). No first/last jump (would force fetching the whole gap). -->\n <mat-paginator *ngIf=\"pagedMode\"\n [length]=\"filterActive ? filteredRows.length : serverTotal + overlayDelta\"\n [pageIndex]=\"pageIndex\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial'}\"\n (page)=\"onServerPage($event)\">\n </mat-paginator>\n\n </div>\n \n <!-- Added (Quiet Loading): the initial-load stage for the NON-table views.\n Without this, card/capsule/grouped showed literally nothing during a first load \u2014 the view component\n renders an empty dataSource and the \"No Data\" line is suppressed while the stage owns the space.\n The progress header is identical to the table's so the two feel like one feature; only the ghost\n furniture differs, because column-shaped rows are wrong in a card grid. -->\n <div *ngIf=\"loadingStage === 'initial' && config?.viewType && config?.viewType !== 'table'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div *ngIf=\"config?.viewType === 'capsule'\" class=\"tin-load-ghost-capsules\" aria-hidden=\"true\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let c of ghostCapsules\"></span>\n </div>\n\n <div *ngIf=\"config?.viewType === 'card'\" class=\"tin-load-ghost-cards\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-card\" *ngFor=\"let c of ghostCards\">\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-title\"></span>\n <span class=\"tin-skel tin-skel-text\"></span>\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-short\"></span>\n </div>\n </div>\n\n <div *ngIf=\"config?.viewType === 'grouped'\" class=\"tin-load-ghost-groups\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-group\" *ngFor=\"let g of ghostGroups\">\n <span class=\"tin-skel tin-load-ghost-group-head\"></span>\n <!-- Pills, not rows: a group card's body is a wrap of chips, so full-width bars promised a table\n and the stage did not resemble what replaced it. -->\n <div class=\"tin-load-ghost-group-items\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let r of ghostCards\"></span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Capsules -->\n <spa-capsules *ngIf=\"config?.viewType === 'capsule' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n (actionClick)=\"actionClicked($event.name, $event.row)\">\n </spa-capsules>\n\n\n <!-- Cards -->\n <spa-cards *ngIf=\"config?.viewType === 'card' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked($event.name, $event.row)\"\n (columnClick)=\"columnClicked($event.column, $event.row)\"\n (showBannerEvent)=\"showBanner($event)\">\n </spa-cards>\n\n <!-- Groups - Added: New grouped view type -->\n <!-- Changed: the grouped view no longer renders its own filter field. It used to sit on a row of its own\n beneath the buttons row, which left both rows half empty and, more importantly, was a SECOND filter with\n no refresh button. The standard header filter (which has refresh, like every other table) now drives it,\n with its text relayed in through filterText. -->\n <spa-groups *ngIf=\"config?.viewType === 'grouped' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [showOwnFilter]=\"false\"\n [filterText]=\"groupFilterText\"\n (actionClick)=\"actionClicked($event.name, $event.row, $event.group, $event.button)\">\n </spa-groups>\n\n\n <div class=\"tin-center\">\n <p *ngIf=\"dataSource?.length == 0 && loadingStage !== 'initial'\"><em>{{config.noDataMessage ?? 'No Data'}}</em></p> <!-- Changed (Quiet Loading): the stage owns the space until it completes, then hands straight over to this message \u2014 no \"No Data\" flashing underneath the ghost rows -->\n </div>\n\n </ng-container> <!-- Added: end section-collapse wrapper -->\n\n</ng-container>\n\n\n<ng-container *ngIf=\"!hasFormAccess\">\n <div class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n</ng-container>\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}.dlg-header-classic{padding-left:24px;padding-right:24px}.dlg-title-classic{font-size:20px;font-weight:500;margin-top:10px;margin-bottom:5px}.dialog-header-titles{display:flex;flex-direction:column;justify-content:center;min-width:0}.tin-dlg-head .dialog-header-titles label{margin:0}.tbl-section-header{display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:8px;border:1px solid rgba(0,0,0,.08);border-radius:10px;background:transparent;cursor:pointer;transition:border-color .15s}.tbl-section-header:hover{border-color:#90a4ae}.tbl-section-header.tbl-section-static{cursor:default}.tbl-section-icon{color:#546e7a}.tbl-section-title{font-size:14px;font-weight:600;color:#000000d1}.tbl-section-count{background:#e3f2fd;color:#1565c0;border-radius:12px;padding:2px 10px;font-size:12px}.tbl-section-chip{font-size:12px;font-weight:500;color:#0009}.tbl-section-spacer{margin-left:auto}.tbl-section-chevron{color:#90a4ae}.tbl-section-more{padding:0 12px 8px}.tbl-section-link{background:none;border:none;color:#1565c0;cursor:pointer;font-size:13px;padding:0}.tbl-section-link:hover{text-decoration:underline}table.tin-no-col-headers tr.mat-mdc-header-row{display:none}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i14$1.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i14$1.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i14$1.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i14$1.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i14$1.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i14$1.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i14$1.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i14$1.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i14$1.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i14$1.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i15.MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SearchComponent, selector: "spa-search", inputs: ["config", "smallScreen", "tableDataSource"], outputs: ["searchClick"] }, { kind: "component", type: TableHeaderComponent, selector: "app-table-header", inputs: ["lastSearch", "config", "hideTitle", "tableDataSource", "tileConfig", "smallScreen", "tileReload", "showFilterButton", "data", "tileData", "isRealTime", "isConnected", "refreshing"], outputs: ["createClick", "customClick", "refreshClick", "tileClick", "tileUnClick", "filterChange"] }, { kind: "component", type: TableRowComponent, selector: "app-table-row", inputs: ["column", "row", "config", "smallScreen"], outputs: ["actionClick", "columnClick", "showBannerEvent"] }, { kind: "component", type: TableActionComponent, selector: "app-table-action", inputs: ["displayedButtons", "config", "row", "smallScreen"], outputs: ["actionClick"] }, { kind: "component", type: InlineCellComponent, selector: "app-inline-cell", inputs: ["field", "data"], outputs: ["valueChange"] }, { kind: "component", type: CapsulesComponent, selector: "spa-capsules", inputs: ["config", "dataSource", "displayedButtons"], outputs: ["actionClick"] }, { kind: "component", type: CardsComponent, selector: "spa-cards", inputs: ["config", "dataSource", "displayedButtons", "smallScreen"], outputs: ["actionClick", "columnClick", "showBannerEvent"] }, { kind: "component", type: GroupsComponent, selector: "spa-groups", inputs: ["config", "dataSource", "displayedButtons", "showOwnFilter", "filterText"], outputs: ["actionClick"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
17610
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TableComponent, isStandalone: false, selector: "spa-table", inputs: { data: "data", tileData: "tileData", config: "config", localMode: "localMode", parentDetails: "parentDetails", reload: "reload", activeTab: "activeTab", inTab: "inTab", nestingLevel: "nestingLevel" }, outputs: { dataLoad: "dataLoad", totalChange: "totalChange", actionSuccess: "actionSuccess", refreshClick: "refreshClick", searchClick: "searchClick", createClick: "createClick", actionClick: "actionClick", inputChange: "inputChange", actionResponse: "actionResponse" }, viewQueries: [{ propertyName: "tablePaginator", first: true, predicate: ["tablePaginator"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "\n<ng-container *ngIf=\"hasFormAccess && !sectionHidden\"> <!-- Changed: sectionConfig.hideWhenEmpty hides the whole table -->\n\n <!-- Added: collapsible flat section header (sectionConfig) \u2014 1px border, no elevation, whole row toggles -->\n <div class=\"tbl-section-header\" *ngIf=\"config.sectionConfig\" (click)=\"toggleSection()\" [attr.aria-expanded]=\"!sectionCollapsed\" [class.tbl-section-static]=\"config.sectionConfig.collapsible === false\" role=\"button\" tabindex=\"0\" (keydown.enter)=\"toggleSection()\">\n <mat-icon class=\"tbl-section-icon\" *ngIf=\"config.sectionConfig.icon\">{{ config.sectionConfig.icon }}</mat-icon>\n <span class=\"tbl-section-title\">{{ config.sectionConfig.title }}</span>\n <span class=\"tbl-section-count\" *ngIf=\"config.sectionConfig.showCount !== false\">{{ dataSource?.length || 0 }}</span>\n <span class=\"tbl-section-chip\" *ngFor=\"let chip of sectionChips()\" [style.color]=\"chip.color\">{{ chip.text }}</span>\n <span class=\"tbl-section-spacer\"></span>\n <!-- Changed: the label is wrapped so a NARROW screen can drop it and leave an icon-only button. Only a\n button that HAS an icon loses its text \u2014 otherwise it would collapse to a blank square. -->\n <button mat-stroked-button color=\"primary\" *ngFor=\"let btn of sectionButtons()\" (click)=\"sectionButtonClicked(btn, $event)\" [matTooltip]=\"btn.display || btn.name\"><mat-icon *ngIf=\"btn.icon?.name\">{{ btn.icon.name }}</mat-icon><span class=\"tbl-section-btn-text\" [class.has-icon]=\"!!btn.icon?.name\">{{ btn.display || btn.name }}</span></button>\n <mat-icon class=\"tbl-section-chevron\" *ngIf=\"config.sectionConfig.collapsible !== false\">{{ sectionCollapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </div>\n\n <!-- Added: the section's \"why this list exists\" line. It sits UNDER the title (a reason only makes sense once\n the list has been named) and only while the section is open \u2014 a shut section already has its own summary\n line, and a paragraph over the top of that is noise. -->\n <p class=\"tbl-section-caption\" *ngIf=\"config.sectionConfig?.caption && !sectionCollapsed\">{{ config.sectionConfig.caption }}</p>\n\n <!-- Added: collapsed section affordance \u2014 mirrors the Day Book \"Show the N\" pattern -->\n <div class=\"tbl-section-more\" *ngIf=\"config.sectionConfig && sectionCollapsed && (dataSource?.length || 0) > 0\">\n <button type=\"button\" class=\"tbl-section-link\" (click)=\"toggleSection()\">Show the {{ dataSource.length }}</button>\n </div>\n\n <ng-container *ngIf=\"!config.sectionConfig || !sectionCollapsed\"> <!-- Added: section collapse hides the table body -->\n\n <!-- Search -->\n <spa-search\n *ngIf=\"config.searchConfig\" [config]=\"config.searchConfig\" [smallScreen]=\"smallScreen\" [tableDataSource]=\"tableDataSource\" style=\"margin-bottom: 20px;\" (searchClick)=\"searchClicked($event)\">\n </spa-search>\n\n <!-- Header -->\n <app-table-header\n [config]=\"config\" [data]=\"dataSource\" [tableDataSource]=\"tableDataSource\" [tileConfig]=\"config.tileConfig\" [tileData]=\"tileData\" [tileReload]=\"tileReload\" [lastSearch]=\"lastSearch\" [smallScreen]=\"smallScreen\"\n [showFilterButton]=\"showFilterButton\" [isRealTime]=\"config.realTime\" [isConnected]=\"isSignalRConnected\" [refreshing]=\"loadingStage === 'refresh'\"\n (createClick)=\"newModel()\" (customClick)=\"customModel($event,null)\"\n (refreshClick)=\"refreshClicked()\" (tileClick)=\"tileClicked($event)\" (tileUnClick)=\"tileUnClicked($event)\" (filterChange)=\"filterChanged($event)\">\n </app-table-header>\n\n <!-- Added (Quiet Loading D4): refresh with data already on screen \u2014 a 2px line flush under the header and the\n spinning refresh icon are the ONLY signals. Rows stay live, clickable and un-dimmed while they swap. -->\n <div *ngIf=\"loadingStage === 'refresh'\" class=\"tin-load-line\" aria-hidden=\"true\">\n <span class=\"tin-load-line-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n\n <!-- Added: paged-mode filter hint \u2014 the client filter only covers rows loaded so far -->\n <div *ngIf=\"pagedMode && filterActive && loadedRows.length < serverTotal\" class=\"paged-filter-hint\">\n <mat-icon>info</mat-icon>\n <span>Filtering only the {{loadedRows.length}} loaded rows of {{serverTotal}}. {{ config.searchConfig ? 'Use Search for complete results.' : 'Refine with search for complete results.' }}</span>\n </div>\n\n\n <!-- Table -->\n <div *ngIf=\"!config.viewType || config?.viewType === 'table'\">\n\n <p *ngIf=\"!config\"><em>Configure Table</em></p>\n <p *ngIf=\"!dataSource && !loadingStage\"><em>Loading...</em></p> <!-- Changed (Quiet Loading): the bare text is replaced by the stage below while a quiet load is on screen -->\n\n <!-- Added (Quiet Loading D3): first load, nothing on screen yet. The progress module is the hero (eased bar +\n counting percentage + caption) and the ghost rows hold the exact space the real rows will fill, so the\n table does not jump when data lands. Only ever rendered when quiet loading is on. -->\n <div *ngIf=\"loadingStage === 'initial'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div class=\"tin-load-ghosts\">\n <div class=\"tin-load-ghost-row\" *ngFor=\"let r of ghostRows\">\n <div class=\"tin-load-ghost-cell\" *ngFor=\"let c of ghostColumns; let i = index\">\n <span class=\"tin-skel\" [style.width.%]=\"ghostWidth(i, c)\" [style.animation-delay.ms]=\"r * 120\"></span> <!-- staggered sweep: each row starts 120ms after the one above -->\n </div>\n </div>\n </div>\n\n </div>\n\n <div *ngIf=\"dataSource && loadingStage !== 'initial' && (!smallScreen || (smallScreen && dataSource?.length > 0))\" [class.tin-load-in]=\"effQuietLoading\"> <!-- Changed (Quiet Loading): the empty header-only table is suppressed while the initial stage stands in for it, and the real rows fade in where the ghosts were (D3) -->\n\n <table mat-table [dataSource]=\"tableDataSource\" [trackBy]=\"trackByRow\" [ngClass]=\"elevation\" [class.tin-no-col-headers]=\"config.hideColumnHeaders\"> <!-- Changed: optional column-header suppression -->\n\n <ng-container *ngFor=\"let column of config.columns\" [matColumnDef]=\"column.name\">\n <th mat-header-cell *matHeaderCellDef >{{ column.alias ?? column.name | camelToWords }}</th>\n <td mat-cell *matCellDef=\"let row;\" class=\"right-padding\" >\n\n <!-- Added: inline edit \u2014 editable cells swap to their form-field editor while the row is in edit mode -->\n <app-inline-cell *ngIf=\"isRowEditing(row) && getInlineField(column); else displayCell\" [field]=\"getInlineField(column)\" [data]=\"editingModel\"></app-inline-cell>\n\n <!-- Rows -->\n <ng-template #displayCell>\n <app-table-row [column]=\"column\" [row]=\"row\" [config]=\"config\" [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked(column.name, row)\" (columnClick)=\"columnClicked(column, row)\" (showBannerEvent)=\"showBanner($event)\">\n </app-table-row>\n </ng-template>\n\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"action\">\n <th mat-header-cell *matHeaderCellDef> Action </th>\n <td mat-cell *matCellDef=\"let row\" [ngStyle]=\"{width:false ? '20px' : actionsWidth}\">\n <div class=\"action-buttons-container\">\n\n <!-- Added: inline edit \u2014 while a row edits in place, its actions collapse to submit/cancel -->\n <ng-container *ngIf=\"isRowEditing(row); else rowActions\">\n <button mat-icon-button matTooltip=\"Save\" matTooltipPosition=\"above\" (click)=\"submitInlineEdit()\"><mat-icon class=\"inline-save\">check</mat-icon></button> <!-- Changed: dropped color=\"primary\" \u2014 the icon now carries a green save cue -->\n <button mat-icon-button matTooltip=\"Cancel\" matTooltipPosition=\"above\" (click)=\"cancelInlineEdit()\"><mat-icon class=\"inline-cancel\">close</mat-icon></button> <!-- Changed: red cancel cue -->\n </ng-container>\n\n <!-- Actions -->\n <ng-template #rowActions>\n <app-table-action\n [displayedButtons]=\"displayedButtons\" [config]=\"config\" [smallScreen]=\"smallScreen\" [row]=\"row\" (actionClick)=\"actionClicked($event.name, $event.row)\">\n </app-table-action>\n </ng-template>\n\n </div>\n </td>\n </ng-container>\n\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\" [ngClass]=\"{'make-gray': (config.greyOut && config.greyOut(row)) || row.pendingApproval, 'row-editing': isRowEditing(row)}\"></tr> <!-- Changed: row-editing flags the row that is open for inline edit -->\n </table>\n\n </div>\n\n <!-- Changed: Removed *ngIf condition to keep paginator always in DOM and maintain ViewChild reference -->\n <!-- Changed: Added CSS class binding to hide when no data instead of conditional rendering -->\n <!-- Changed: Legacy paginator only renders in non-paged mode (pagedMode is constant per instance, set before first render) -->\n <mat-paginator *ngIf=\"!pagedMode\"\n #tablePaginator\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial'}\"\n showFirstLastButtons>\n </mat-paginator>\n\n <!-- Added: manual paginator for server-side paged mode \u2014 fully state-bound, never attached to MatTableDataSource. Always visible: when filtering it pages the in-memory filtered subset (length = filtered count); otherwise the server window (length = true total). No first/last jump (would force fetching the whole gap). -->\n <mat-paginator *ngIf=\"pagedMode\"\n [length]=\"filterActive ? filteredRows.length : serverTotal + overlayDelta\"\n [pageIndex]=\"pageIndex\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial'}\"\n (page)=\"onServerPage($event)\">\n </mat-paginator>\n\n </div>\n \n <!-- Added (Quiet Loading): the initial-load stage for the NON-table views.\n Without this, card/capsule/grouped showed literally nothing during a first load \u2014 the view component\n renders an empty dataSource and the \"No Data\" line is suppressed while the stage owns the space.\n The progress header is identical to the table's so the two feel like one feature; only the ghost\n furniture differs, because column-shaped rows are wrong in a card grid. -->\n <div *ngIf=\"loadingStage === 'initial' && config?.viewType && config?.viewType !== 'table'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div *ngIf=\"config?.viewType === 'capsule'\" class=\"tin-load-ghost-capsules\" aria-hidden=\"true\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let c of ghostCapsules\"></span>\n </div>\n\n <div *ngIf=\"config?.viewType === 'card'\" class=\"tin-load-ghost-cards\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-card\" *ngFor=\"let c of ghostCards\">\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-title\"></span>\n <span class=\"tin-skel tin-skel-text\"></span>\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-short\"></span>\n </div>\n </div>\n\n <div *ngIf=\"config?.viewType === 'grouped'\" class=\"tin-load-ghost-groups\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-group\" *ngFor=\"let g of ghostGroups\">\n <span class=\"tin-skel tin-load-ghost-group-head\"></span>\n <!-- Pills, not rows: a group card's body is a wrap of chips, so full-width bars promised a table\n and the stage did not resemble what replaced it. -->\n <div class=\"tin-load-ghost-group-items\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let r of ghostCards\"></span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Capsules -->\n <spa-capsules *ngIf=\"config?.viewType === 'capsule' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n (actionClick)=\"actionClicked($event.name, $event.row)\">\n </spa-capsules>\n\n\n <!-- Cards -->\n <spa-cards *ngIf=\"config?.viewType === 'card' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked($event.name, $event.row)\"\n (columnClick)=\"columnClicked($event.column, $event.row)\"\n (showBannerEvent)=\"showBanner($event)\">\n </spa-cards>\n\n <!-- Groups - Added: New grouped view type -->\n <!-- Changed: the grouped view no longer renders its own filter field. It used to sit on a row of its own\n beneath the buttons row, which left both rows half empty and, more importantly, was a SECOND filter with\n no refresh button. The standard header filter (which has refresh, like every other table) now drives it,\n with its text relayed in through filterText. -->\n <spa-groups *ngIf=\"config?.viewType === 'grouped' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [showOwnFilter]=\"false\"\n [filterText]=\"groupFilterText\"\n (actionClick)=\"actionClicked($event.name, $event.row, $event.group, $event.button)\">\n </spa-groups>\n\n\n <div class=\"tin-center\">\n <p *ngIf=\"dataSource?.length == 0 && loadingStage !== 'initial'\"><em>{{config.noDataMessage ?? 'No Data'}}</em></p> <!-- Changed (Quiet Loading): the stage owns the space until it completes, then hands straight over to this message \u2014 no \"No Data\" flashing underneath the ghost rows -->\n </div>\n\n </ng-container> <!-- Added: end section-collapse wrapper -->\n\n</ng-container>\n\n\n<ng-container *ngIf=\"!hasFormAccess\">\n <div class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n</ng-container>\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}.dlg-header-classic{padding-left:24px;padding-right:24px}.dlg-title-classic{font-size:20px;font-weight:500;margin-top:10px;margin-bottom:5px}.dialog-header-titles{display:flex;flex-direction:column;justify-content:center;min-width:0}.tin-dlg-head .dialog-header-titles label{margin:0}.tbl-section-header{display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:8px;border:1px solid rgba(0,0,0,.08);border-radius:10px;background:transparent;cursor:pointer;transition:border-color .15s}.tbl-section-header:hover{border-color:#90a4ae}.tbl-section-header.tbl-section-static{cursor:default}.tbl-section-icon{color:#546e7a}.tbl-section-title{font-size:14px;font-weight:600;color:#000000d1}.tbl-section-count{background:#e3f2fd;color:#1565c0;border-radius:12px;padding:2px 10px;font-size:12px}.tbl-section-chip{font-size:12px;font-weight:500;color:#0009}.tbl-section-spacer{margin-left:auto}.tbl-section-chevron{color:#90a4ae}.tbl-section-caption{margin:-4px 12px 10px;font-size:12.5px;line-height:1.45;color:#0000008c;max-width:82ch}.tbl-section-more{padding:0 12px 8px}.tbl-section-link{background:none;border:none;color:#1565c0;cursor:pointer;font-size:13px;padding:0}.tbl-section-link:hover{text-decoration:underline}.tbl-section-icon,.tbl-section-chevron,.tbl-section-count{flex:0 0 auto}.tbl-section-title{flex:1 1 auto;min-width:0}@media (max-width: 700px){.tbl-section-header{gap:8px;padding:10px}.tbl-section-chip,.tbl-section-btn-text.has-icon{display:none}.tbl-section-header button{min-width:0;padding:0 10px}}table.tin-no-col-headers tr.mat-mdc-header-row{display:none}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i14.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i14.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i14.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i14.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i14.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i14.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i14.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i14.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i14.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i14.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i15$1.MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SearchComponent, selector: "spa-search", inputs: ["config", "smallScreen", "tableDataSource"], outputs: ["searchClick"] }, { kind: "component", type: TableHeaderComponent, selector: "app-table-header", inputs: ["lastSearch", "config", "hideTitle", "tableDataSource", "tileConfig", "smallScreen", "tileReload", "showFilterButton", "data", "tileData", "isRealTime", "isConnected", "refreshing"], outputs: ["createClick", "customClick", "refreshClick", "tileClick", "tileUnClick", "filterChange"] }, { kind: "component", type: TableRowComponent, selector: "app-table-row", inputs: ["column", "row", "config", "smallScreen"], outputs: ["actionClick", "columnClick", "showBannerEvent"] }, { kind: "component", type: TableActionComponent, selector: "app-table-action", inputs: ["displayedButtons", "config", "row", "smallScreen"], outputs: ["actionClick"] }, { kind: "component", type: InlineCellComponent, selector: "app-inline-cell", inputs: ["field", "data"], outputs: ["valueChange"] }, { kind: "component", type: CapsulesComponent, selector: "spa-capsules", inputs: ["config", "dataSource", "displayedButtons"], outputs: ["actionClick"] }, { kind: "component", type: CardsComponent, selector: "spa-cards", inputs: ["config", "dataSource", "displayedButtons", "smallScreen"], outputs: ["actionClick", "columnClick", "showBannerEvent"] }, { kind: "component", type: GroupsComponent, selector: "spa-groups", inputs: ["config", "dataSource", "displayedButtons", "showOwnFilter", "filterText"], outputs: ["actionClick"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
17471
17611
  }
17472
17612
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableComponent, decorators: [{
17473
17613
  type: Component,
17474
- args: [{ selector: 'spa-table', standalone: false, template: "\n<ng-container *ngIf=\"hasFormAccess && !sectionHidden\"> <!-- Changed: sectionConfig.hideWhenEmpty hides the whole table -->\n\n <!-- Added: collapsible flat section header (sectionConfig) \u2014 1px border, no elevation, whole row toggles -->\n <div class=\"tbl-section-header\" *ngIf=\"config.sectionConfig\" (click)=\"toggleSection()\" [attr.aria-expanded]=\"!sectionCollapsed\" [class.tbl-section-static]=\"config.sectionConfig.collapsible === false\" role=\"button\" tabindex=\"0\" (keydown.enter)=\"toggleSection()\">\n <mat-icon class=\"tbl-section-icon\" *ngIf=\"config.sectionConfig.icon\">{{ config.sectionConfig.icon }}</mat-icon>\n <span class=\"tbl-section-title\">{{ config.sectionConfig.title }}</span>\n <span class=\"tbl-section-count\" *ngIf=\"config.sectionConfig.showCount !== false\">{{ dataSource?.length || 0 }}</span>\n <span class=\"tbl-section-chip\" *ngFor=\"let chip of sectionChips()\" [style.color]=\"chip.color\">{{ chip.text }}</span>\n <span class=\"tbl-section-spacer\"></span>\n <button mat-stroked-button color=\"primary\" *ngFor=\"let btn of sectionButtons()\" (click)=\"sectionButtonClicked(btn, $event)\"><mat-icon *ngIf=\"btn.icon?.name\">{{ btn.icon.name }}</mat-icon>{{ btn.display || btn.name }}</button>\n <mat-icon class=\"tbl-section-chevron\" *ngIf=\"config.sectionConfig.collapsible !== false\">{{ sectionCollapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </div>\n\n <!-- Added: collapsed section affordance \u2014 mirrors the Day Book \"Show the N\" pattern -->\n <div class=\"tbl-section-more\" *ngIf=\"config.sectionConfig && sectionCollapsed && (dataSource?.length || 0) > 0\">\n <button type=\"button\" class=\"tbl-section-link\" (click)=\"toggleSection()\">Show the {{ dataSource.length }}</button>\n </div>\n\n <ng-container *ngIf=\"!config.sectionConfig || !sectionCollapsed\"> <!-- Added: section collapse hides the table body -->\n\n <!-- Search -->\n <spa-search\n *ngIf=\"config.searchConfig\" [config]=\"config.searchConfig\" [smallScreen]=\"smallScreen\" [tableDataSource]=\"tableDataSource\" style=\"margin-bottom: 20px;\" (searchClick)=\"searchClicked($event)\">\n </spa-search>\n\n <!-- Header -->\n <app-table-header\n [config]=\"config\" [data]=\"dataSource\" [tableDataSource]=\"tableDataSource\" [tileConfig]=\"config.tileConfig\" [tileData]=\"tileData\" [tileReload]=\"tileReload\" [lastSearch]=\"lastSearch\" [smallScreen]=\"smallScreen\"\n [showFilterButton]=\"showFilterButton\" [isRealTime]=\"config.realTime\" [isConnected]=\"isSignalRConnected\" [refreshing]=\"loadingStage === 'refresh'\"\n (createClick)=\"newModel()\" (customClick)=\"customModel($event,null)\"\n (refreshClick)=\"refreshClicked()\" (tileClick)=\"tileClicked($event)\" (tileUnClick)=\"tileUnClicked($event)\" (filterChange)=\"filterChanged($event)\">\n </app-table-header>\n\n <!-- Added (Quiet Loading D4): refresh with data already on screen \u2014 a 2px line flush under the header and the\n spinning refresh icon are the ONLY signals. Rows stay live, clickable and un-dimmed while they swap. -->\n <div *ngIf=\"loadingStage === 'refresh'\" class=\"tin-load-line\" aria-hidden=\"true\">\n <span class=\"tin-load-line-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n\n <!-- Added: paged-mode filter hint \u2014 the client filter only covers rows loaded so far -->\n <div *ngIf=\"pagedMode && filterActive && loadedRows.length < serverTotal\" class=\"paged-filter-hint\">\n <mat-icon>info</mat-icon>\n <span>Filtering only the {{loadedRows.length}} loaded rows of {{serverTotal}}. {{ config.searchConfig ? 'Use Search for complete results.' : 'Refine with search for complete results.' }}</span>\n </div>\n\n\n <!-- Table -->\n <div *ngIf=\"!config.viewType || config?.viewType === 'table'\">\n\n <p *ngIf=\"!config\"><em>Configure Table</em></p>\n <p *ngIf=\"!dataSource && !loadingStage\"><em>Loading...</em></p> <!-- Changed (Quiet Loading): the bare text is replaced by the stage below while a quiet load is on screen -->\n\n <!-- Added (Quiet Loading D3): first load, nothing on screen yet. The progress module is the hero (eased bar +\n counting percentage + caption) and the ghost rows hold the exact space the real rows will fill, so the\n table does not jump when data lands. Only ever rendered when quiet loading is on. -->\n <div *ngIf=\"loadingStage === 'initial'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div class=\"tin-load-ghosts\">\n <div class=\"tin-load-ghost-row\" *ngFor=\"let r of ghostRows\">\n <div class=\"tin-load-ghost-cell\" *ngFor=\"let c of ghostColumns; let i = index\">\n <span class=\"tin-skel\" [style.width.%]=\"ghostWidth(i, c)\" [style.animation-delay.ms]=\"r * 120\"></span> <!-- staggered sweep: each row starts 120ms after the one above -->\n </div>\n </div>\n </div>\n\n </div>\n\n <div *ngIf=\"dataSource && loadingStage !== 'initial' && (!smallScreen || (smallScreen && dataSource?.length > 0))\" [class.tin-load-in]=\"effQuietLoading\"> <!-- Changed (Quiet Loading): the empty header-only table is suppressed while the initial stage stands in for it, and the real rows fade in where the ghosts were (D3) -->\n\n <table mat-table [dataSource]=\"tableDataSource\" [trackBy]=\"trackByRow\" [ngClass]=\"elevation\" [class.tin-no-col-headers]=\"config.hideColumnHeaders\"> <!-- Changed: optional column-header suppression -->\n\n <ng-container *ngFor=\"let column of config.columns\" [matColumnDef]=\"column.name\">\n <th mat-header-cell *matHeaderCellDef >{{ column.alias ?? column.name | camelToWords }}</th>\n <td mat-cell *matCellDef=\"let row;\" class=\"right-padding\" >\n\n <!-- Added: inline edit \u2014 editable cells swap to their form-field editor while the row is in edit mode -->\n <app-inline-cell *ngIf=\"isRowEditing(row) && getInlineField(column); else displayCell\" [field]=\"getInlineField(column)\" [data]=\"editingModel\"></app-inline-cell>\n\n <!-- Rows -->\n <ng-template #displayCell>\n <app-table-row [column]=\"column\" [row]=\"row\" [config]=\"config\" [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked(column.name, row)\" (columnClick)=\"columnClicked(column, row)\" (showBannerEvent)=\"showBanner($event)\">\n </app-table-row>\n </ng-template>\n\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"action\">\n <th mat-header-cell *matHeaderCellDef> Action </th>\n <td mat-cell *matCellDef=\"let row\" [ngStyle]=\"{width:false ? '20px' : actionsWidth}\">\n <div class=\"action-buttons-container\">\n\n <!-- Added: inline edit \u2014 while a row edits in place, its actions collapse to submit/cancel -->\n <ng-container *ngIf=\"isRowEditing(row); else rowActions\">\n <button mat-icon-button matTooltip=\"Save\" matTooltipPosition=\"above\" (click)=\"submitInlineEdit()\"><mat-icon class=\"inline-save\">check</mat-icon></button> <!-- Changed: dropped color=\"primary\" \u2014 the icon now carries a green save cue -->\n <button mat-icon-button matTooltip=\"Cancel\" matTooltipPosition=\"above\" (click)=\"cancelInlineEdit()\"><mat-icon class=\"inline-cancel\">close</mat-icon></button> <!-- Changed: red cancel cue -->\n </ng-container>\n\n <!-- Actions -->\n <ng-template #rowActions>\n <app-table-action\n [displayedButtons]=\"displayedButtons\" [config]=\"config\" [smallScreen]=\"smallScreen\" [row]=\"row\" (actionClick)=\"actionClicked($event.name, $event.row)\">\n </app-table-action>\n </ng-template>\n\n </div>\n </td>\n </ng-container>\n\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\" [ngClass]=\"{'make-gray': (config.greyOut && config.greyOut(row)) || row.pendingApproval, 'row-editing': isRowEditing(row)}\"></tr> <!-- Changed: row-editing flags the row that is open for inline edit -->\n </table>\n\n </div>\n\n <!-- Changed: Removed *ngIf condition to keep paginator always in DOM and maintain ViewChild reference -->\n <!-- Changed: Added CSS class binding to hide when no data instead of conditional rendering -->\n <!-- Changed: Legacy paginator only renders in non-paged mode (pagedMode is constant per instance, set before first render) -->\n <mat-paginator *ngIf=\"!pagedMode\"\n #tablePaginator\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial'}\"\n showFirstLastButtons>\n </mat-paginator>\n\n <!-- Added: manual paginator for server-side paged mode \u2014 fully state-bound, never attached to MatTableDataSource. Always visible: when filtering it pages the in-memory filtered subset (length = filtered count); otherwise the server window (length = true total). No first/last jump (would force fetching the whole gap). -->\n <mat-paginator *ngIf=\"pagedMode\"\n [length]=\"filterActive ? filteredRows.length : serverTotal + overlayDelta\"\n [pageIndex]=\"pageIndex\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial'}\"\n (page)=\"onServerPage($event)\">\n </mat-paginator>\n\n </div>\n \n <!-- Added (Quiet Loading): the initial-load stage for the NON-table views.\n Without this, card/capsule/grouped showed literally nothing during a first load \u2014 the view component\n renders an empty dataSource and the \"No Data\" line is suppressed while the stage owns the space.\n The progress header is identical to the table's so the two feel like one feature; only the ghost\n furniture differs, because column-shaped rows are wrong in a card grid. -->\n <div *ngIf=\"loadingStage === 'initial' && config?.viewType && config?.viewType !== 'table'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div *ngIf=\"config?.viewType === 'capsule'\" class=\"tin-load-ghost-capsules\" aria-hidden=\"true\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let c of ghostCapsules\"></span>\n </div>\n\n <div *ngIf=\"config?.viewType === 'card'\" class=\"tin-load-ghost-cards\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-card\" *ngFor=\"let c of ghostCards\">\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-title\"></span>\n <span class=\"tin-skel tin-skel-text\"></span>\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-short\"></span>\n </div>\n </div>\n\n <div *ngIf=\"config?.viewType === 'grouped'\" class=\"tin-load-ghost-groups\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-group\" *ngFor=\"let g of ghostGroups\">\n <span class=\"tin-skel tin-load-ghost-group-head\"></span>\n <!-- Pills, not rows: a group card's body is a wrap of chips, so full-width bars promised a table\n and the stage did not resemble what replaced it. -->\n <div class=\"tin-load-ghost-group-items\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let r of ghostCards\"></span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Capsules -->\n <spa-capsules *ngIf=\"config?.viewType === 'capsule' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n (actionClick)=\"actionClicked($event.name, $event.row)\">\n </spa-capsules>\n\n\n <!-- Cards -->\n <spa-cards *ngIf=\"config?.viewType === 'card' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked($event.name, $event.row)\"\n (columnClick)=\"columnClicked($event.column, $event.row)\"\n (showBannerEvent)=\"showBanner($event)\">\n </spa-cards>\n\n <!-- Groups - Added: New grouped view type -->\n <!-- Changed: the grouped view no longer renders its own filter field. It used to sit on a row of its own\n beneath the buttons row, which left both rows half empty and, more importantly, was a SECOND filter with\n no refresh button. The standard header filter (which has refresh, like every other table) now drives it,\n with its text relayed in through filterText. -->\n <spa-groups *ngIf=\"config?.viewType === 'grouped' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [showOwnFilter]=\"false\"\n [filterText]=\"groupFilterText\"\n (actionClick)=\"actionClicked($event.name, $event.row, $event.group, $event.button)\">\n </spa-groups>\n\n\n <div class=\"tin-center\">\n <p *ngIf=\"dataSource?.length == 0 && loadingStage !== 'initial'\"><em>{{config.noDataMessage ?? 'No Data'}}</em></p> <!-- Changed (Quiet Loading): the stage owns the space until it completes, then hands straight over to this message \u2014 no \"No Data\" flashing underneath the ghost rows -->\n </div>\n\n </ng-container> <!-- Added: end section-collapse wrapper -->\n\n</ng-container>\n\n\n<ng-container *ngIf=\"!hasFormAccess\">\n <div class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n</ng-container>\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}.dlg-header-classic{padding-left:24px;padding-right:24px}.dlg-title-classic{font-size:20px;font-weight:500;margin-top:10px;margin-bottom:5px}.dialog-header-titles{display:flex;flex-direction:column;justify-content:center;min-width:0}.tin-dlg-head .dialog-header-titles label{margin:0}.tbl-section-header{display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:8px;border:1px solid rgba(0,0,0,.08);border-radius:10px;background:transparent;cursor:pointer;transition:border-color .15s}.tbl-section-header:hover{border-color:#90a4ae}.tbl-section-header.tbl-section-static{cursor:default}.tbl-section-icon{color:#546e7a}.tbl-section-title{font-size:14px;font-weight:600;color:#000000d1}.tbl-section-count{background:#e3f2fd;color:#1565c0;border-radius:12px;padding:2px 10px;font-size:12px}.tbl-section-chip{font-size:12px;font-weight:500;color:#0009}.tbl-section-spacer{margin-left:auto}.tbl-section-chevron{color:#90a4ae}.tbl-section-more{padding:0 12px 8px}.tbl-section-link{background:none;border:none;color:#1565c0;cursor:pointer;font-size:13px;padding:0}.tbl-section-link:hover{text-decoration:underline}table.tin-no-col-headers tr.mat-mdc-header-row{display:none}\n"] }]
17614
+ args: [{ selector: 'spa-table', standalone: false, template: "\n<ng-container *ngIf=\"hasFormAccess && !sectionHidden\"> <!-- Changed: sectionConfig.hideWhenEmpty hides the whole table -->\n\n <!-- Added: collapsible flat section header (sectionConfig) \u2014 1px border, no elevation, whole row toggles -->\n <div class=\"tbl-section-header\" *ngIf=\"config.sectionConfig\" (click)=\"toggleSection()\" [attr.aria-expanded]=\"!sectionCollapsed\" [class.tbl-section-static]=\"config.sectionConfig.collapsible === false\" role=\"button\" tabindex=\"0\" (keydown.enter)=\"toggleSection()\">\n <mat-icon class=\"tbl-section-icon\" *ngIf=\"config.sectionConfig.icon\">{{ config.sectionConfig.icon }}</mat-icon>\n <span class=\"tbl-section-title\">{{ config.sectionConfig.title }}</span>\n <span class=\"tbl-section-count\" *ngIf=\"config.sectionConfig.showCount !== false\">{{ dataSource?.length || 0 }}</span>\n <span class=\"tbl-section-chip\" *ngFor=\"let chip of sectionChips()\" [style.color]=\"chip.color\">{{ chip.text }}</span>\n <span class=\"tbl-section-spacer\"></span>\n <!-- Changed: the label is wrapped so a NARROW screen can drop it and leave an icon-only button. Only a\n button that HAS an icon loses its text \u2014 otherwise it would collapse to a blank square. -->\n <button mat-stroked-button color=\"primary\" *ngFor=\"let btn of sectionButtons()\" (click)=\"sectionButtonClicked(btn, $event)\" [matTooltip]=\"btn.display || btn.name\"><mat-icon *ngIf=\"btn.icon?.name\">{{ btn.icon.name }}</mat-icon><span class=\"tbl-section-btn-text\" [class.has-icon]=\"!!btn.icon?.name\">{{ btn.display || btn.name }}</span></button>\n <mat-icon class=\"tbl-section-chevron\" *ngIf=\"config.sectionConfig.collapsible !== false\">{{ sectionCollapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </div>\n\n <!-- Added: the section's \"why this list exists\" line. It sits UNDER the title (a reason only makes sense once\n the list has been named) and only while the section is open \u2014 a shut section already has its own summary\n line, and a paragraph over the top of that is noise. -->\n <p class=\"tbl-section-caption\" *ngIf=\"config.sectionConfig?.caption && !sectionCollapsed\">{{ config.sectionConfig.caption }}</p>\n\n <!-- Added: collapsed section affordance \u2014 mirrors the Day Book \"Show the N\" pattern -->\n <div class=\"tbl-section-more\" *ngIf=\"config.sectionConfig && sectionCollapsed && (dataSource?.length || 0) > 0\">\n <button type=\"button\" class=\"tbl-section-link\" (click)=\"toggleSection()\">Show the {{ dataSource.length }}</button>\n </div>\n\n <ng-container *ngIf=\"!config.sectionConfig || !sectionCollapsed\"> <!-- Added: section collapse hides the table body -->\n\n <!-- Search -->\n <spa-search\n *ngIf=\"config.searchConfig\" [config]=\"config.searchConfig\" [smallScreen]=\"smallScreen\" [tableDataSource]=\"tableDataSource\" style=\"margin-bottom: 20px;\" (searchClick)=\"searchClicked($event)\">\n </spa-search>\n\n <!-- Header -->\n <app-table-header\n [config]=\"config\" [data]=\"dataSource\" [tableDataSource]=\"tableDataSource\" [tileConfig]=\"config.tileConfig\" [tileData]=\"tileData\" [tileReload]=\"tileReload\" [lastSearch]=\"lastSearch\" [smallScreen]=\"smallScreen\"\n [showFilterButton]=\"showFilterButton\" [isRealTime]=\"config.realTime\" [isConnected]=\"isSignalRConnected\" [refreshing]=\"loadingStage === 'refresh'\"\n (createClick)=\"newModel()\" (customClick)=\"customModel($event,null)\"\n (refreshClick)=\"refreshClicked()\" (tileClick)=\"tileClicked($event)\" (tileUnClick)=\"tileUnClicked($event)\" (filterChange)=\"filterChanged($event)\">\n </app-table-header>\n\n <!-- Added (Quiet Loading D4): refresh with data already on screen \u2014 a 2px line flush under the header and the\n spinning refresh icon are the ONLY signals. Rows stay live, clickable and un-dimmed while they swap. -->\n <div *ngIf=\"loadingStage === 'refresh'\" class=\"tin-load-line\" aria-hidden=\"true\">\n <span class=\"tin-load-line-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n\n <!-- Added: paged-mode filter hint \u2014 the client filter only covers rows loaded so far -->\n <div *ngIf=\"pagedMode && filterActive && loadedRows.length < serverTotal\" class=\"paged-filter-hint\">\n <mat-icon>info</mat-icon>\n <span>Filtering only the {{loadedRows.length}} loaded rows of {{serverTotal}}. {{ config.searchConfig ? 'Use Search for complete results.' : 'Refine with search for complete results.' }}</span>\n </div>\n\n\n <!-- Table -->\n <div *ngIf=\"!config.viewType || config?.viewType === 'table'\">\n\n <p *ngIf=\"!config\"><em>Configure Table</em></p>\n <p *ngIf=\"!dataSource && !loadingStage\"><em>Loading...</em></p> <!-- Changed (Quiet Loading): the bare text is replaced by the stage below while a quiet load is on screen -->\n\n <!-- Added (Quiet Loading D3): first load, nothing on screen yet. The progress module is the hero (eased bar +\n counting percentage + caption) and the ghost rows hold the exact space the real rows will fill, so the\n table does not jump when data lands. Only ever rendered when quiet loading is on. -->\n <div *ngIf=\"loadingStage === 'initial'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div class=\"tin-load-ghosts\">\n <div class=\"tin-load-ghost-row\" *ngFor=\"let r of ghostRows\">\n <div class=\"tin-load-ghost-cell\" *ngFor=\"let c of ghostColumns; let i = index\">\n <span class=\"tin-skel\" [style.width.%]=\"ghostWidth(i, c)\" [style.animation-delay.ms]=\"r * 120\"></span> <!-- staggered sweep: each row starts 120ms after the one above -->\n </div>\n </div>\n </div>\n\n </div>\n\n <div *ngIf=\"dataSource && loadingStage !== 'initial' && (!smallScreen || (smallScreen && dataSource?.length > 0))\" [class.tin-load-in]=\"effQuietLoading\"> <!-- Changed (Quiet Loading): the empty header-only table is suppressed while the initial stage stands in for it, and the real rows fade in where the ghosts were (D3) -->\n\n <table mat-table [dataSource]=\"tableDataSource\" [trackBy]=\"trackByRow\" [ngClass]=\"elevation\" [class.tin-no-col-headers]=\"config.hideColumnHeaders\"> <!-- Changed: optional column-header suppression -->\n\n <ng-container *ngFor=\"let column of config.columns\" [matColumnDef]=\"column.name\">\n <th mat-header-cell *matHeaderCellDef >{{ column.alias ?? column.name | camelToWords }}</th>\n <td mat-cell *matCellDef=\"let row;\" class=\"right-padding\" >\n\n <!-- Added: inline edit \u2014 editable cells swap to their form-field editor while the row is in edit mode -->\n <app-inline-cell *ngIf=\"isRowEditing(row) && getInlineField(column); else displayCell\" [field]=\"getInlineField(column)\" [data]=\"editingModel\"></app-inline-cell>\n\n <!-- Rows -->\n <ng-template #displayCell>\n <app-table-row [column]=\"column\" [row]=\"row\" [config]=\"config\" [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked(column.name, row)\" (columnClick)=\"columnClicked(column, row)\" (showBannerEvent)=\"showBanner($event)\">\n </app-table-row>\n </ng-template>\n\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"action\">\n <th mat-header-cell *matHeaderCellDef> Action </th>\n <td mat-cell *matCellDef=\"let row\" [ngStyle]=\"{width:false ? '20px' : actionsWidth}\">\n <div class=\"action-buttons-container\">\n\n <!-- Added: inline edit \u2014 while a row edits in place, its actions collapse to submit/cancel -->\n <ng-container *ngIf=\"isRowEditing(row); else rowActions\">\n <button mat-icon-button matTooltip=\"Save\" matTooltipPosition=\"above\" (click)=\"submitInlineEdit()\"><mat-icon class=\"inline-save\">check</mat-icon></button> <!-- Changed: dropped color=\"primary\" \u2014 the icon now carries a green save cue -->\n <button mat-icon-button matTooltip=\"Cancel\" matTooltipPosition=\"above\" (click)=\"cancelInlineEdit()\"><mat-icon class=\"inline-cancel\">close</mat-icon></button> <!-- Changed: red cancel cue -->\n </ng-container>\n\n <!-- Actions -->\n <ng-template #rowActions>\n <app-table-action\n [displayedButtons]=\"displayedButtons\" [config]=\"config\" [smallScreen]=\"smallScreen\" [row]=\"row\" (actionClick)=\"actionClicked($event.name, $event.row)\">\n </app-table-action>\n </ng-template>\n\n </div>\n </td>\n </ng-container>\n\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\" [ngClass]=\"{'make-gray': (config.greyOut && config.greyOut(row)) || row.pendingApproval, 'row-editing': isRowEditing(row)}\"></tr> <!-- Changed: row-editing flags the row that is open for inline edit -->\n </table>\n\n </div>\n\n <!-- Changed: Removed *ngIf condition to keep paginator always in DOM and maintain ViewChild reference -->\n <!-- Changed: Added CSS class binding to hide when no data instead of conditional rendering -->\n <!-- Changed: Legacy paginator only renders in non-paged mode (pagedMode is constant per instance, set before first render) -->\n <mat-paginator *ngIf=\"!pagedMode\"\n #tablePaginator\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial'}\"\n showFirstLastButtons>\n </mat-paginator>\n\n <!-- Added: manual paginator for server-side paged mode \u2014 fully state-bound, never attached to MatTableDataSource. Always visible: when filtering it pages the in-memory filtered subset (length = filtered count); otherwise the server window (length = true total). No first/last jump (would force fetching the whole gap). -->\n <mat-paginator *ngIf=\"pagedMode\"\n [length]=\"filterActive ? filteredRows.length : serverTotal + overlayDelta\"\n [pageIndex]=\"pageIndex\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial'}\"\n (page)=\"onServerPage($event)\">\n </mat-paginator>\n\n </div>\n \n <!-- Added (Quiet Loading): the initial-load stage for the NON-table views.\n Without this, card/capsule/grouped showed literally nothing during a first load \u2014 the view component\n renders an empty dataSource and the \"No Data\" line is suppressed while the stage owns the space.\n The progress header is identical to the table's so the two feel like one feature; only the ghost\n furniture differs, because column-shaped rows are wrong in a card grid. -->\n <div *ngIf=\"loadingStage === 'initial' && config?.viewType && config?.viewType !== 'table'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div *ngIf=\"config?.viewType === 'capsule'\" class=\"tin-load-ghost-capsules\" aria-hidden=\"true\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let c of ghostCapsules\"></span>\n </div>\n\n <div *ngIf=\"config?.viewType === 'card'\" class=\"tin-load-ghost-cards\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-card\" *ngFor=\"let c of ghostCards\">\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-title\"></span>\n <span class=\"tin-skel tin-skel-text\"></span>\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-short\"></span>\n </div>\n </div>\n\n <div *ngIf=\"config?.viewType === 'grouped'\" class=\"tin-load-ghost-groups\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-group\" *ngFor=\"let g of ghostGroups\">\n <span class=\"tin-skel tin-load-ghost-group-head\"></span>\n <!-- Pills, not rows: a group card's body is a wrap of chips, so full-width bars promised a table\n and the stage did not resemble what replaced it. -->\n <div class=\"tin-load-ghost-group-items\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let r of ghostCards\"></span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Capsules -->\n <spa-capsules *ngIf=\"config?.viewType === 'capsule' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n (actionClick)=\"actionClicked($event.name, $event.row)\">\n </spa-capsules>\n\n\n <!-- Cards -->\n <spa-cards *ngIf=\"config?.viewType === 'card' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked($event.name, $event.row)\"\n (columnClick)=\"columnClicked($event.column, $event.row)\"\n (showBannerEvent)=\"showBanner($event)\">\n </spa-cards>\n\n <!-- Groups - Added: New grouped view type -->\n <!-- Changed: the grouped view no longer renders its own filter field. It used to sit on a row of its own\n beneath the buttons row, which left both rows half empty and, more importantly, was a SECOND filter with\n no refresh button. The standard header filter (which has refresh, like every other table) now drives it,\n with its text relayed in through filterText. -->\n <spa-groups *ngIf=\"config?.viewType === 'grouped' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [showOwnFilter]=\"false\"\n [filterText]=\"groupFilterText\"\n (actionClick)=\"actionClicked($event.name, $event.row, $event.group, $event.button)\">\n </spa-groups>\n\n\n <div class=\"tin-center\">\n <p *ngIf=\"dataSource?.length == 0 && loadingStage !== 'initial'\"><em>{{config.noDataMessage ?? 'No Data'}}</em></p> <!-- Changed (Quiet Loading): the stage owns the space until it completes, then hands straight over to this message \u2014 no \"No Data\" flashing underneath the ghost rows -->\n </div>\n\n </ng-container> <!-- Added: end section-collapse wrapper -->\n\n</ng-container>\n\n\n<ng-container *ngIf=\"!hasFormAccess\">\n <div class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n</ng-container>\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}.dlg-header-classic{padding-left:24px;padding-right:24px}.dlg-title-classic{font-size:20px;font-weight:500;margin-top:10px;margin-bottom:5px}.dialog-header-titles{display:flex;flex-direction:column;justify-content:center;min-width:0}.tin-dlg-head .dialog-header-titles label{margin:0}.tbl-section-header{display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:8px;border:1px solid rgba(0,0,0,.08);border-radius:10px;background:transparent;cursor:pointer;transition:border-color .15s}.tbl-section-header:hover{border-color:#90a4ae}.tbl-section-header.tbl-section-static{cursor:default}.tbl-section-icon{color:#546e7a}.tbl-section-title{font-size:14px;font-weight:600;color:#000000d1}.tbl-section-count{background:#e3f2fd;color:#1565c0;border-radius:12px;padding:2px 10px;font-size:12px}.tbl-section-chip{font-size:12px;font-weight:500;color:#0009}.tbl-section-spacer{margin-left:auto}.tbl-section-chevron{color:#90a4ae}.tbl-section-caption{margin:-4px 12px 10px;font-size:12.5px;line-height:1.45;color:#0000008c;max-width:82ch}.tbl-section-more{padding:0 12px 8px}.tbl-section-link{background:none;border:none;color:#1565c0;cursor:pointer;font-size:13px;padding:0}.tbl-section-link:hover{text-decoration:underline}.tbl-section-icon,.tbl-section-chevron,.tbl-section-count{flex:0 0 auto}.tbl-section-title{flex:1 1 auto;min-width:0}@media (max-width: 700px){.tbl-section-header{gap:8px;padding:10px}.tbl-section-chip,.tbl-section-btn-text.has-icon{display:none}.tbl-section-header button{min-width:0;padding:0 10px}}table.tin-no-col-headers tr.mat-mdc-header-row{display:none}\n"] }]
17475
17615
  }], ctorParameters: () => [{ type: DataServiceLib }, { type: MessageService }, { type: i1$4.BreakpointObserver }, { type: i4.MatDialog }, { type: ButtonService }, { type: DialogService }, { type: TableConfigService }, { type: ConditionService }, { type: AuthService }, { type: SignalRService }, { type: OfflineService }, { type: ApiErrorService }, { type: undefined, decorators: [{
17476
17616
  type: Optional
17477
17617
  }, {
@@ -17518,88 +17658,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
17518
17658
  type: Output
17519
17659
  }] } });
17520
17660
 
17521
- // Added: the per-section row template slot. An app writes
17522
- // <ng-template spaDayBookSection="wean" let-r let-book="book"> … </ng-template>
17523
- // inside <spa-day-book> and owns the markup for that section's rows — including inline editors, which no
17524
- // row-action callback could ever express. The library still owns the page: layout, lanes, the stage strip,
17525
- // loading and empty states, counts, collapse, paging and the section chrome. Sections with no template fall
17526
- // back to the spa-table rendering, so existing config-only day books (test-spa) are untouched.
17527
- class DayBookSectionDirective {
17528
- constructor() {
17529
- this.spaDayBookSection = ''; // the DayBookSection.key this template draws rows for
17530
- this.templateRef = inject((TemplateRef));
17531
- }
17532
- // Gives the app's template real types on let-row / let-book instead of any
17533
- static ngTemplateContextGuard(dir, ctx) { return true; }
17534
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DayBookSectionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
17535
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.14", type: DayBookSectionDirective, isStandalone: false, selector: "[spaDayBookSection]", inputs: { spaDayBookSection: "spaDayBookSection" }, ngImport: i0 }); }
17536
- }
17537
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DayBookSectionDirective, decorators: [{
17538
- type: Directive,
17539
- args: [{
17540
- selector: '[spaDayBookSection]',
17541
- standalone: false
17542
- }]
17543
- }], propDecorators: { spaDayBookSection: [{
17544
- type: Input,
17545
- args: ['spaDayBookSection']
17546
- }] } });
17547
- // Added: the per-section HEADER slot — the section-level (batch) action counterpart of the row slot above. An app
17548
- // writes
17549
- // <ng-template spaDayBookSectionHeader="toInvoice" let-section let-book="book" let-count="count"> … </ng-template>
17550
- // and the library places it in that section's header chrome, between the count and the collapse control. This is
17551
- // the shape the row slot cannot express: "Invoice all 12" acts on the whole list, so there is no row to hang it
17552
- // on, and the money subtotal beside the count is a fact about the section rather than about any row in it.
17553
- // Deliberately a SIBLING of spaDayBookSection rather than a second idiom — same selector-plus-section-key form,
17554
- // same collect-by-key wiring, same context-guard typing, same materialize-once context discipline. It renders
17555
- // only on a section that also has a row template: a spa-table section draws its own header, and the library has
17556
- // no chrome there to project into.
17557
- class DayBookSectionHeaderDirective {
17558
- constructor() {
17559
- this.spaDayBookSectionHeader = ''; // the DayBookSection.key this template draws the header extras for
17560
- this.templateRef = inject((TemplateRef));
17561
- }
17562
- // Gives the app's template real types on let-section / let-book / let-count instead of any
17563
- static ngTemplateContextGuard(dir, ctx) { return true; }
17564
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DayBookSectionHeaderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
17565
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.14", type: DayBookSectionHeaderDirective, isStandalone: false, selector: "[spaDayBookSectionHeader]", inputs: { spaDayBookSectionHeader: "spaDayBookSectionHeader" }, ngImport: i0 }); }
17566
- }
17567
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DayBookSectionHeaderDirective, decorators: [{
17568
- type: Directive,
17569
- args: [{
17570
- selector: '[spaDayBookSectionHeader]',
17571
- standalone: false
17572
- }]
17573
- }], propDecorators: { spaDayBookSectionHeader: [{
17574
- type: Input,
17575
- args: ['spaDayBookSectionHeader']
17576
- }] } });
17577
17661
  // Added: spa-day-book — the centralized Day Book page. Replaces the per-app copy-forks (shift/grip/piglet):
17578
- // the library owns the behaviour (lanes, stage strip, section chrome, all-clear), the app supplies only
17579
- // appConfig.dayBookConfig (or [dayBookConfig]) + its own backend endpoint returning the flat book object, and
17580
- // optionally a row template per section. Design decisions carried in from the forks: zero-count tiles HIDE
17581
- // (not grey out), a stage click un-collapses its section BEFORE scrolling (piglet's fix), and "nothing
17582
- // outstanding" derives from the configured stages so it can never drift from the sections (grip's bug, killed
17583
- // by construction).
17662
+ // the library owns the behaviour (lanes, stage tiles, section chrome, all-clear) and the app supplies only
17663
+ // appConfig.dayBookConfig (or [dayBookConfig]) + its own backend endpoint returning the flat book object.
17664
+ // Changed: there is now ONE rendering path — config. The app row/header template hatches are gone, and with
17665
+ // them the last way for a consumer to reproduce its old fork inside the library's chrome. Design decisions
17666
+ // carried in from the forks: zero-count tiles HIDE (not grey out), a stage click un-collapses its section
17667
+ // BEFORE scrolling (piglet's fix), and "nothing outstanding" derives from the configured stages so it can
17668
+ // never drift from the sections (grip's bug, killed by construction).
17584
17669
  class DayBookComponent {
17585
17670
  constructor() {
17586
17671
  this.dataService = inject(DataServiceLib);
17672
+ this.messageService = inject(MessageService); // Added: page actions confirm and toast like every other button in the suite
17673
+ this.apiErrorService = inject(ApiErrorService);
17674
+ this.buttonService = inject(ButtonService);
17587
17675
  this.book = null;
17588
- this.loading = false;
17676
+ this.loading = false; // the re-entrancy guard — deliberately NOT bound in the template (see showLoading)
17677
+ // Added (NG0100): what the template actually binds. `loading` flips inside load(), and load() can be reached
17678
+ // from inside a change-detection pass (a host that re-enters the page, a child's lifecycle hook) — moving a
17679
+ // BOUND flag there is exactly what ExpressionChangedAfterItHasBeenChecked reports. The bound copy therefore
17680
+ // moves on the next microtask, after the pass that already read it has finished.
17681
+ this.showLoading = false;
17589
17682
  this.lane = 'all';
17590
17683
  this.tileConfig = null;
17591
17684
  this.tileData = null; // Added: book + resolved stage counts, a NEW object per build so spa-tiles sees a change
17592
- this.stripStages = []; // Added: materialized strip (stageStyle:'strip') — value included, a hire desk reads the money as much as the count
17593
17685
  this.sectionConfigs = [];
17594
17686
  this.visibleSections = []; // Changed: materialized (was an allocating getter feeding *ngFor)
17595
17687
  this.visibleGroups = []; // Added: the lane groups rendered on 'Everything' — materialized, same discipline
17596
17688
  this.ungroupedSections = []; // Added: sections that declare no lane — they stay above the groups, never hidden behind one
17597
17689
  this.lanes = []; // Changed: materialized (was a getter building a new array literal every cycle)
17690
+ this.pageActions = []; // Added: config-driven header buttons — materialized, never a getter feeding *ngFor
17598
17691
  // Added: the operator's own expand/collapse choice per lane, remembered next to the remembered lane pill
17599
17692
  this.groupState = {};
17600
17693
  this.grouped = false; // Added: materialized — grouping only ever applies on 'Everything', and only with two or more populated lanes
17601
- this.templates = new Map();
17602
- this.headers = new Map(); // Added: header slot, keyed by section
17603
17694
  }
17604
17695
  get config() {
17605
17696
  return this.dayBookConfig || this.dataService.appConfig?.dayBookConfig; // Changed: input wins, appConfig is the fallback
@@ -17607,9 +17698,6 @@ class DayBookComponent {
17607
17698
  get title() {
17608
17699
  return this.config?.title || 'Day Book';
17609
17700
  }
17610
- get stripMode() {
17611
- return this.config?.stageStyle === 'strip';
17612
- }
17613
17701
  ngOnInit() {
17614
17702
  const saved = localStorage.getItem('tin-daybook-lane');
17615
17703
  if (saved)
@@ -17622,47 +17710,80 @@ class DayBookComponent {
17622
17710
  catch {
17623
17711
  this.groupState = {};
17624
17712
  }
17713
+ this.buildActions(); // the header buttons exist before the first book does — Refresh must be there while it loads
17625
17714
  this.load();
17626
17715
  }
17627
- // Added: the app's templates are content children map them by section key, and rebuild if they ever change
17628
- ngAfterContentInit() {
17629
- this.mapTemplates();
17630
- this.rowTemplates?.changes.subscribe(() => { this.mapTemplates(); if (this.book)
17631
- this.rebuild(); });
17632
- this.headerTemplates?.changes.subscribe(() => { this.mapTemplates(); if (this.book)
17633
- this.rebuild(); }); // Added: header slot
17634
- if (this.book)
17635
- this.rebuild(); // a book that arrived before content init must pick the templates up
17636
- }
17637
- mapTemplates() {
17638
- this.templates.clear();
17639
- (this.rowTemplates || []).forEach(t => { if (t.spaDayBookSection)
17640
- this.templates.set(t.spaDayBookSection, t.templateRef); });
17641
- this.headers.clear(); // Added: header slot, same mapping
17642
- (this.headerTemplates || []).forEach(t => { if (t.spaDayBookSectionHeader)
17643
- this.headers.set(t.spaDayBookSectionHeader, t.templateRef); });
17716
+ // Added (NG0100): the one place the bound loading flag moves, always one microtask behind the guard
17717
+ setShowLoading(value) {
17718
+ if (this.showLoading === value)
17719
+ return;
17720
+ Promise.resolve().then(() => this.showLoading = value);
17644
17721
  }
17645
17722
  load() {
17646
17723
  if (!this.config?.loadAction || this.loading)
17647
17724
  return;
17648
17725
  this.loading = true;
17726
+ this.setShowLoading(true);
17649
17727
  this.dataService.CallApi(this.config.loadAction).subscribe({
17650
17728
  next: (response) => {
17651
17729
  this.loading = false;
17730
+ this.setShowLoading(false);
17652
17731
  if (!response.success)
17653
17732
  return;
17654
17733
  this.book = response.data;
17655
17734
  this.rebuild();
17656
17735
  },
17657
- error: () => this.loading = false
17736
+ error: () => { this.loading = false; this.setShowLoading(false); }
17658
17737
  });
17659
17738
  }
17660
17739
  // Added: public so an app that mutated the book in place (a row actioned without a refetch) can re-derive the
17661
- // strip, the counts and the section rows without the page reshuffling under the operator's hand.
17740
+ // tiles, the counts and the section rows without the page reshuffling under the operator's hand.
17662
17741
  rebuild() {
17663
17742
  this.buildLanes();
17664
17743
  this.buildTiles();
17665
17744
  this.buildSections();
17745
+ this.buildActions(); // a page action may hide itself off a book fact, so it is re-resolved with everything else
17746
+ }
17747
+ //---------- Page actions ----------
17748
+ // Added: the header buttons the [dayBookActions] slot used to carry. Content projection needs a consumer
17749
+ // component to do the projecting, and a config-only Day Book has no component — so every app converted to
17750
+ // pure config silently lost its Refresh. These come from config instead, and the ng-content slot is still
17751
+ // there beside them for anyone who does host the page themselves.
17752
+ buildActions() {
17753
+ this.pageActions = (this.config?.actions || []).filter(b => Core.isItemVisible(b, this.book));
17754
+ }
17755
+ pageActionClicked(button) {
17756
+ if (button.onClick) {
17757
+ button.onClick(this.book);
17758
+ if (!button.action)
17759
+ return; // a pure onClick button (a navigation, a dialog the app opens itself) is fully handled
17760
+ }
17761
+ // No endpoint and no handler IS the Refresh case — the only work a bare page button can mean is "load it again"
17762
+ if (!button.action) {
17763
+ this.load();
17764
+ return;
17765
+ }
17766
+ if (button.confirm) {
17767
+ this.messageService.confirm(this.buttonService.getConfirmMessage(button, this.book)).subscribe(result => {
17768
+ if (result === 'yes')
17769
+ this.runPageAction(button);
17770
+ });
17771
+ return;
17772
+ }
17773
+ this.runPageAction(button);
17774
+ }
17775
+ runPageAction(button) {
17776
+ this.dataService.CallApi(button.action, this.book).subscribe({
17777
+ next: (response) => {
17778
+ if (!response.success) {
17779
+ this.apiErrorService.presentAppFailure(response, 'action', button.action.url);
17780
+ return;
17781
+ }
17782
+ this.messageService.toast(button.action.successMessage || (response.message && response.message !== 'success' ? response.message : 'Updated'));
17783
+ this.load(); // whatever it just did, the book it did it to is now stale
17784
+ },
17785
+ error: (error) => this.apiErrorService.present(error)
17786
+ });
17666
17787
  }
17667
17788
  //---------- Lanes ----------
17668
17789
  buildLanes() {
@@ -17708,16 +17829,10 @@ class DayBookComponent {
17708
17829
  }
17709
17830
  // Stages map onto spa-tiles: name = countField so the tile reads its count straight off the book object;
17710
17831
  // zero-count tiles HIDE via the tile's own visibility predicate (design decision — no greyed-out tiles).
17711
- // With stageStyle:'strip' they render instead as a left-to-right cycle where a zero stage greys rather than
17712
- // disappears a production cycle with a gap in it stops reading as a cycle.
17832
+ // Changed: the stage row is ALWAYS spa-tiles. The 'strip' alternative is gone it existed only so the apps
17833
+ // could keep their fork's stage row, and one page cannot have two stage rows and still be one page.
17713
17834
  buildTiles() {
17714
17835
  const stages = (this.config?.stages || []).filter(s => Core.isItemVisible(s, this.book));
17715
- if (this.stripMode) {
17716
- this.stripStages = stages.filter(s => this.inLane(s.lane))
17717
- .map(s => ({ stage: s, count: this.stageCount(s), value: this.stageValue(s), alert: s.alert !== undefined && Core.isItemVisible({ visible: s.alert }, this.book) }));
17718
- this.tileConfig = null;
17719
- return;
17720
- }
17721
17836
  const tiles = stages.map(s => ({
17722
17837
  name: s.countField, alias: s.label, style: 'icon', icon: s.icon || 'checklist', info: s.hint,
17723
17838
  badge: typeof s.valueField === 'string' ? s.valueField : undefined, badgeColor: '#607d8b', // Changed: a computed valueField has no field name for the tile badge to read
@@ -17726,7 +17841,11 @@ class DayBookComponent {
17726
17841
  hidden: (d) => !d?.[s.countField] || !this.inLane(s.lane), // Changed: lane filter lives in the predicate so pill clicks re-filter tiles live
17727
17842
  data: s.key // stage key travels on the tile so the click can find its section
17728
17843
  }));
17729
- this.tileConfig = { tiles, clickable: true }; // a NEW object each build, so spa-tiles sees an input change
17844
+ // Changed: the Day Book opts into the CAROUSEL (owner's instruction 2026-08-08). A book carries 9-16
17845
+ // stages — more than any row can show — so one scrolling row beats either squeezing them or stacking
17846
+ // them down the page above the lists. This is a Day Book decision made once in the library rather than
17847
+ // repeated in five app configs; `stageTiles` still lets an app override it, including back to 'auto'.
17848
+ this.tileConfig = { tiles, clickable: true, layout: 'carousel', ...(this.config?.stageTiles ?? {}) }; // a NEW object each build, so spa-tiles sees an input change
17730
17849
  // Added: counts resolved onto a copy of the book, so a countField naming a LIST still shows a number
17731
17850
  this.tileData = this.book ? { ...this.book, ...stages.reduce((a, s) => { a[s.countField] = this.stageCount(s); return a; }, {}) } : null;
17732
17851
  }
@@ -17737,55 +17856,45 @@ class DayBookComponent {
17737
17856
  }
17738
17857
  //---------- Sections ----------
17739
17858
  // Clone the app's TableConfig per section rather than mutating the shared singleton (house rule).
17740
- // Collapse and row limit survive a rebuild an operator who opened a backlog section keeps it open when a
17741
- // row action refreshes the book.
17859
+ // Changed: one path only a section IS a spa-table, and spa-table owns its own collapse and paging.
17742
17860
  buildSections() {
17743
17861
  this.sectionConfigs = (this.config?.sections || []).map(section => {
17744
- const previous = this.sectionConfigs.find(x => x.section.key === section.key);
17745
17862
  const all = this.book?.[section.listField] || [];
17746
- const template = this.templates.get(section.key) || null;
17747
- const headerTemplate = this.headers.get(section.key) || null; // Added: the section's batch-action / subtotal slot
17748
17863
  const view = {
17749
- section, cfg: null, template, headerTemplate,
17750
- // Materialized once here, exactly as the row contexts are — an outlet context rebuilt in the template is
17751
- // a new object every change-detection pass, which re-creates the projected view under the operator.
17752
- headerContext: headerTemplate ? { $implicit: section, section, book: this.book, count: all.length } : null,
17753
- all, rows: [], contexts: [], outletContext: null, count: all.length,
17864
+ section, cfg: null,
17865
+ all, rows: all, outletContext: null, count: all.length,
17754
17866
  caption: typeof section.caption === 'function' ? section.caption(this.book) : (section.caption || ''),
17755
- limit: previous?.limit ?? (section.pageSize || this.config?.pageSize || 10),
17756
- collapsed: previous?.collapsed ?? !!section.collapsed
17867
+ limit: section.pageSize || this.config?.pageSize || 10,
17868
+ collapsed: !!section.collapsed
17757
17869
  };
17758
- if (!template && section.tableConfig) {
17870
+ if (section.tableConfig) {
17759
17871
  const cfg = { ...section.tableConfig };
17760
17872
  cfg.sectionConfig = section.tableConfig.sectionConfig ? { ...section.tableConfig.sectionConfig, collapsed: section.collapsed } : undefined;
17761
17873
  cfg.elevation = cfg.elevation ?? 'none'; // flat tables on the section cards — Day Book design language
17762
- if (cfg.sectionConfig)
17874
+ if (cfg.sectionConfig) {
17763
17875
  cfg.sectionConfig.hideWhenEmpty = cfg.sectionConfig.hideWhenEmpty !== false; // sections disappear when empty by default
17876
+ // Added: the section's "why this list exists" line rides INSIDE the table's own header, under the
17877
+ // title. It used to be drawn only on the app-template path, so every app that converted to config
17878
+ // lost it. An explicit sectionConfig.caption still wins — the section-level one is the default.
17879
+ cfg.sectionConfig.caption = cfg.sectionConfig.caption || view.caption || undefined;
17880
+ }
17881
+ // Added: pageSize was previously only honoured by the app-template path's row limit. It now seeds the
17882
+ // table's own paginator, so it still means what it says instead of becoming dead config.
17883
+ if (!cfg.pageSizes && (section.pageSize || this.config?.pageSize))
17884
+ cfg.pageSizes = [view.limit, ...[10, 20, 50].filter(n => n !== view.limit)];
17764
17885
  view.cfg = cfg;
17765
17886
  }
17766
- this.materialize(view);
17767
17887
  view.outletContext = { $implicit: view }; // built after the view exists, and never rebuilt in the template
17768
17888
  return view;
17769
17889
  });
17770
17890
  this.buildVisibleSections();
17771
17891
  }
17772
- // Added: rows and their template contexts resolved once, here. A templated section pages and collapses in the
17773
- // library (spa-table does its own), and each context object is kept stable so ngTemplateOutlet does not
17774
- // re-create the row's view — and any inline editor open inside it — on every change-detection cycle.
17775
- materialize(view) {
17776
- if (!view.template) {
17777
- view.rows = view.all;
17778
- return;
17779
- }
17780
- view.rows = view.collapsed ? [] : view.all.slice(0, view.limit);
17781
- view.contexts = view.rows.map((row, index) => ({ $implicit: row, row, index, section: view.section, book: this.book }));
17782
- }
17783
17892
  // Changed: was a getter — now recomputed only when the book loads or the lane changes
17784
17893
  buildVisibleSections() {
17785
17894
  this.visibleSections = this.sectionConfigs
17786
17895
  .filter(x => this.inLane(x.section.lane))
17787
17896
  .filter(x => Core.isItemVisible(x.section, this.book)) // Added: a section can hide itself off a book fact (a module the app switched off)
17788
- .filter(x => x.template ? x.count > 0 : (x.cfg?.sectionConfig?.hideWhenEmpty === false || x.rows.length > 0)); // Changed: empty sections hide the whole card, not just the table (fork behaviour)
17897
+ .filter(x => x.cfg?.sectionConfig?.hideWhenEmpty === false || x.rows.length > 0); // Changed: empty sections hide the whole card, not just the table (fork behaviour)
17789
17898
  this.buildGroups(); // Added: the same visible set, stacked into collapsible lane groups on 'Everything'
17790
17899
  }
17791
17900
  //---------- Lane groups ----------
@@ -17830,20 +17939,6 @@ class DayBookComponent {
17830
17939
  this.groupState[group.lane.key] = group.collapsed;
17831
17940
  localStorage.setItem('tin-daybook-groups', JSON.stringify(this.groupState));
17832
17941
  }
17833
- hiddenCount(view) {
17834
- if (!view.template || view.collapsed)
17835
- return 0;
17836
- return Math.max(0, view.count - view.limit);
17837
- }
17838
- // Added: collapse and "show n more" for templated sections — spa-table owns its own for the config path
17839
- toggleSection(view) {
17840
- view.collapsed = !view.collapsed;
17841
- this.materialize(view);
17842
- }
17843
- showMore(view) {
17844
- view.limit = view.count;
17845
- this.materialize(view);
17846
- }
17847
17942
  // Un-collapse BEFORE scrolling (piglet's fix, applied suite-wide) — a click must never land on a bare header
17848
17943
  goTo(key) {
17849
17944
  const target = this.sectionConfigs.find(x => x.section.key === key);
@@ -17853,15 +17948,9 @@ class DayBookComponent {
17853
17948
  const group = this.visibleGroups.find(g => g.lane.key === target.section.lane);
17854
17949
  if (group && group.collapsed)
17855
17950
  this.toggleGroup(group);
17856
- if (target.template) {
17857
- if (target.collapsed)
17858
- this.toggleSection(target);
17859
- }
17860
- else {
17861
- const table = this.tables?.find(t => t.config === target.cfg);
17862
- if (table && table.sectionCollapsed)
17863
- table.toggleSection(); // Changed: goes through toggleSection so the paginator re-attaches
17864
- }
17951
+ const table = this.tables?.find(t => t.config === target.cfg); // Changed: one path — the section is always a spa-table
17952
+ if (table && table.sectionCollapsed)
17953
+ table.toggleSection(); // goes through toggleSection so the paginator re-attaches
17865
17954
  }
17866
17955
  setTimeout(() => document.getElementById('sec-' + key)?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50);
17867
17956
  }
@@ -17885,20 +17974,14 @@ class DayBookComponent {
17885
17974
  return stages.length > 0 && stages.every(s => !this.stageCount(s));
17886
17975
  }
17887
17976
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DayBookComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
17888
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: DayBookComponent, isStandalone: false, selector: "spa-day-book", inputs: { dayBookConfig: "dayBookConfig" }, queries: [{ propertyName: "rowTemplates", predicate: DayBookSectionDirective, descendants: true }, { propertyName: "headerTemplates", predicate: DayBookSectionHeaderDirective, descendants: true }], viewQueries: [{ propertyName: "tables", predicate: TableComponent, descendants: true }], ngImport: i0, template: "<div class=\"db-page\" *ngIf=\"config\">\n\n <!-- Header: title, subtitle, books-from caption, owed total, app-supplied actions -->\n <div class=\"db-head\">\n <div class=\"db-head-text\">\n <h1>{{ title }}</h1>\n <p class=\"db-sub\" *ngIf=\"config.subtitle\">{{ config.subtitle }}</p>\n <span class=\"db-caption\" *ngIf=\"booksFrom\">books from {{ booksFrom | date: 'mediumDate' }}</span>\n </div>\n <span class=\"db-owed\" *ngIf=\"owed !== null\">Owed to us: <b>{{ owed | currency }}</b></span>\n <!-- Added: the app's own page actions (refresh, an escape hatch to a full screen) sit in the library header -->\n <div class=\"db-head-actions\"><ng-content select=\"[dayBookActions]\"></ng-content></div>\n </div>\n\n <!-- Lane pills -->\n <div class=\"db-lanes\" *ngIf=\"lanes.length\">\n <button type=\"button\" class=\"db-lane\" *ngFor=\"let l of lanes\" [class.active]=\"lane === l.key\" (click)=\"setLane(l.key)\">\n <mat-icon *ngIf=\"l.icon\">{{ l.icon }}</mat-icon>{{ l.label }}\n </button>\n </div>\n\n <!-- Added: the library owns the loading state \u2014 first load only, a refresh keeps the page on screen -->\n <div class=\"db-loading\" *ngIf=\"loading && !book\">{{ config.loadingMessage || 'Loading\u2026' }}</div>\n\n <ng-container *ngIf=\"book\">\n\n <!-- Stage strip \u2014 spa-tiles by default (zero-count tiles hide), or a left-to-right cycle strip where a\n zero stage greys rather than disappears (stageStyle: 'strip') -->\n <div class=\"db-pipe\" *ngIf=\"stripMode\">\n <button type=\"button\" class=\"db-stage\" *ngFor=\"let s of stripStages\" [class.empty]=\"!s.count\" [class.alert]=\"s.alert\" (click)=\"goTo(s.stage.key)\" [title]=\"s.stage.hint || ''\">\n <mat-icon>{{ s.stage.icon || 'checklist' }}</mat-icon>\n <span class=\"st-label\">{{ s.stage.label }}</span>\n <span class=\"st-count\">{{ s.count }}</span>\n <!-- Added: the money behind the count. A hire desk reads \"4 jobs / $12,400\" as one fact, not two -->\n <span class=\"st-value\" *ngIf=\"s.value\">{{ s.value | currency: undefined : 'symbol' : '1.0-0' }}</span>\n </button>\n </div>\n <spa-tiles *ngIf=\"!stripMode && tileConfig\" [config]=\"tileConfig\" [data]=\"tileData\" (tileClick)=\"onTileClick($event)\"></spa-tiles>\n\n <!-- All clear -->\n <div class=\"db-clear\" *ngIf=\"allClear\">\n <mat-icon>task_alt</mat-icon>\n <span>{{ config.allClearMessage || 'Nothing outstanding \u2014 all caught up.' }}</span>\n </div>\n\n <!-- Sections. A book carries 9-16 of these cards and, on 'Everything', all of them used to stack in one\n column \u2014 so they are grouped into one collapsible lane group each, with the first open and the rest\n showing only their identity: label, count, money and a chip per list. Sections that declare no lane\n sit above the groups, and with grouping off (one lane, a picked lane, or groupLanes:false) the page\n renders exactly as it did before. -->\n <ng-container *ngFor=\"let s of ungroupedSections\">\n <ng-container *ngTemplateOutlet=\"sectionCard; context: s.outletContext\"></ng-container>\n </ng-container>\n\n <section class=\"db-group\" *ngFor=\"let g of visibleGroups\" [class.closed]=\"g.collapsed\">\n <button type=\"button\" class=\"db-ghead\" (click)=\"toggleGroup(g)\" [attr.aria-expanded]=\"!g.collapsed\">\n <mat-icon *ngIf=\"g.lane.icon\">{{ g.lane.icon }}</mat-icon>\n <span class=\"g-label\">{{ g.lane.label }}</span>\n <span class=\"db-count\">{{ g.count }}</span>\n <span class=\"g-value\" *ngIf=\"g.value\">{{ g.value | currency: undefined : 'symbol' : '1.0-0' }}</span>\n <span class=\"g-lists\">{{ g.sections.length }} {{ g.sections.length === 1 ? 'list' : 'lists' }}</span>\n <mat-icon class=\"g-chev\">{{ g.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n <!-- Collapsed, the group still says what it is about \u2014 one chip per non-empty list, and a chip is a way\n in: it opens the group and scrolls to that section, the same path a stage click takes. -->\n <div class=\"db-chips\" *ngIf=\"g.collapsed\">\n <button type=\"button\" class=\"db-chip\" *ngFor=\"let c of g.chips\" (click)=\"goTo(c.key)\">{{ c.label }}<span>{{ c.count }}</span></button>\n <span class=\"db-chip-none\" *ngIf=\"!g.chips.length\">Nothing outstanding here</span>\n </div>\n <div class=\"db-gbody\" *ngIf=\"!g.collapsed\">\n <ng-container *ngFor=\"let s of g.sections\">\n <ng-container *ngTemplateOutlet=\"sectionCard; context: s.outletContext\"></ng-container>\n </ng-container>\n </div>\n </section>\n\n <!-- One section card, drawn from two places (flat above, and inside a lane group). An app-supplied row\n template draws the rows inside the library's card chrome, otherwise the section is a spa-table fed\n local rows from the book object. -->\n <ng-template #sectionCard let-s>\n\n <section class=\"db-sec\" *ngIf=\"s.template; else libraryTable\" [id]=\"'sec-' + s.section.key\" [class.alert]=\"s.section.tone === 'alert'\" [class.quiet]=\"s.section.tone === 'quiet'\">\n <header>\n <mat-icon *ngIf=\"s.section.icon\">{{ s.section.icon }}</mat-icon>\n <h2>{{ s.section.label || s.section.key }}</h2>\n <span class=\"db-count\">{{ s.count }}</span>\n <!-- Added: the section-level slot \u2014 batch actions (\"Invoice all 12\") and the money subtotal beside the\n count. It acts on the whole list, so it sits in the header rather than travelling on a row. -->\n <ng-container *ngIf=\"s.headerTemplate\">\n <ng-container *ngTemplateOutlet=\"s.headerTemplate; context: s.headerContext\"></ng-container>\n </ng-container>\n <button type=\"button\" class=\"db-collapse\" (click)=\"toggleSection(s)\" [attr.aria-expanded]=\"!s.collapsed\">\n <mat-icon>{{ s.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n </header>\n <p class=\"db-why\" *ngIf=\"s.caption\">{{ s.caption }}</p>\n <ng-container *ngFor=\"let ctx of s.contexts\">\n <ng-container *ngTemplateOutlet=\"s.template; context: ctx\"></ng-container>\n </ng-container>\n <button type=\"button\" class=\"db-more\" *ngIf=\"hiddenCount(s)\" (click)=\"showMore(s)\">Show {{ hiddenCount(s) }} more</button>\n </section>\n\n <ng-template #libraryTable>\n <mat-card class=\"db-section\" [id]=\"'sec-' + s.section.key\">\n <spa-table [config]=\"s.cfg\" [data]=\"s.rows\"></spa-table>\n </mat-card>\n </ng-template>\n\n </ng-template>\n\n </ng-container>\n\n</div>\n\n<!-- Graceful empty state (feature not configured) -->\n<div class=\"db-empty\" *ngIf=\"!config\">\n <mat-icon>checklist</mat-icon>\n <p>The Day Book is not configured for this app.</p>\n</div>\n", styles: [":host{--db-accent: #1565c0;--db-accent-soft: #e3f2fd;--db-alert: #e53935;--db-alert-text: #c62828;--db-quiet: #b0bec5}.db-page{max-width:1200px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.db-head{display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap}.db-head-text h1{margin:0;font-size:24px}.db-head-text .db-sub{margin:4px 0 0;font-size:14px;color:#0000008c}.db-caption{font-size:12px;color:#0000008c}.db-owed{font-size:13px;color:#000000b3;margin-left:auto}.db-head-actions{display:flex;gap:8px;margin-left:auto}.db-lanes{display:flex;gap:6px;flex-wrap:wrap;align-items:center}.db-lane{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(0,0,0,.12);border-radius:16px;background:transparent;padding:4px 14px;font:inherit;font-size:13px;cursor:pointer;transition:border-color .15s,background .15s}.db-lane:hover{border-color:#90a4ae}.db-lane.active{border-color:var(--db-accent);background:var(--db-accent-soft);color:var(--db-accent);font-weight:500}.db-lane mat-icon{font-size:17px;width:17px;height:17px}.db-loading{padding:40px;text-align:center;color:#00000080}.db-pipe{display:flex;gap:6px;overflow-x:auto;padding-bottom:4px}.db-stage{flex:1 1 0;min-width:104px;display:flex;flex-direction:column;align-items:flex-start;gap:2px;border:1px solid #e0e0e0;border-top:3px solid #bdbdbd;border-radius:5px;background:#fff;padding:9px 11px 10px;font:inherit;cursor:pointer;text-align:left;position:relative}.db-stage:hover{background:#fafbfa}.db-stage mat-icon{font-size:18px;width:18px;height:18px;color:#00000073}.db-stage .st-label{font-size:11.5px;color:#0009;text-transform:uppercase;letter-spacing:.3px;white-space:normal;line-height:1.25;min-height:2.5em}.db-stage .st-count{font-size:21px;font-weight:600;line-height:1.1;font-variant-numeric:tabular-nums}.db-stage .st-value{font-size:11.5px;color:#00000080;font-variant-numeric:tabular-nums}.db-stage.empty{opacity:.45}.db-stage.alert{border-top-color:var(--db-alert)}.db-stage.alert .st-count{color:var(--db-alert-text)}.db-stage:after{content:\"\\203a\";position:absolute;right:-6px;top:50%;transform:translateY(-50%);color:#00000038;font-size:18px;z-index:1}.db-stage:last-child:after{content:none}.db-clear{display:flex;align-items:center;gap:10px;border:1px solid #c8e6c9;background:#f6fbf6;border-radius:10px;padding:14px 16px;color:#2e7d32}.db-section{padding:12px 16px}.db-sec{background:#fff;border:1px solid #e0e0e0;border-left:4px solid var(--db-accent);border-radius:6px;overflow:hidden;scroll-margin-top:12px}.db-sec.alert{border-left-color:var(--db-alert)}.db-sec.alert>header mat-icon{color:var(--db-alert)}.db-sec.quiet{border-left-color:var(--db-quiet)}.db-sec.quiet>header mat-icon{color:#78909c}.db-sec>header{display:flex;align-items:center;gap:12px;padding:14px 18px 10px;flex-wrap:wrap}.db-sec>header h2{margin:0;font-size:17px;font-weight:500}.db-sec>header mat-icon{font-size:22px;width:22px;height:22px;color:var(--db-accent)}.db-count{background:#00000012;border-radius:11px;padding:1px 9px;font-size:13px;font-weight:600;font-variant-numeric:tabular-nums}.db-why{margin:0 18px 10px;color:#0000008c;font-size:13px;max-width:82ch}.db-collapse{border:0;background:none;padding:2px;cursor:pointer;color:#00000073;display:inline-flex;margin-left:auto}.db-collapse:hover{color:#000000bf}.db-collapse mat-icon{font-size:22px;width:22px;height:22px}.db-more{border:0;background:none;padding:10px 18px 12px;font:inherit;font-size:13px;color:var(--db-accent);cursor:pointer;border-top:1px solid #eeeeee;width:100%;text-align:left}.db-more:hover{text-decoration:underline}.db-group{display:flex;flex-direction:column;gap:12px}.db-group.closed{gap:0}.db-ghead{display:flex;align-items:center;gap:10px;width:100%;border:1px solid #e0e0e0;border-radius:6px;background:#fafafa;padding:11px 14px;font:inherit;cursor:pointer;text-align:left;transition:background .15s,border-color .15s}.db-ghead:hover{background:#f2f5f7;border-color:#cfd8dc}.db-group.closed .db-ghead{border-bottom-left-radius:0;border-bottom-right-radius:0}.db-ghead>mat-icon{font-size:20px;width:20px;height:20px;color:var(--db-accent)}.db-ghead .g-label{font-size:15px;font-weight:600;letter-spacing:.2px}.db-ghead .g-value{font-size:13px;font-weight:600;color:#000000b3;font-variant-numeric:tabular-nums}.db-ghead .g-lists{font-size:12px;color:#00000073;margin-left:auto}.db-ghead .g-chev{font-size:22px;width:22px;height:22px;color:#00000073}.db-chips{display:flex;flex-wrap:wrap;gap:6px;border:1px solid #e0e0e0;border-top:0;border-radius:0 0 6px 6px;background:#fff;padding:9px 14px 11px}.db-chip{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(0,0,0,.12);border-radius:14px;background:transparent;padding:2px 6px 2px 11px;font:inherit;font-size:12.5px;color:#000000b3;cursor:pointer}.db-chip:hover{border-color:var(--db-accent);color:var(--db-accent)}.db-chip span{background:#00000012;border-radius:10px;padding:0 7px;font-weight:600;font-variant-numeric:tabular-nums}.db-chip-none{font-size:12.5px;color:#00000073}.db-gbody{display:flex;flex-direction:column;gap:12px}.db-empty{padding:48px;text-align:center;color:#00000080}@media (max-width: 760px){.db-stage:after{content:none}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: TilesComponent, selector: "spa-tiles", inputs: ["config", "lastSearch", "data", "reload"], outputs: ["tileActionSelected", "tileClick", "tileUnClick"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }, { kind: "pipe", type: i1$2.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }] }); }
17977
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: DayBookComponent, isStandalone: false, selector: "spa-day-book", inputs: { dayBookConfig: "dayBookConfig" }, viewQueries: [{ propertyName: "tables", predicate: TableComponent, descendants: true }], ngImport: i0, template: "<div class=\"db-page\" *ngIf=\"config\">\n\n <!-- Header: title, subtitle, books-from caption, owed total, app-supplied actions -->\n <div class=\"db-head\">\n <div class=\"db-head-text\">\n <h1>{{ title }}</h1>\n <p class=\"db-sub\" *ngIf=\"config.subtitle\">{{ config.subtitle }}</p>\n <span class=\"db-caption\" *ngIf=\"booksFrom\">books from {{ booksFrom | date: 'mediumDate' }}</span>\n </div>\n <span class=\"db-owed\" *ngIf=\"owed !== null\">Owed to us: <b>{{ owed | currency }}</b></span>\n <!-- The page's own actions (refresh, an escape hatch to a full screen). Changed: they now come from\n config.actions, because an app with no component of its own has nothing to project with \u2014 the\n ng-content slot stays beside them for anyone who does host the page themselves. -->\n <!-- Changed: an action WITH an icon renders icon-only (owner, 2026-08-08 \u2014 \"the refresh and all trips\n buttons can just be icons with no text\"). The label is not lost, it moves to the tooltip and to\n aria-label, so the control still announces itself to a screen reader and on hover. An action with\n no icon keeps its text button, because an unlabelled blank button would be unusable. -->\n <div class=\"db-head-actions\">\n <ng-container *ngFor=\"let a of pageActions\">\n <button mat-icon-button color=\"primary\" type=\"button\" *ngIf=\"a.icon?.name; else textAction\" (click)=\"pageActionClicked(a)\" [matTooltip]=\"a.tip || a.display || a.name\" [attr.aria-label]=\"a.display || a.name\">\n <mat-icon>{{ a.icon.name }}</mat-icon>\n </button>\n <ng-template #textAction>\n <button mat-stroked-button color=\"primary\" type=\"button\" (click)=\"pageActionClicked(a)\" [title]=\"a.tip || ''\">{{ a.display || a.name }}</button>\n </ng-template>\n </ng-container>\n <ng-content select=\"[dayBookActions]\"></ng-content>\n </div>\n </div>\n\n <!-- Lane pills -->\n <div class=\"db-lanes\" *ngIf=\"lanes.length\">\n <button type=\"button\" class=\"db-lane\" *ngFor=\"let l of lanes\" [class.active]=\"lane === l.key\" (click)=\"setLane(l.key)\">\n <mat-icon *ngIf=\"l.icon\">{{ l.icon }}</mat-icon>{{ l.label }}\n </button>\n </div>\n\n <!-- Added: the library owns the loading state \u2014 first load only, a refresh keeps the page on screen.\n Changed: binds showLoading, the copy that only ever moves between change-detection passes (NG0100) -->\n <div class=\"db-loading\" *ngIf=\"showLoading && !book\">{{ config.loadingMessage || 'Loading\u2026' }}</div>\n\n <ng-container *ngIf=\"book\">\n\n <!-- The stage row. Changed: always spa-tiles \u2014 zero-count tiles hide -->\n <spa-tiles *ngIf=\"tileConfig\" [config]=\"tileConfig\" [data]=\"tileData\" (tileClick)=\"onTileClick($event)\"></spa-tiles>\n\n <!-- All clear -->\n <div class=\"db-clear\" *ngIf=\"allClear\">\n <mat-icon>task_alt</mat-icon>\n <span>{{ config.allClearMessage || 'Nothing outstanding \u2014 all caught up.' }}</span>\n </div>\n\n <!-- Sections. A book carries 9-16 of these cards and, on 'Everything', all of them used to stack in one\n column \u2014 so they are grouped into one collapsible lane group each, with the first open and the rest\n showing only their identity: label, count, money and a chip per list. Sections that declare no lane\n sit above the groups, and with grouping off (one lane, a picked lane, or groupLanes:false) the page\n renders exactly as it did before. -->\n <ng-container *ngFor=\"let s of ungroupedSections\">\n <ng-container *ngTemplateOutlet=\"sectionCard; context: s.outletContext\"></ng-container>\n </ng-container>\n\n <section class=\"db-group\" *ngFor=\"let g of visibleGroups\" [class.closed]=\"g.collapsed\">\n <button type=\"button\" class=\"db-ghead\" (click)=\"toggleGroup(g)\" [attr.aria-expanded]=\"!g.collapsed\">\n <mat-icon *ngIf=\"g.lane.icon\">{{ g.lane.icon }}</mat-icon>\n <span class=\"g-label\">{{ g.lane.label }}</span>\n <span class=\"db-count\">{{ g.count }}</span>\n <span class=\"g-value\" *ngIf=\"g.value\">{{ g.value | currency: undefined : 'symbol' : '1.0-0' }}</span>\n <span class=\"g-lists\">{{ g.sections.length }} {{ g.sections.length === 1 ? 'list' : 'lists' }}</span>\n <mat-icon class=\"g-chev\">{{ g.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n <!-- Collapsed, the group still says what it is about \u2014 one chip per non-empty list, and a chip is a way\n in: it opens the group and scrolls to that section, the same path a stage click takes. -->\n <div class=\"db-chips\" *ngIf=\"g.collapsed\">\n <button type=\"button\" class=\"db-chip\" *ngFor=\"let c of g.chips\" (click)=\"goTo(c.key)\">{{ c.label }}<span>{{ c.count }}</span></button>\n <span class=\"db-chip-none\" *ngIf=\"!g.chips.length\">Nothing outstanding here</span>\n </div>\n <div class=\"db-gbody\" *ngIf=\"!g.collapsed\">\n <ng-container *ngFor=\"let s of g.sections\">\n <ng-container *ngTemplateOutlet=\"sectionCard; context: s.outletContext\"></ng-container>\n </ng-container>\n </div>\n </section>\n\n <!-- One section card, drawn from two places (flat above, and inside a lane group). Changed: ONE rendering\n path \u2014 a section is a spa-table fed local rows off the book object. The table draws its own section\n header (icon, title, count, chips, buttons, caption, chevron); (actionSuccess) reloads the book so a\n row that has just been actioned leaves the list instead of sitting there looking undone \u2014 the section\n has no loadAction of its own to refresh from. -->\n <ng-template #sectionCard let-s>\n <mat-card class=\"db-section\" [id]=\"'sec-' + s.section.key\" [class.alert]=\"s.section.tone === 'alert'\" [class.quiet]=\"s.section.tone === 'quiet'\">\n <!-- the \"why this list exists\" line rides inside the table's own header, under the title. With no\n section header there is nothing to ride, so the card carries it instead. -->\n <p class=\"db-why\" *ngIf=\"s.caption && !s.cfg?.sectionConfig\">{{ s.caption }}</p>\n <spa-table [config]=\"s.cfg\" [data]=\"s.rows\" (actionSuccess)=\"load()\"></spa-table>\n </mat-card>\n </ng-template>\n\n </ng-container>\n\n</div>\n\n<!-- Graceful empty state (feature not configured) -->\n<div class=\"db-empty\" *ngIf=\"!config\">\n <mat-icon>checklist</mat-icon>\n <p>The Day Book is not configured for this app.</p>\n</div>\n", styles: [":host{--db-accent: #1565c0;--db-accent-soft: #e3f2fd;--db-alert: #e53935;--db-alert-text: #c62828;--db-quiet: #b0bec5}.db-page{max-width:1200px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.db-head{display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap}.db-head-text h1{margin:0;font-size:24px}.db-head-text .db-sub{margin:4px 0 0;font-size:14px;color:#0000008c}.db-caption{font-size:12px;color:#0000008c}.db-owed{font-size:13px;color:#000000b3;margin-left:auto}.db-head-actions{display:flex;gap:8px;margin-left:auto}.db-lanes{display:flex;gap:6px;flex-wrap:wrap;align-items:center}.db-lane{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(0,0,0,.12);border-radius:16px;background:transparent;padding:4px 14px;font:inherit;font-size:13px;cursor:pointer;transition:border-color .15s,background .15s}.db-lane:hover{border-color:#90a4ae}.db-lane.active{border-color:var(--db-accent);background:var(--db-accent-soft);color:var(--db-accent);font-weight:500}.db-lane mat-icon{font-size:17px;width:17px;height:17px}@media (max-width: 700px){.db-head{gap:6px 10px;align-items:center}.db-head-text{flex:1 1 auto;min-width:0;order:1}.db-head-text h1{font-size:20px}.db-head-text .db-sub{display:none}.db-caption{font-size:11px}.db-head-actions{order:2;margin-left:auto;gap:2px}.db-owed{order:3;flex:1 0 100%;margin-left:0;font-size:13px}.db-lanes{flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}.db-lanes::-webkit-scrollbar{display:none}.db-lane{flex:0 0 auto}}.db-loading{padding:40px;text-align:center;color:#00000080}.db-clear{display:flex;align-items:center;gap:10px;border:1px solid #c8e6c9;background:#f6fbf6;border-radius:10px;padding:14px 16px;color:#2e7d32}.db-section{padding:12px 16px;scroll-margin-top:12px;border-left:4px solid transparent}.db-section.alert{border-left-color:var(--db-alert)}.db-section.quiet{border-left-color:var(--db-quiet)}.db-count{background:#00000012;border-radius:11px;padding:1px 9px;font-size:13px;font-weight:600;font-variant-numeric:tabular-nums}.db-why{margin:0 0 10px;color:#0000008c;font-size:13px;max-width:82ch}.db-group{display:flex;flex-direction:column;gap:12px}.db-group.closed{gap:0}.db-ghead{display:flex;align-items:center;gap:10px;width:100%;border:1px solid #e0e0e0;border-radius:6px;background:#fafafa;padding:11px 14px;font:inherit;cursor:pointer;text-align:left;transition:background .15s,border-color .15s}.db-ghead:hover{background:#f2f5f7;border-color:#cfd8dc}.db-group.closed .db-ghead{border-bottom-left-radius:0;border-bottom-right-radius:0}.db-ghead>mat-icon{font-size:20px;width:20px;height:20px;color:var(--db-accent)}.db-ghead .g-label{font-size:15px;font-weight:600;letter-spacing:.2px}.db-ghead .g-value{font-size:13px;font-weight:600;color:#000000b3;font-variant-numeric:tabular-nums}.db-ghead .g-lists{font-size:12px;color:#00000073;margin-left:auto}.db-ghead .g-chev{font-size:22px;width:22px;height:22px;color:#00000073}.db-chips{display:flex;flex-wrap:wrap;gap:6px;border:1px solid #e0e0e0;border-top:0;border-radius:0 0 6px 6px;background:#fff;padding:9px 14px 11px}.db-chip{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(0,0,0,.12);border-radius:14px;background:transparent;padding:2px 6px 2px 11px;font:inherit;font-size:12.5px;color:#000000b3;cursor:pointer}.db-chip:hover{border-color:var(--db-accent);color:var(--db-accent)}.db-chip span{background:#00000012;border-radius:10px;padding:0 7px;font-weight:600;font-variant-numeric:tabular-nums}.db-chip-none{font-size:12.5px;color:#00000073}.db-gbody{display:flex;flex-direction:column;gap:12px}.db-empty{padding:48px;text-align:center;color:#00000080}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: TilesComponent, selector: "spa-tiles", inputs: ["config", "lastSearch", "data", "reload"], outputs: ["tileActionSelected", "tileClick", "tileUnClick"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }, { kind: "pipe", type: i1$2.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }] }); }
17889
17978
  }
17890
17979
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DayBookComponent, decorators: [{
17891
17980
  type: Component,
17892
- args: [{ selector: 'spa-day-book', standalone: false, template: "<div class=\"db-page\" *ngIf=\"config\">\n\n <!-- Header: title, subtitle, books-from caption, owed total, app-supplied actions -->\n <div class=\"db-head\">\n <div class=\"db-head-text\">\n <h1>{{ title }}</h1>\n <p class=\"db-sub\" *ngIf=\"config.subtitle\">{{ config.subtitle }}</p>\n <span class=\"db-caption\" *ngIf=\"booksFrom\">books from {{ booksFrom | date: 'mediumDate' }}</span>\n </div>\n <span class=\"db-owed\" *ngIf=\"owed !== null\">Owed to us: <b>{{ owed | currency }}</b></span>\n <!-- Added: the app's own page actions (refresh, an escape hatch to a full screen) sit in the library header -->\n <div class=\"db-head-actions\"><ng-content select=\"[dayBookActions]\"></ng-content></div>\n </div>\n\n <!-- Lane pills -->\n <div class=\"db-lanes\" *ngIf=\"lanes.length\">\n <button type=\"button\" class=\"db-lane\" *ngFor=\"let l of lanes\" [class.active]=\"lane === l.key\" (click)=\"setLane(l.key)\">\n <mat-icon *ngIf=\"l.icon\">{{ l.icon }}</mat-icon>{{ l.label }}\n </button>\n </div>\n\n <!-- Added: the library owns the loading state \u2014 first load only, a refresh keeps the page on screen -->\n <div class=\"db-loading\" *ngIf=\"loading && !book\">{{ config.loadingMessage || 'Loading\u2026' }}</div>\n\n <ng-container *ngIf=\"book\">\n\n <!-- Stage strip \u2014 spa-tiles by default (zero-count tiles hide), or a left-to-right cycle strip where a\n zero stage greys rather than disappears (stageStyle: 'strip') -->\n <div class=\"db-pipe\" *ngIf=\"stripMode\">\n <button type=\"button\" class=\"db-stage\" *ngFor=\"let s of stripStages\" [class.empty]=\"!s.count\" [class.alert]=\"s.alert\" (click)=\"goTo(s.stage.key)\" [title]=\"s.stage.hint || ''\">\n <mat-icon>{{ s.stage.icon || 'checklist' }}</mat-icon>\n <span class=\"st-label\">{{ s.stage.label }}</span>\n <span class=\"st-count\">{{ s.count }}</span>\n <!-- Added: the money behind the count. A hire desk reads \"4 jobs / $12,400\" as one fact, not two -->\n <span class=\"st-value\" *ngIf=\"s.value\">{{ s.value | currency: undefined : 'symbol' : '1.0-0' }}</span>\n </button>\n </div>\n <spa-tiles *ngIf=\"!stripMode && tileConfig\" [config]=\"tileConfig\" [data]=\"tileData\" (tileClick)=\"onTileClick($event)\"></spa-tiles>\n\n <!-- All clear -->\n <div class=\"db-clear\" *ngIf=\"allClear\">\n <mat-icon>task_alt</mat-icon>\n <span>{{ config.allClearMessage || 'Nothing outstanding \u2014 all caught up.' }}</span>\n </div>\n\n <!-- Sections. A book carries 9-16 of these cards and, on 'Everything', all of them used to stack in one\n column \u2014 so they are grouped into one collapsible lane group each, with the first open and the rest\n showing only their identity: label, count, money and a chip per list. Sections that declare no lane\n sit above the groups, and with grouping off (one lane, a picked lane, or groupLanes:false) the page\n renders exactly as it did before. -->\n <ng-container *ngFor=\"let s of ungroupedSections\">\n <ng-container *ngTemplateOutlet=\"sectionCard; context: s.outletContext\"></ng-container>\n </ng-container>\n\n <section class=\"db-group\" *ngFor=\"let g of visibleGroups\" [class.closed]=\"g.collapsed\">\n <button type=\"button\" class=\"db-ghead\" (click)=\"toggleGroup(g)\" [attr.aria-expanded]=\"!g.collapsed\">\n <mat-icon *ngIf=\"g.lane.icon\">{{ g.lane.icon }}</mat-icon>\n <span class=\"g-label\">{{ g.lane.label }}</span>\n <span class=\"db-count\">{{ g.count }}</span>\n <span class=\"g-value\" *ngIf=\"g.value\">{{ g.value | currency: undefined : 'symbol' : '1.0-0' }}</span>\n <span class=\"g-lists\">{{ g.sections.length }} {{ g.sections.length === 1 ? 'list' : 'lists' }}</span>\n <mat-icon class=\"g-chev\">{{ g.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n <!-- Collapsed, the group still says what it is about \u2014 one chip per non-empty list, and a chip is a way\n in: it opens the group and scrolls to that section, the same path a stage click takes. -->\n <div class=\"db-chips\" *ngIf=\"g.collapsed\">\n <button type=\"button\" class=\"db-chip\" *ngFor=\"let c of g.chips\" (click)=\"goTo(c.key)\">{{ c.label }}<span>{{ c.count }}</span></button>\n <span class=\"db-chip-none\" *ngIf=\"!g.chips.length\">Nothing outstanding here</span>\n </div>\n <div class=\"db-gbody\" *ngIf=\"!g.collapsed\">\n <ng-container *ngFor=\"let s of g.sections\">\n <ng-container *ngTemplateOutlet=\"sectionCard; context: s.outletContext\"></ng-container>\n </ng-container>\n </div>\n </section>\n\n <!-- One section card, drawn from two places (flat above, and inside a lane group). An app-supplied row\n template draws the rows inside the library's card chrome, otherwise the section is a spa-table fed\n local rows from the book object. -->\n <ng-template #sectionCard let-s>\n\n <section class=\"db-sec\" *ngIf=\"s.template; else libraryTable\" [id]=\"'sec-' + s.section.key\" [class.alert]=\"s.section.tone === 'alert'\" [class.quiet]=\"s.section.tone === 'quiet'\">\n <header>\n <mat-icon *ngIf=\"s.section.icon\">{{ s.section.icon }}</mat-icon>\n <h2>{{ s.section.label || s.section.key }}</h2>\n <span class=\"db-count\">{{ s.count }}</span>\n <!-- Added: the section-level slot \u2014 batch actions (\"Invoice all 12\") and the money subtotal beside the\n count. It acts on the whole list, so it sits in the header rather than travelling on a row. -->\n <ng-container *ngIf=\"s.headerTemplate\">\n <ng-container *ngTemplateOutlet=\"s.headerTemplate; context: s.headerContext\"></ng-container>\n </ng-container>\n <button type=\"button\" class=\"db-collapse\" (click)=\"toggleSection(s)\" [attr.aria-expanded]=\"!s.collapsed\">\n <mat-icon>{{ s.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n </header>\n <p class=\"db-why\" *ngIf=\"s.caption\">{{ s.caption }}</p>\n <ng-container *ngFor=\"let ctx of s.contexts\">\n <ng-container *ngTemplateOutlet=\"s.template; context: ctx\"></ng-container>\n </ng-container>\n <button type=\"button\" class=\"db-more\" *ngIf=\"hiddenCount(s)\" (click)=\"showMore(s)\">Show {{ hiddenCount(s) }} more</button>\n </section>\n\n <ng-template #libraryTable>\n <mat-card class=\"db-section\" [id]=\"'sec-' + s.section.key\">\n <spa-table [config]=\"s.cfg\" [data]=\"s.rows\"></spa-table>\n </mat-card>\n </ng-template>\n\n </ng-template>\n\n </ng-container>\n\n</div>\n\n<!-- Graceful empty state (feature not configured) -->\n<div class=\"db-empty\" *ngIf=\"!config\">\n <mat-icon>checklist</mat-icon>\n <p>The Day Book is not configured for this app.</p>\n</div>\n", styles: [":host{--db-accent: #1565c0;--db-accent-soft: #e3f2fd;--db-alert: #e53935;--db-alert-text: #c62828;--db-quiet: #b0bec5}.db-page{max-width:1200px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.db-head{display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap}.db-head-text h1{margin:0;font-size:24px}.db-head-text .db-sub{margin:4px 0 0;font-size:14px;color:#0000008c}.db-caption{font-size:12px;color:#0000008c}.db-owed{font-size:13px;color:#000000b3;margin-left:auto}.db-head-actions{display:flex;gap:8px;margin-left:auto}.db-lanes{display:flex;gap:6px;flex-wrap:wrap;align-items:center}.db-lane{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(0,0,0,.12);border-radius:16px;background:transparent;padding:4px 14px;font:inherit;font-size:13px;cursor:pointer;transition:border-color .15s,background .15s}.db-lane:hover{border-color:#90a4ae}.db-lane.active{border-color:var(--db-accent);background:var(--db-accent-soft);color:var(--db-accent);font-weight:500}.db-lane mat-icon{font-size:17px;width:17px;height:17px}.db-loading{padding:40px;text-align:center;color:#00000080}.db-pipe{display:flex;gap:6px;overflow-x:auto;padding-bottom:4px}.db-stage{flex:1 1 0;min-width:104px;display:flex;flex-direction:column;align-items:flex-start;gap:2px;border:1px solid #e0e0e0;border-top:3px solid #bdbdbd;border-radius:5px;background:#fff;padding:9px 11px 10px;font:inherit;cursor:pointer;text-align:left;position:relative}.db-stage:hover{background:#fafbfa}.db-stage mat-icon{font-size:18px;width:18px;height:18px;color:#00000073}.db-stage .st-label{font-size:11.5px;color:#0009;text-transform:uppercase;letter-spacing:.3px;white-space:normal;line-height:1.25;min-height:2.5em}.db-stage .st-count{font-size:21px;font-weight:600;line-height:1.1;font-variant-numeric:tabular-nums}.db-stage .st-value{font-size:11.5px;color:#00000080;font-variant-numeric:tabular-nums}.db-stage.empty{opacity:.45}.db-stage.alert{border-top-color:var(--db-alert)}.db-stage.alert .st-count{color:var(--db-alert-text)}.db-stage:after{content:\"\\203a\";position:absolute;right:-6px;top:50%;transform:translateY(-50%);color:#00000038;font-size:18px;z-index:1}.db-stage:last-child:after{content:none}.db-clear{display:flex;align-items:center;gap:10px;border:1px solid #c8e6c9;background:#f6fbf6;border-radius:10px;padding:14px 16px;color:#2e7d32}.db-section{padding:12px 16px}.db-sec{background:#fff;border:1px solid #e0e0e0;border-left:4px solid var(--db-accent);border-radius:6px;overflow:hidden;scroll-margin-top:12px}.db-sec.alert{border-left-color:var(--db-alert)}.db-sec.alert>header mat-icon{color:var(--db-alert)}.db-sec.quiet{border-left-color:var(--db-quiet)}.db-sec.quiet>header mat-icon{color:#78909c}.db-sec>header{display:flex;align-items:center;gap:12px;padding:14px 18px 10px;flex-wrap:wrap}.db-sec>header h2{margin:0;font-size:17px;font-weight:500}.db-sec>header mat-icon{font-size:22px;width:22px;height:22px;color:var(--db-accent)}.db-count{background:#00000012;border-radius:11px;padding:1px 9px;font-size:13px;font-weight:600;font-variant-numeric:tabular-nums}.db-why{margin:0 18px 10px;color:#0000008c;font-size:13px;max-width:82ch}.db-collapse{border:0;background:none;padding:2px;cursor:pointer;color:#00000073;display:inline-flex;margin-left:auto}.db-collapse:hover{color:#000000bf}.db-collapse mat-icon{font-size:22px;width:22px;height:22px}.db-more{border:0;background:none;padding:10px 18px 12px;font:inherit;font-size:13px;color:var(--db-accent);cursor:pointer;border-top:1px solid #eeeeee;width:100%;text-align:left}.db-more:hover{text-decoration:underline}.db-group{display:flex;flex-direction:column;gap:12px}.db-group.closed{gap:0}.db-ghead{display:flex;align-items:center;gap:10px;width:100%;border:1px solid #e0e0e0;border-radius:6px;background:#fafafa;padding:11px 14px;font:inherit;cursor:pointer;text-align:left;transition:background .15s,border-color .15s}.db-ghead:hover{background:#f2f5f7;border-color:#cfd8dc}.db-group.closed .db-ghead{border-bottom-left-radius:0;border-bottom-right-radius:0}.db-ghead>mat-icon{font-size:20px;width:20px;height:20px;color:var(--db-accent)}.db-ghead .g-label{font-size:15px;font-weight:600;letter-spacing:.2px}.db-ghead .g-value{font-size:13px;font-weight:600;color:#000000b3;font-variant-numeric:tabular-nums}.db-ghead .g-lists{font-size:12px;color:#00000073;margin-left:auto}.db-ghead .g-chev{font-size:22px;width:22px;height:22px;color:#00000073}.db-chips{display:flex;flex-wrap:wrap;gap:6px;border:1px solid #e0e0e0;border-top:0;border-radius:0 0 6px 6px;background:#fff;padding:9px 14px 11px}.db-chip{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(0,0,0,.12);border-radius:14px;background:transparent;padding:2px 6px 2px 11px;font:inherit;font-size:12.5px;color:#000000b3;cursor:pointer}.db-chip:hover{border-color:var(--db-accent);color:var(--db-accent)}.db-chip span{background:#00000012;border-radius:10px;padding:0 7px;font-weight:600;font-variant-numeric:tabular-nums}.db-chip-none{font-size:12.5px;color:#00000073}.db-gbody{display:flex;flex-direction:column;gap:12px}.db-empty{padding:48px;text-align:center;color:#00000080}@media (max-width: 760px){.db-stage:after{content:none}}\n"] }]
17981
+ args: [{ selector: 'spa-day-book', standalone: false, template: "<div class=\"db-page\" *ngIf=\"config\">\n\n <!-- Header: title, subtitle, books-from caption, owed total, app-supplied actions -->\n <div class=\"db-head\">\n <div class=\"db-head-text\">\n <h1>{{ title }}</h1>\n <p class=\"db-sub\" *ngIf=\"config.subtitle\">{{ config.subtitle }}</p>\n <span class=\"db-caption\" *ngIf=\"booksFrom\">books from {{ booksFrom | date: 'mediumDate' }}</span>\n </div>\n <span class=\"db-owed\" *ngIf=\"owed !== null\">Owed to us: <b>{{ owed | currency }}</b></span>\n <!-- The page's own actions (refresh, an escape hatch to a full screen). Changed: they now come from\n config.actions, because an app with no component of its own has nothing to project with \u2014 the\n ng-content slot stays beside them for anyone who does host the page themselves. -->\n <!-- Changed: an action WITH an icon renders icon-only (owner, 2026-08-08 \u2014 \"the refresh and all trips\n buttons can just be icons with no text\"). The label is not lost, it moves to the tooltip and to\n aria-label, so the control still announces itself to a screen reader and on hover. An action with\n no icon keeps its text button, because an unlabelled blank button would be unusable. -->\n <div class=\"db-head-actions\">\n <ng-container *ngFor=\"let a of pageActions\">\n <button mat-icon-button color=\"primary\" type=\"button\" *ngIf=\"a.icon?.name; else textAction\" (click)=\"pageActionClicked(a)\" [matTooltip]=\"a.tip || a.display || a.name\" [attr.aria-label]=\"a.display || a.name\">\n <mat-icon>{{ a.icon.name }}</mat-icon>\n </button>\n <ng-template #textAction>\n <button mat-stroked-button color=\"primary\" type=\"button\" (click)=\"pageActionClicked(a)\" [title]=\"a.tip || ''\">{{ a.display || a.name }}</button>\n </ng-template>\n </ng-container>\n <ng-content select=\"[dayBookActions]\"></ng-content>\n </div>\n </div>\n\n <!-- Lane pills -->\n <div class=\"db-lanes\" *ngIf=\"lanes.length\">\n <button type=\"button\" class=\"db-lane\" *ngFor=\"let l of lanes\" [class.active]=\"lane === l.key\" (click)=\"setLane(l.key)\">\n <mat-icon *ngIf=\"l.icon\">{{ l.icon }}</mat-icon>{{ l.label }}\n </button>\n </div>\n\n <!-- Added: the library owns the loading state \u2014 first load only, a refresh keeps the page on screen.\n Changed: binds showLoading, the copy that only ever moves between change-detection passes (NG0100) -->\n <div class=\"db-loading\" *ngIf=\"showLoading && !book\">{{ config.loadingMessage || 'Loading\u2026' }}</div>\n\n <ng-container *ngIf=\"book\">\n\n <!-- The stage row. Changed: always spa-tiles \u2014 zero-count tiles hide -->\n <spa-tiles *ngIf=\"tileConfig\" [config]=\"tileConfig\" [data]=\"tileData\" (tileClick)=\"onTileClick($event)\"></spa-tiles>\n\n <!-- All clear -->\n <div class=\"db-clear\" *ngIf=\"allClear\">\n <mat-icon>task_alt</mat-icon>\n <span>{{ config.allClearMessage || 'Nothing outstanding \u2014 all caught up.' }}</span>\n </div>\n\n <!-- Sections. A book carries 9-16 of these cards and, on 'Everything', all of them used to stack in one\n column \u2014 so they are grouped into one collapsible lane group each, with the first open and the rest\n showing only their identity: label, count, money and a chip per list. Sections that declare no lane\n sit above the groups, and with grouping off (one lane, a picked lane, or groupLanes:false) the page\n renders exactly as it did before. -->\n <ng-container *ngFor=\"let s of ungroupedSections\">\n <ng-container *ngTemplateOutlet=\"sectionCard; context: s.outletContext\"></ng-container>\n </ng-container>\n\n <section class=\"db-group\" *ngFor=\"let g of visibleGroups\" [class.closed]=\"g.collapsed\">\n <button type=\"button\" class=\"db-ghead\" (click)=\"toggleGroup(g)\" [attr.aria-expanded]=\"!g.collapsed\">\n <mat-icon *ngIf=\"g.lane.icon\">{{ g.lane.icon }}</mat-icon>\n <span class=\"g-label\">{{ g.lane.label }}</span>\n <span class=\"db-count\">{{ g.count }}</span>\n <span class=\"g-value\" *ngIf=\"g.value\">{{ g.value | currency: undefined : 'symbol' : '1.0-0' }}</span>\n <span class=\"g-lists\">{{ g.sections.length }} {{ g.sections.length === 1 ? 'list' : 'lists' }}</span>\n <mat-icon class=\"g-chev\">{{ g.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n <!-- Collapsed, the group still says what it is about \u2014 one chip per non-empty list, and a chip is a way\n in: it opens the group and scrolls to that section, the same path a stage click takes. -->\n <div class=\"db-chips\" *ngIf=\"g.collapsed\">\n <button type=\"button\" class=\"db-chip\" *ngFor=\"let c of g.chips\" (click)=\"goTo(c.key)\">{{ c.label }}<span>{{ c.count }}</span></button>\n <span class=\"db-chip-none\" *ngIf=\"!g.chips.length\">Nothing outstanding here</span>\n </div>\n <div class=\"db-gbody\" *ngIf=\"!g.collapsed\">\n <ng-container *ngFor=\"let s of g.sections\">\n <ng-container *ngTemplateOutlet=\"sectionCard; context: s.outletContext\"></ng-container>\n </ng-container>\n </div>\n </section>\n\n <!-- One section card, drawn from two places (flat above, and inside a lane group). Changed: ONE rendering\n path \u2014 a section is a spa-table fed local rows off the book object. The table draws its own section\n header (icon, title, count, chips, buttons, caption, chevron); (actionSuccess) reloads the book so a\n row that has just been actioned leaves the list instead of sitting there looking undone \u2014 the section\n has no loadAction of its own to refresh from. -->\n <ng-template #sectionCard let-s>\n <mat-card class=\"db-section\" [id]=\"'sec-' + s.section.key\" [class.alert]=\"s.section.tone === 'alert'\" [class.quiet]=\"s.section.tone === 'quiet'\">\n <!-- the \"why this list exists\" line rides inside the table's own header, under the title. With no\n section header there is nothing to ride, so the card carries it instead. -->\n <p class=\"db-why\" *ngIf=\"s.caption && !s.cfg?.sectionConfig\">{{ s.caption }}</p>\n <spa-table [config]=\"s.cfg\" [data]=\"s.rows\" (actionSuccess)=\"load()\"></spa-table>\n </mat-card>\n </ng-template>\n\n </ng-container>\n\n</div>\n\n<!-- Graceful empty state (feature not configured) -->\n<div class=\"db-empty\" *ngIf=\"!config\">\n <mat-icon>checklist</mat-icon>\n <p>The Day Book is not configured for this app.</p>\n</div>\n", styles: [":host{--db-accent: #1565c0;--db-accent-soft: #e3f2fd;--db-alert: #e53935;--db-alert-text: #c62828;--db-quiet: #b0bec5}.db-page{max-width:1200px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.db-head{display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap}.db-head-text h1{margin:0;font-size:24px}.db-head-text .db-sub{margin:4px 0 0;font-size:14px;color:#0000008c}.db-caption{font-size:12px;color:#0000008c}.db-owed{font-size:13px;color:#000000b3;margin-left:auto}.db-head-actions{display:flex;gap:8px;margin-left:auto}.db-lanes{display:flex;gap:6px;flex-wrap:wrap;align-items:center}.db-lane{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(0,0,0,.12);border-radius:16px;background:transparent;padding:4px 14px;font:inherit;font-size:13px;cursor:pointer;transition:border-color .15s,background .15s}.db-lane:hover{border-color:#90a4ae}.db-lane.active{border-color:var(--db-accent);background:var(--db-accent-soft);color:var(--db-accent);font-weight:500}.db-lane mat-icon{font-size:17px;width:17px;height:17px}@media (max-width: 700px){.db-head{gap:6px 10px;align-items:center}.db-head-text{flex:1 1 auto;min-width:0;order:1}.db-head-text h1{font-size:20px}.db-head-text .db-sub{display:none}.db-caption{font-size:11px}.db-head-actions{order:2;margin-left:auto;gap:2px}.db-owed{order:3;flex:1 0 100%;margin-left:0;font-size:13px}.db-lanes{flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}.db-lanes::-webkit-scrollbar{display:none}.db-lane{flex:0 0 auto}}.db-loading{padding:40px;text-align:center;color:#00000080}.db-clear{display:flex;align-items:center;gap:10px;border:1px solid #c8e6c9;background:#f6fbf6;border-radius:10px;padding:14px 16px;color:#2e7d32}.db-section{padding:12px 16px;scroll-margin-top:12px;border-left:4px solid transparent}.db-section.alert{border-left-color:var(--db-alert)}.db-section.quiet{border-left-color:var(--db-quiet)}.db-count{background:#00000012;border-radius:11px;padding:1px 9px;font-size:13px;font-weight:600;font-variant-numeric:tabular-nums}.db-why{margin:0 0 10px;color:#0000008c;font-size:13px;max-width:82ch}.db-group{display:flex;flex-direction:column;gap:12px}.db-group.closed{gap:0}.db-ghead{display:flex;align-items:center;gap:10px;width:100%;border:1px solid #e0e0e0;border-radius:6px;background:#fafafa;padding:11px 14px;font:inherit;cursor:pointer;text-align:left;transition:background .15s,border-color .15s}.db-ghead:hover{background:#f2f5f7;border-color:#cfd8dc}.db-group.closed .db-ghead{border-bottom-left-radius:0;border-bottom-right-radius:0}.db-ghead>mat-icon{font-size:20px;width:20px;height:20px;color:var(--db-accent)}.db-ghead .g-label{font-size:15px;font-weight:600;letter-spacing:.2px}.db-ghead .g-value{font-size:13px;font-weight:600;color:#000000b3;font-variant-numeric:tabular-nums}.db-ghead .g-lists{font-size:12px;color:#00000073;margin-left:auto}.db-ghead .g-chev{font-size:22px;width:22px;height:22px;color:#00000073}.db-chips{display:flex;flex-wrap:wrap;gap:6px;border:1px solid #e0e0e0;border-top:0;border-radius:0 0 6px 6px;background:#fff;padding:9px 14px 11px}.db-chip{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(0,0,0,.12);border-radius:14px;background:transparent;padding:2px 6px 2px 11px;font:inherit;font-size:12.5px;color:#000000b3;cursor:pointer}.db-chip:hover{border-color:var(--db-accent);color:var(--db-accent)}.db-chip span{background:#00000012;border-radius:10px;padding:0 7px;font-weight:600;font-variant-numeric:tabular-nums}.db-chip-none{font-size:12.5px;color:#00000073}.db-gbody{display:flex;flex-direction:column;gap:12px}.db-empty{padding:48px;text-align:center;color:#00000080}\n"] }]
17893
17982
  }], propDecorators: { tables: [{
17894
17983
  type: ViewChildren,
17895
17984
  args: [TableComponent]
17896
- }], rowTemplates: [{
17897
- type: ContentChildren,
17898
- args: [DayBookSectionDirective, { descendants: true }]
17899
- }], headerTemplates: [{
17900
- type: ContentChildren,
17901
- args: [DayBookSectionHeaderDirective, { descendants: true }]
17902
17985
  }], dayBookConfig: [{
17903
17986
  type: Input
17904
17987
  }] } });
@@ -19905,7 +19988,7 @@ class AgentComponent {
19905
19988
  }
19906
19989
  }
19907
19990
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AgentComponent, deps: [{ token: AgentService }, { token: SignalRService }, { token: i0.NgZone }, { token: i1$1.Router }, { token: i1$4.BreakpointObserver }], target: i0.ɵɵFactoryTarget.Component }); }
19908
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: AgentComponent, isStandalone: false, selector: "spa-agent", inputs: { pageMode: "pageMode" }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, viewQueries: [{ propertyName: "messageContainer", first: true, predicate: ["messageContainer"], descendants: true }, { propertyName: "messageInput", first: true, predicate: ["messageInput"], descendants: true }, { propertyName: "cameraInput", first: true, predicate: ["cameraInput"], descendants: true }, { propertyName: "galleryInput", first: true, predicate: ["galleryInput"], descendants: true }], ngImport: i0, template: "<!-- Floating chat widget for in-app Agent (renamed from Assistant) -->\n\n<!-- FAB toggle button -->\n<!-- Changed: the page presentation has no FAB \u2014 the page IS the chat, so a button to open it is nonsense.\n Changed: showFab also stands the FAB down on mobile (the bottom tab bar's Chat tab replaces it) and on\n the home/agent route at any width, where it would otherwise hover over the page it opens. -->\n<button mat-fab class=\"agent-fab\" *ngIf=\"showFab\" (click)=\"toggleChat()\" [matTooltip]=\"agentName\">\n <mat-icon>{{ isOpen ? 'close' : 'chat_bubble' }}</mat-icon>\n</button>\n\n<!-- Chat window -->\n<!-- Changed: rendered unconditionally in page mode (isOpen only governs the floating window); .page-mode\n swaps the fixed overlay for in-flow layout and 'none' opts out of the slide-up animation -->\n<!-- Changed: showWindow keeps the floating window off wherever its FAB is off, so mobile and the chat route\n can never show two chat surfaces at once -->\n<div class=\"agent-window\" [class.page-mode]=\"pageMode\" *ngIf=\"showWindow\" [@slideUp]=\"pageMode ? 'none' : 'float'\">\n\n <!-- Header with online status -->\n <div class=\"agent-header\">\n <div class=\"header-info\">\n <mat-icon class=\"header-icon\">smart_toy</mat-icon>\n <div class=\"header-text\">\n <span class=\"header-title\">{{ agentName }}</span>\n <span class=\"status-badge\" [class.online]=\"isOnline\" [class.offline]=\"!isOnline\">\n <span class=\"status-dot\"></span>\n {{ isOnline ? 'Online' : 'Offline' }}\n </span>\n </div>\n </div>\n <div class=\"header-actions\">\n <!-- Changed: earlier conversations existed but were unreachable \u2014 the widget only ever showed the newest -->\n <button mat-icon-button matTooltip=\"Conversations\" (click)=\"toggleConversationList()\">\n <mat-icon>forum</mat-icon>\n </button>\n <button mat-icon-button matTooltip=\"New conversation\" (click)=\"newConversation()\">\n <mat-icon>add_comment</mat-icon>\n </button>\n <!-- Changed: minimise is float-only \u2014 there is nothing to minimise a routed page into -->\n <button mat-icon-button matTooltip=\"Close\" *ngIf=\"!pageMode\" (click)=\"toggleChat()\">\n <mat-icon>remove</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Conversation switcher. A dropdown rather than a panel: the window is small and this is a jump list,\n not somewhere the user spends time. -->\n <div class=\"agent-conversations\" *ngIf=\"showConversations\">\n <div class=\"conversation-row\" *ngFor=\"let c of conversations\"\n [class.current]=\"c.conversationID === currentConversationId\"\n (click)=\"selectConversation(c.conversationID)\">\n <mat-icon class=\"conversation-icon\">chat</mat-icon>\n <span class=\"conversation-title\">{{ c.title }}</span>\n </div>\n <div class=\"conversation-empty\" *ngIf=\"conversations.length === 0\">No earlier conversations</div>\n </div>\n\n <!-- Changed: the session banner. The agent has always known it was holding a half-built record; until the\n reply carried structured state, that fact only existed as prose the user had to read carefully. -->\n <div class=\"agent-session-banner\" *ngIf=\"sessionInfo?.sessionType\">\n <mat-icon class=\"session-icon\">edit_note</mat-icon>\n <span class=\"session-text\">{{ sessionVerb }} a {{ sessionInfo?.modelName }}</span>\n <!-- Changed: chips rather than a sentence. What is still required is the one thing the user has to act\n on, and a chip per field is scannable where a comma-separated list inside a sentence is not. -->\n <span class=\"session-chip\" *ngFor=\"let field of sessionInfo?.missingFields\">{{ field | camelToWords }}</span>\n </div>\n\n <!-- Messages area -->\n <div class=\"agent-messages\" #messageContainer>\n\n <!-- Welcome experience (show when no messages) -->\n <div class=\"agent-greeting\" *ngIf=\"messages.length === 0\">\n <mat-icon class=\"greeting-icon\">smart_toy</mat-icon>\n <p class=\"greeting-text\">{{ greeting }}</p>\n\n <!-- Three capability pillars -->\n <div class=\"capability-pillars\">\n\n <!-- Pillar 1: Navigate & Inquire -->\n <div class=\"pillar\" (click)=\"sendSuggested('How do I post a transaction?')\">\n <div class=\"pillar-header\">\n <mat-icon class=\"pillar-icon\">explore</mat-icon>\n <span class=\"pillar-title\">Ask & Navigate</span>\n </div>\n <p class=\"pillar-desc\">Ask how to do things or find features in the app</p>\n <span class=\"pillar-example\">\"How do I post a transaction?\"</span>\n </div>\n\n <!-- Pillar 2: Create & Modify -->\n <div class=\"pillar\" (click)=\"sendSuggested('Create a new customer')\">\n <div class=\"pillar-header\">\n <mat-icon class=\"pillar-icon\">edit_note</mat-icon>\n <span class=\"pillar-title\">Create & Modify</span>\n </div>\n <p class=\"pillar-desc\">Add, edit, or delete records using natural language</p>\n <span class=\"pillar-example\">\"Create a new customer called Acme Ltd\"</span>\n </div>\n\n <!-- Pillar 3: Get Information -->\n <div class=\"pillar\" (click)=\"sendSuggested('List all accounts')\">\n <div class=\"pillar-header\">\n <mat-icon class=\"pillar-icon\">search</mat-icon>\n <span class=\"pillar-title\">Get Information</span>\n </div>\n <p class=\"pillar-desc\">Retrieve details, list records, or get summaries</p>\n <span class=\"pillar-example\">\"Show me the details of Customer A\"</span>\n </div>\n\n </div>\n </div>\n\n <!-- Message bubbles -->\n <ng-container *ngFor=\"let msg of messages; trackBy: trackMessage\">\n\n <!-- Changed: a conversation boundary is drawn, not spoken. Without a visible line, the agent starting a\n fresh thread after a long gap just looks like it forgetting what was said. -->\n <div class=\"conversation-divider\" *ngIf=\"msg.divider\">\n <span class=\"divider-label\">{{ msg.content }}</span>\n </div>\n\n <div *ngIf=\"!msg.divider\" class=\"message-row\" [class.user-row]=\"msg.role === 'user'\" [class.agent-row]=\"msg.role === 'assistant'\">\n <!-- Changed: agent replies are markdown and were being printed verbatim, so a carefully formatted\n answer arrived as a wall of asterisks. User messages stay plain text \u2014 they are not markdown,\n and interpreting them as such would be putting words in the user's mouth. -->\n <div class=\"message-bubble\" [class.user-bubble]=\"msg.role === 'user'\" [class.agent-bubble]=\"msg.role === 'assistant'\" [class.pending-bubble]=\"msg.pending\">\n <span *ngIf=\"msg.role === 'assistant'\" [innerHTML]=\"msg.content | agentMarkdown\"></span>\n <span *ngIf=\"msg.role !== 'assistant'\">{{ msg.content }}</span>\n </div>\n </div>\n\n </ng-container>\n\n <!-- Typing indicator -->\n <div class=\"message-row agent-row\" *ngIf=\"isTyping\">\n <div class=\"message-bubble agent-bubble typing-bubble\">\n <span class=\"typing-dot\"></span>\n <span class=\"typing-dot\"></span>\n <span class=\"typing-dot\"></span>\n </div>\n </div>\n\n </div>\n\n <!-- Media error (mic blocked / unsupported browser) -->\n <div class=\"agent-media-error\" *ngIf=\"mediaError\">\n <mat-icon class=\"media-error-icon\">error_outline</mat-icon>\n <span>{{ mediaError }}</span>\n </div>\n\n <!-- Staged attachment: a photo waits here until the user adds a caption and presses send -->\n <div class=\"agent-attachment\" *ngIf=\"pendingPhoto\">\n <mat-icon class=\"attach-icon\">image</mat-icon>\n <span class=\"attach-label\">Photo attached</span>\n <button mat-icon-button class=\"attach-remove\" matTooltip=\"Remove photo\" (click)=\"clearPhoto()\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <!-- Transcribing indicator for the Stop button -->\n <div class=\"agent-transcribing\" *ngIf=\"isTranscribing\">\n <mat-icon class=\"transcribing-icon\">graphic_eq</mat-icon>\n <span>Transcribing\u2026</span>\n </div>\n\n <!-- Voice mode: replaces the composer entirely. Everything here is sized to be used without looking \u2014\n a big state indicator, the last exchange, and a thumb-sized exit. -->\n <div class=\"agent-voice\" *ngIf=\"voiceMode\">\n\n <div class=\"voice-state\" [class.is-starting]=\"voiceState === 'starting'\"\n [class.is-listening]=\"voiceState === 'listening' || voiceState === 'capturing'\"\n [class.is-thinking]=\"voiceState === 'thinking'\" [class.is-speaking]=\"voiceState === 'speaking'\">\n <mat-icon class=\"voice-state-icon\">{{ voiceStateIcon }}</mat-icon>\n <span class=\"voice-state-label\">{{ voiceStateLabel }}</span>\n </div>\n\n <!-- Live level, so the mic being open is visible as well as audible. Not shown while calibrating: the\n meter would invite exactly the talking that ruins the measurement. -->\n <div class=\"voice-meter\" *ngIf=\"voiceState === 'listening' || voiceState === 'capturing'\">\n <div class=\"voice-meter-fill\" [style.width.%]=\"voiceLevel * 100\"></div>\n </div>\n\n <!-- Deliberately NO transcript/reply repeated here: both already appear in the chat thread directly above,\n and duplicating them pushed the state indicator \u2014 the one thing that must be readable at a glance \u2014\n down the panel. The state, the meter and the exit are all this needs. -->\n\n <!-- Says explicitly that pauses are fine, because the natural worry is that stopping to think will send -->\n <p class=\"voice-hint\" *ngIf=\"voiceState === 'starting'\">Checking the background noise \u2014 hold on\u2026</p>\n <p class=\"voice-hint\" *ngIf=\"voiceState === 'capturing'\">Take your time \u2014 it waits until you're done.</p>\n <p class=\"voice-hint\" *ngIf=\"voiceState === 'listening'\">Just speak \u2014 it sends when you finish.</p>\n\n <button mat-flat-button color=\"warn\" class=\"voice-exit\" (click)=\"stopVoiceMode()\">\n <mat-icon>close</mat-icon>\n <span>Exit voice mode</span>\n </button>\n </div>\n\n <!-- Input area -->\n <div class=\"agent-input\" [class.recording]=\"isRecording\" *ngIf=\"!voiceMode\">\n\n <!-- Normal input: attach, text, voice note, hands-free, send -->\n <ng-container *ngIf=\"!isRecording\">\n <button mat-icon-button class=\"media-btn\" matTooltip=\"Attach\" [matMenuTriggerFor]=\"attachMenu\">\n <mat-icon>add</mat-icon>\n </button>\n <mat-form-field appearance=\"outline\" class=\"input-field\">\n <input matInput #messageInput placeholder=\"Type, speak or snap...\" [(ngModel)]=\"inputText\" (keydown)=\"onKeydown($event)\" autocomplete=\"off\" />\n </mat-form-field>\n <!-- Tap records a voice note; HOLD enters hands-free voice mode. Deliberately no separate button for\n voice mode: the row is already at capacity, and a fifth control would crowd out the text field on a\n phone. The gesture is the accelerator, the \"+\" menu below is the discoverable path to the same\n thing \u2014 a gesture nobody can see is a feature nobody finds. -->\n <button mat-icon-button class=\"media-btn mic-btn\" [class.arming]=\"micHeld\"\n matTooltip=\"Tap to record \u00B7 hold for hands-free\"\n (click)=\"onMicClick()\"\n (pointerdown)=\"onMicPressStart()\" (pointerup)=\"onMicPressEnd()\"\n (pointerleave)=\"onMicPressEnd()\" (pointercancel)=\"onMicPressEnd()\">\n <mat-icon>mic</mat-icon>\n </button>\n <button mat-icon-button color=\"primary\" class=\"send-btn\" (click)=\"sendMessage()\" [disabled]=\"!canSend\">\n <mat-icon>send</mat-icon>\n </button>\n </ng-container>\n\n <!-- Recording bar: replaces the input while a voice note is being captured.\n Three exits \u2014 discard it, stop and read it back as text, or send it straight away. -->\n <ng-container *ngIf=\"isRecording\">\n <button mat-icon-button class=\"media-btn cancel-btn\" matTooltip=\"Discard\" (click)=\"cancelRecording()\">\n <mat-icon>delete</mat-icon>\n </button>\n <div class=\"recording-status\">\n <span class=\"rec-dot\"></span>\n <span class=\"rec-label\">Recording</span>\n <span class=\"rec-time\">{{ recordDisplay }}</span>\n </div>\n <button mat-icon-button class=\"media-btn stop-btn\" matTooltip=\"Stop and add as text\" (click)=\"stopAndTranscribe()\">\n <mat-icon>stop_circle</mat-icon>\n </button>\n <button mat-icon-button color=\"primary\" class=\"send-btn\" matTooltip=\"Send now\" (click)=\"stopRecording()\">\n <mat-icon>send</mat-icon>\n </button>\n </ng-container>\n\n <!-- Attach menu \u2014 keeps the row compact: one \"+\" instead of a button per source -->\n <mat-menu #attachMenu=\"matMenu\">\n <button mat-menu-item (click)=\"takePhoto()\">\n <mat-icon>photo_camera</mat-icon>\n <span>Take photo</span>\n </button>\n <button mat-menu-item (click)=\"choosePhoto()\">\n <mat-icon>photo_library</mat-icon>\n <span>Choose photo</span>\n </button>\n <!-- The discoverable way into voice mode. The mic long-press does the same thing faster, but only\n once you know it exists \u2014 this is how you find out. -->\n <button mat-menu-item (click)=\"startVoiceMode()\">\n <mat-icon>record_voice_over</mat-icon>\n <span>Hands-free voice mode</span>\n </button>\n </mat-menu>\n\n <!-- Two separate pickers on purpose. `capture=\"environment\"` hands off to the phone's camera app and\n opens the rear lens; WITHOUT it the same input opens the gallery/file picker. One input cannot do\n both, which is why the old single button always went to the camera on mobile. -->\n <input type=\"file\" #cameraInput accept=\"image/*\" capture=\"environment\" hidden (change)=\"onPhotoSelected($event)\" />\n <input type=\"file\" #galleryInput accept=\"image/*\" hidden (change)=\"onPhotoSelected($event)\" />\n </div>\n\n</div>\n", styles: [".agent-fab{position:fixed;bottom:24px;right:24px;z-index:1001;background:#1976d2;color:#fff}.agent-window{position:fixed;bottom:96px;right:24px;width:400px;height:580px;background:#fff;border-radius:16px;box-shadow:0 8px 32px #00000026;display:flex;flex-direction:column;z-index:1000;overflow:hidden}.agent-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#1976d2;color:#fff;min-height:56px}.header-info{display:flex;align-items:center;gap:10px}.header-icon{font-size:24px;width:24px;height:24px}.header-text{display:flex;flex-direction:column;gap:1px}.header-title{font-size:16px;font-weight:500;line-height:1.2}.status-badge{display:flex;align-items:center;gap:4px;font-size:11px;font-weight:400;opacity:.9;line-height:1}.status-dot{width:7px;height:7px;border-radius:50%;display:inline-block}.status-badge.online .status-dot{background:#4caf50;box-shadow:0 0 4px #4caf5099}.status-badge.offline .status-dot{background:#9e9e9e}.header-actions{display:flex;gap:0}.header-actions button{color:#fff}.agent-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:8px}.agent-greeting{display:flex;flex-direction:column;align-items:center;text-align:center;padding:20px 8px 8px;gap:10px}.greeting-icon{font-size:48px;width:48px;height:48px;color:#1976d2;opacity:.8}.greeting-text{font-size:15px;color:#424242;line-height:1.5;margin:0}.capability-pillars{display:flex;flex-direction:column;gap:10px;margin-top:8px;width:100%}.pillar{border:1px solid #e0e0e0;border-radius:12px;padding:12px 14px;text-align:left;cursor:pointer;transition:all .2s ease;background:#fafafa}.pillar:hover{border-color:#1976d2;background:#1976d20a;box-shadow:0 2px 8px #1976d21a}.pillar-header{display:flex;align-items:center;gap:8px;margin-bottom:4px}.pillar-icon{font-size:20px;width:20px;height:20px;color:#1976d2}.pillar-title{font-size:13px;font-weight:600;color:#212121}.pillar-desc{font-size:12px;color:#616161;margin:0 0 6px;line-height:1.4}.pillar-example{font-size:12px;color:#1976d2;font-style:italic;opacity:.85}.message-row{display:flex;max-width:85%}.user-row{align-self:flex-end}.agent-row{align-self:flex-start}.message-bubble{padding:10px 14px;border-radius:16px;font-size:14px;line-height:1.5;word-break:break-word;white-space:pre-wrap}.user-bubble{background:#1976d2;color:#fff;border-bottom-right-radius:4px}.agent-bubble{background:#f0f0f0;color:#212121;border-bottom-left-radius:4px}.typing-bubble{display:flex;align-items:center;gap:4px;padding:12px 18px}.typing-dot{width:8px;height:8px;border-radius:50%;background:#9e9e9e;animation:typingBounce 1.4s infinite ease-in-out}.typing-dot:nth-child(2){animation-delay:.2s}.typing-dot:nth-child(3){animation-delay:.4s}@keyframes typingBounce{0%,60%,to{transform:translateY(0);opacity:.4}30%{transform:translateY(-6px);opacity:1}}.agent-input{display:flex;align-items:center;padding:8px 12px;border-top:1px solid #e0e0e0;gap:4px}.input-field{flex:1}.input-field ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.input-field ::ng-deep .mat-mdc-text-field-wrapper{padding:0 12px}.send-btn,.media-btn{margin-bottom:0;display:inline-flex;align-items:center;justify-content:center}.media-btn{color:#757575}.media-btn:hover{color:#424242}.cancel-btn:hover{color:#d32f2f}.agent-input.recording{background:#fff5f5}.recording-status{flex:1;display:flex;align-items:center;gap:8px;padding:0 4px;font-size:13px;color:#616161}.rec-label{flex:1}.rec-time{font-variant-numeric:tabular-nums;color:#424242}.rec-dot{width:10px;height:10px;border-radius:50%;background:#e53935;animation:rec-pulse 1.2s ease-in-out infinite}@keyframes rec-pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.85)}}.pending-bubble{opacity:.6;font-style:italic}.agent-attachment{display:flex;align-items:center;gap:8px;padding:6px 12px;font-size:12px;color:#1565c0;background:#e8f1fb;border-top:1px solid #cfe0f3}.attach-icon{font-size:18px;width:18px;height:18px}.attach-label{flex:1}.attach-remove{width:24px;height:24px;min-width:0;padding:0;flex:0 0 auto;line-height:24px;display:inline-flex;align-items:center;justify-content:center;color:#1565c0}.attach-remove .mat-icon{font-size:16px;width:16px;height:16px;line-height:16px}.agent-transcribing{display:flex;align-items:center;gap:8px;padding:6px 12px;font-size:12px;color:#616161;background:#f5f5f5;border-top:1px solid #e0e0e0}.transcribing-icon{font-size:18px;width:18px;height:18px;animation:rec-pulse 1.2s ease-in-out infinite}.stop-btn{color:#e53935}.agent-media-error{display:flex;align-items:center;gap:8px;padding:8px 12px;font-size:12px;color:#c62828;background:#fdecea;border-top:1px solid #f5c6c2}.media-error-icon{font-size:18px;width:18px;height:18px}.mic-btn{transition:transform .12s ease,color .12s ease}.mic-btn.arming{transform:scale(1.25);color:#1565c0}.agent-voice{display:flex;flex-direction:column;align-items:center;gap:12px;padding:20px 16px 18px;border-top:1px solid rgba(0,0,0,.08);background:#fafafa}.voice-state{display:flex;align-items:center;gap:10px;font-size:20px;font-weight:600;letter-spacing:.3px}.voice-state-icon{font-size:28px;width:28px;height:28px}.voice-state.is-starting{color:#00000073}.voice-state.is-listening{color:#2e7d32}.voice-state.is-thinking{color:#ef6c00}.voice-state.is-speaking{color:#1565c0}.voice-state.is-listening .voice-state-icon{animation:voicePulse 1.4s ease-in-out infinite}@keyframes voicePulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.55;transform:scale(1.12)}}.voice-meter{width:100%;max-width:260px;height:6px;border-radius:3px;background:#00000014;overflow:hidden}.voice-meter-fill{height:100%;background:#2e7d32;border-radius:3px;transition:width 80ms linear}.voice-hint{margin:0;font-size:13px;opacity:.6}.voice-exit{width:100%;max-width:280px;height:48px;font-size:15px}.voice-exit mat-icon{margin-right:6px}@media (max-width: 600px){.agent-window{inset:0 0 auto;width:100%;height:100%;height:100dvh;height:var(--agent-vh, 100dvh);border-radius:0}.agent-fab{bottom:72px;right:16px}}.agent-window.page-mode{position:relative;inset:auto;width:100%;max-width:100%;height:calc(var(--agent-vh, 100dvh) - var(--agent-page-offset, 140px));border-radius:12px;border:1px solid #e0e0e0;box-shadow:none;z-index:auto}@media (max-width: 600px){.agent-window.page-mode{height:calc(var(--agent-vh, 100dvh) - var(--agent-page-offset, 72px));border-radius:0;border:none}}.agent-conversations{max-height:180px;overflow-y:auto;border-bottom:1px solid #e0e0e0;background:#fafafa}.conversation-row{display:flex;align-items:center;gap:8px;padding:8px 14px;cursor:pointer;font-size:13px}.conversation-row:hover{background:#eef4fb}.conversation-row.current{background:#e3f0fd;font-weight:500}.conversation-icon{font-size:18px;width:18px;height:18px;opacity:.6}.conversation-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.conversation-empty{padding:10px 14px;font-size:12px;opacity:.6}.agent-session-banner{display:flex;align-items:center;flex-wrap:wrap;gap:6px;padding:6px 14px;background:#fff8e1;border-bottom:1px solid #ffe082;font-size:12px}.session-icon{font-size:16px;width:16px;height:16px;opacity:.7}.session-text{font-weight:500}.session-missing{opacity:.75}.conversation-divider{display:flex;align-items:center;gap:8px;margin:14px 4px;opacity:.55}.conversation-divider:before,.conversation-divider:after{content:\"\";flex:1;height:1px;background:#c8c8c8}.divider-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em}.session-chip{display:inline-block;padding:1px 8px;border-radius:10px;background:#ffe082;font-size:11px;white-space:nowrap}.agent-bubble .md-heading{display:block;margin:4px 0 2px;font-size:13px}.agent-bubble .md-bullet{opacity:.6;margin-right:2px}.agent-bubble code{padding:1px 4px;border-radius:3px;background:#0000000f;font-size:12px}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$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: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i7$2.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7$2.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7$2.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatFabButton, selector: "button[mat-fab], a[mat-fab], button[matFab], a[matFab]", inputs: ["extended"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4$2.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }, { kind: "pipe", type: AgentMarkdownPipe, name: "agentMarkdown" }], animations: [
19991
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: AgentComponent, isStandalone: false, selector: "spa-agent", inputs: { pageMode: "pageMode" }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, viewQueries: [{ propertyName: "messageContainer", first: true, predicate: ["messageContainer"], descendants: true }, { propertyName: "messageInput", first: true, predicate: ["messageInput"], descendants: true }, { propertyName: "cameraInput", first: true, predicate: ["cameraInput"], descendants: true }, { propertyName: "galleryInput", first: true, predicate: ["galleryInput"], descendants: true }], ngImport: i0, template: "<!-- Floating chat widget for in-app Agent (renamed from Assistant) -->\n\n<!-- FAB toggle button -->\n<!-- Changed: the page presentation has no FAB \u2014 the page IS the chat, so a button to open it is nonsense.\n Changed: showFab also stands the FAB down on mobile (the bottom tab bar's Chat tab replaces it) and on\n the home/agent route at any width, where it would otherwise hover over the page it opens. -->\n<button mat-fab class=\"agent-fab\" *ngIf=\"showFab\" (click)=\"toggleChat()\" [matTooltip]=\"agentName\">\n <mat-icon>{{ isOpen ? 'close' : 'chat_bubble' }}</mat-icon>\n</button>\n\n<!-- Chat window -->\n<!-- Changed: rendered unconditionally in page mode (isOpen only governs the floating window); .page-mode\n swaps the fixed overlay for in-flow layout and 'none' opts out of the slide-up animation -->\n<!-- Changed: showWindow keeps the floating window off wherever its FAB is off, so mobile and the chat route\n can never show two chat surfaces at once -->\n<div class=\"agent-window\" [class.page-mode]=\"pageMode\" *ngIf=\"showWindow\" [@slideUp]=\"pageMode ? 'none' : 'float'\">\n\n <!-- Header with online status -->\n <div class=\"agent-header\">\n <div class=\"header-info\">\n <mat-icon class=\"header-icon\">smart_toy</mat-icon>\n <div class=\"header-text\">\n <span class=\"header-title\">{{ agentName }}</span>\n <span class=\"status-badge\" [class.online]=\"isOnline\" [class.offline]=\"!isOnline\">\n <span class=\"status-dot\"></span>\n {{ isOnline ? 'Online' : 'Offline' }}\n </span>\n </div>\n </div>\n <div class=\"header-actions\">\n <!-- Changed: earlier conversations existed but were unreachable \u2014 the widget only ever showed the newest -->\n <button mat-icon-button matTooltip=\"Conversations\" (click)=\"toggleConversationList()\">\n <mat-icon>forum</mat-icon>\n </button>\n <button mat-icon-button matTooltip=\"New conversation\" (click)=\"newConversation()\">\n <mat-icon>add_comment</mat-icon>\n </button>\n <!-- Changed: minimise is float-only \u2014 there is nothing to minimise a routed page into -->\n <button mat-icon-button matTooltip=\"Close\" *ngIf=\"!pageMode\" (click)=\"toggleChat()\">\n <mat-icon>remove</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Conversation switcher. A dropdown rather than a panel: the window is small and this is a jump list,\n not somewhere the user spends time. -->\n <div class=\"agent-conversations\" *ngIf=\"showConversations\">\n <div class=\"conversation-row\" *ngFor=\"let c of conversations\"\n [class.current]=\"c.conversationID === currentConversationId\"\n (click)=\"selectConversation(c.conversationID)\">\n <mat-icon class=\"conversation-icon\">chat</mat-icon>\n <span class=\"conversation-title\">{{ c.title }}</span>\n </div>\n <div class=\"conversation-empty\" *ngIf=\"conversations.length === 0\">No earlier conversations</div>\n </div>\n\n <!-- Changed: the session banner. The agent has always known it was holding a half-built record; until the\n reply carried structured state, that fact only existed as prose the user had to read carefully. -->\n <div class=\"agent-session-banner\" *ngIf=\"sessionInfo?.sessionType\">\n <mat-icon class=\"session-icon\">edit_note</mat-icon>\n <span class=\"session-text\">{{ sessionVerb }} a {{ sessionInfo?.modelName }}</span>\n <!-- Changed: chips rather than a sentence. What is still required is the one thing the user has to act\n on, and a chip per field is scannable where a comma-separated list inside a sentence is not. -->\n <span class=\"session-chip\" *ngFor=\"let field of sessionInfo?.missingFields\">{{ field | camelToWords }}</span>\n </div>\n\n <!-- Messages area -->\n <div class=\"agent-messages\" #messageContainer>\n\n <!-- Welcome experience (show when no messages) -->\n <div class=\"agent-greeting\" *ngIf=\"messages.length === 0\">\n <mat-icon class=\"greeting-icon\">smart_toy</mat-icon>\n <p class=\"greeting-text\">{{ greeting }}</p>\n\n <!-- Three capability pillars -->\n <div class=\"capability-pillars\">\n\n <!-- Pillar 1: Navigate & Inquire -->\n <div class=\"pillar\" (click)=\"sendSuggested('How do I post a transaction?')\">\n <div class=\"pillar-header\">\n <mat-icon class=\"pillar-icon\">explore</mat-icon>\n <span class=\"pillar-title\">Ask & Navigate</span>\n </div>\n <p class=\"pillar-desc\">Ask how to do things or find features in the app</p>\n <span class=\"pillar-example\">\"How do I post a transaction?\"</span>\n </div>\n\n <!-- Pillar 2: Create & Modify -->\n <div class=\"pillar\" (click)=\"sendSuggested('Create a new customer')\">\n <div class=\"pillar-header\">\n <mat-icon class=\"pillar-icon\">edit_note</mat-icon>\n <span class=\"pillar-title\">Create & Modify</span>\n </div>\n <p class=\"pillar-desc\">Add, edit, or delete records using natural language</p>\n <span class=\"pillar-example\">\"Create a new customer called Acme Ltd\"</span>\n </div>\n\n <!-- Pillar 3: Get Information -->\n <div class=\"pillar\" (click)=\"sendSuggested('List all accounts')\">\n <div class=\"pillar-header\">\n <mat-icon class=\"pillar-icon\">search</mat-icon>\n <span class=\"pillar-title\">Get Information</span>\n </div>\n <p class=\"pillar-desc\">Retrieve details, list records, or get summaries</p>\n <span class=\"pillar-example\">\"Show me the details of Customer A\"</span>\n </div>\n\n </div>\n </div>\n\n <!-- Message bubbles -->\n <ng-container *ngFor=\"let msg of messages; trackBy: trackMessage\">\n\n <!-- Changed: a conversation boundary is drawn, not spoken. Without a visible line, the agent starting a\n fresh thread after a long gap just looks like it forgetting what was said. -->\n <div class=\"conversation-divider\" *ngIf=\"msg.divider\">\n <span class=\"divider-label\">{{ msg.content }}</span>\n </div>\n\n <div *ngIf=\"!msg.divider\" class=\"message-row\" [class.user-row]=\"msg.role === 'user'\" [class.agent-row]=\"msg.role === 'assistant'\">\n <!-- Changed: agent replies are markdown and were being printed verbatim, so a carefully formatted\n answer arrived as a wall of asterisks. User messages stay plain text \u2014 they are not markdown,\n and interpreting them as such would be putting words in the user's mouth. -->\n <div class=\"message-bubble\" [class.user-bubble]=\"msg.role === 'user'\" [class.agent-bubble]=\"msg.role === 'assistant'\" [class.pending-bubble]=\"msg.pending\">\n <span *ngIf=\"msg.role === 'assistant'\" [innerHTML]=\"msg.content | agentMarkdown\"></span>\n <span *ngIf=\"msg.role !== 'assistant'\">{{ msg.content }}</span>\n </div>\n </div>\n\n </ng-container>\n\n <!-- Typing indicator -->\n <div class=\"message-row agent-row\" *ngIf=\"isTyping\">\n <div class=\"message-bubble agent-bubble typing-bubble\">\n <span class=\"typing-dot\"></span>\n <span class=\"typing-dot\"></span>\n <span class=\"typing-dot\"></span>\n </div>\n </div>\n\n </div>\n\n <!-- Media error (mic blocked / unsupported browser) -->\n <div class=\"agent-media-error\" *ngIf=\"mediaError\">\n <mat-icon class=\"media-error-icon\">error_outline</mat-icon>\n <span>{{ mediaError }}</span>\n </div>\n\n <!-- Staged attachment: a photo waits here until the user adds a caption and presses send -->\n <div class=\"agent-attachment\" *ngIf=\"pendingPhoto\">\n <mat-icon class=\"attach-icon\">image</mat-icon>\n <span class=\"attach-label\">Photo attached</span>\n <button mat-icon-button class=\"attach-remove\" matTooltip=\"Remove photo\" (click)=\"clearPhoto()\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <!-- Transcribing indicator for the Stop button -->\n <div class=\"agent-transcribing\" *ngIf=\"isTranscribing\">\n <mat-icon class=\"transcribing-icon\">graphic_eq</mat-icon>\n <span>Transcribing\u2026</span>\n </div>\n\n <!-- Voice mode: replaces the composer entirely. Everything here is sized to be used without looking \u2014\n a big state indicator, the last exchange, and a thumb-sized exit. -->\n <div class=\"agent-voice\" *ngIf=\"voiceMode\">\n\n <div class=\"voice-state\" [class.is-starting]=\"voiceState === 'starting'\"\n [class.is-listening]=\"voiceState === 'listening' || voiceState === 'capturing'\"\n [class.is-thinking]=\"voiceState === 'thinking'\" [class.is-speaking]=\"voiceState === 'speaking'\">\n <mat-icon class=\"voice-state-icon\">{{ voiceStateIcon }}</mat-icon>\n <span class=\"voice-state-label\">{{ voiceStateLabel }}</span>\n </div>\n\n <!-- Live level, so the mic being open is visible as well as audible. Not shown while calibrating: the\n meter would invite exactly the talking that ruins the measurement. -->\n <div class=\"voice-meter\" *ngIf=\"voiceState === 'listening' || voiceState === 'capturing'\">\n <div class=\"voice-meter-fill\" [style.width.%]=\"voiceLevel * 100\"></div>\n </div>\n\n <!-- Deliberately NO transcript/reply repeated here: both already appear in the chat thread directly above,\n and duplicating them pushed the state indicator \u2014 the one thing that must be readable at a glance \u2014\n down the panel. The state, the meter and the exit are all this needs. -->\n\n <!-- Says explicitly that pauses are fine, because the natural worry is that stopping to think will send -->\n <p class=\"voice-hint\" *ngIf=\"voiceState === 'starting'\">Checking the background noise \u2014 hold on\u2026</p>\n <p class=\"voice-hint\" *ngIf=\"voiceState === 'capturing'\">Take your time \u2014 it waits until you're done.</p>\n <p class=\"voice-hint\" *ngIf=\"voiceState === 'listening'\">Just speak \u2014 it sends when you finish.</p>\n\n <button mat-flat-button color=\"warn\" class=\"voice-exit\" (click)=\"stopVoiceMode()\">\n <mat-icon>close</mat-icon>\n <span>Exit voice mode</span>\n </button>\n </div>\n\n <!-- Input area -->\n <div class=\"agent-input\" [class.recording]=\"isRecording\" *ngIf=\"!voiceMode\">\n\n <!-- Normal input: attach, text, voice note, hands-free, send -->\n <ng-container *ngIf=\"!isRecording\">\n <button mat-icon-button class=\"media-btn\" matTooltip=\"Attach\" [matMenuTriggerFor]=\"attachMenu\">\n <mat-icon>add</mat-icon>\n </button>\n <mat-form-field appearance=\"outline\" class=\"input-field\">\n <input matInput #messageInput placeholder=\"Type, speak or snap...\" [(ngModel)]=\"inputText\" (keydown)=\"onKeydown($event)\" autocomplete=\"off\" />\n </mat-form-field>\n <!-- Tap records a voice note; HOLD enters hands-free voice mode. Deliberately no separate button for\n voice mode: the row is already at capacity, and a fifth control would crowd out the text field on a\n phone. The gesture is the accelerator, the \"+\" menu below is the discoverable path to the same\n thing \u2014 a gesture nobody can see is a feature nobody finds. -->\n <button mat-icon-button class=\"media-btn mic-btn\" [class.arming]=\"micHeld\"\n matTooltip=\"Tap to record \u00B7 hold for hands-free\"\n (click)=\"onMicClick()\"\n (pointerdown)=\"onMicPressStart()\" (pointerup)=\"onMicPressEnd()\"\n (pointerleave)=\"onMicPressEnd()\" (pointercancel)=\"onMicPressEnd()\">\n <mat-icon>mic</mat-icon>\n </button>\n <button mat-icon-button color=\"primary\" class=\"send-btn\" (click)=\"sendMessage()\" [disabled]=\"!canSend\">\n <mat-icon>send</mat-icon>\n </button>\n </ng-container>\n\n <!-- Recording bar: replaces the input while a voice note is being captured.\n Three exits \u2014 discard it, stop and read it back as text, or send it straight away. -->\n <ng-container *ngIf=\"isRecording\">\n <button mat-icon-button class=\"media-btn cancel-btn\" matTooltip=\"Discard\" (click)=\"cancelRecording()\">\n <mat-icon>delete</mat-icon>\n </button>\n <div class=\"recording-status\">\n <span class=\"rec-dot\"></span>\n <span class=\"rec-label\">Recording</span>\n <span class=\"rec-time\">{{ recordDisplay }}</span>\n </div>\n <button mat-icon-button class=\"media-btn stop-btn\" matTooltip=\"Stop and add as text\" (click)=\"stopAndTranscribe()\">\n <mat-icon>stop_circle</mat-icon>\n </button>\n <button mat-icon-button color=\"primary\" class=\"send-btn\" matTooltip=\"Send now\" (click)=\"stopRecording()\">\n <mat-icon>send</mat-icon>\n </button>\n </ng-container>\n\n <!-- Attach menu \u2014 keeps the row compact: one \"+\" instead of a button per source -->\n <mat-menu #attachMenu=\"matMenu\">\n <button mat-menu-item (click)=\"takePhoto()\">\n <mat-icon>photo_camera</mat-icon>\n <span>Take photo</span>\n </button>\n <button mat-menu-item (click)=\"choosePhoto()\">\n <mat-icon>photo_library</mat-icon>\n <span>Choose photo</span>\n </button>\n <!-- The discoverable way into voice mode. The mic long-press does the same thing faster, but only\n once you know it exists \u2014 this is how you find out. -->\n <button mat-menu-item (click)=\"startVoiceMode()\">\n <mat-icon>record_voice_over</mat-icon>\n <span>Hands-free voice mode</span>\n </button>\n </mat-menu>\n\n <!-- Two separate pickers on purpose. `capture=\"environment\"` hands off to the phone's camera app and\n opens the rear lens; WITHOUT it the same input opens the gallery/file picker. One input cannot do\n both, which is why the old single button always went to the camera on mobile. -->\n <input type=\"file\" #cameraInput accept=\"image/*\" capture=\"environment\" hidden (change)=\"onPhotoSelected($event)\" />\n <input type=\"file\" #galleryInput accept=\"image/*\" hidden (change)=\"onPhotoSelected($event)\" />\n </div>\n\n</div>\n", styles: [".agent-fab{position:fixed;bottom:24px;right:24px;z-index:1001;background:#1976d2;color:#fff}.agent-window{position:fixed;bottom:96px;right:24px;width:400px;height:580px;background:#fff;border-radius:16px;box-shadow:0 8px 32px #00000026;display:flex;flex-direction:column;z-index:1000;overflow:hidden}.agent-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#1976d2;color:#fff;min-height:56px}.header-info{display:flex;align-items:center;gap:10px}.header-icon{font-size:24px;width:24px;height:24px}.header-text{display:flex;flex-direction:column;gap:1px}.header-title{font-size:16px;font-weight:500;line-height:1.2}.status-badge{display:flex;align-items:center;gap:4px;font-size:11px;font-weight:400;opacity:.9;line-height:1}.status-dot{width:7px;height:7px;border-radius:50%;display:inline-block}.status-badge.online .status-dot{background:#4caf50;box-shadow:0 0 4px #4caf5099}.status-badge.offline .status-dot{background:#9e9e9e}.header-actions{display:flex;gap:0}.header-actions button{color:#fff}.agent-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:8px}.agent-greeting{display:flex;flex-direction:column;align-items:center;text-align:center;padding:20px 8px 8px;gap:10px}.greeting-icon{font-size:48px;width:48px;height:48px;color:#1976d2;opacity:.8}.greeting-text{font-size:15px;color:#424242;line-height:1.5;margin:0}.capability-pillars{display:flex;flex-direction:column;gap:10px;margin-top:8px;width:100%}.pillar{border:1px solid #e0e0e0;border-radius:12px;padding:12px 14px;text-align:left;cursor:pointer;transition:all .2s ease;background:#fafafa}.pillar:hover{border-color:#1976d2;background:#1976d20a;box-shadow:0 2px 8px #1976d21a}.pillar-header{display:flex;align-items:center;gap:8px;margin-bottom:4px}.pillar-icon{font-size:20px;width:20px;height:20px;color:#1976d2}.pillar-title{font-size:13px;font-weight:600;color:#212121}.pillar-desc{font-size:12px;color:#616161;margin:0 0 6px;line-height:1.4}.pillar-example{font-size:12px;color:#1976d2;font-style:italic;opacity:.85}.message-row{display:flex;max-width:85%}.user-row{align-self:flex-end}.agent-row{align-self:flex-start}.message-bubble{padding:10px 14px;border-radius:16px;font-size:14px;line-height:1.5;word-break:break-word;white-space:pre-wrap}.user-bubble{background:#1976d2;color:#fff;border-bottom-right-radius:4px}.agent-bubble{background:#f0f0f0;color:#212121;border-bottom-left-radius:4px}.typing-bubble{display:flex;align-items:center;gap:4px;padding:12px 18px}.typing-dot{width:8px;height:8px;border-radius:50%;background:#9e9e9e;animation:typingBounce 1.4s infinite ease-in-out}.typing-dot:nth-child(2){animation-delay:.2s}.typing-dot:nth-child(3){animation-delay:.4s}@keyframes typingBounce{0%,60%,to{transform:translateY(0);opacity:.4}30%{transform:translateY(-6px);opacity:1}}.agent-input{display:flex;align-items:center;padding:8px 12px;border-top:1px solid #e0e0e0;gap:4px}.input-field{flex:1}.input-field ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.input-field ::ng-deep .mat-mdc-text-field-wrapper{padding:0 12px}.send-btn,.media-btn{margin-bottom:0;display:inline-flex;align-items:center;justify-content:center}.media-btn{color:#757575}.media-btn:hover{color:#424242}.cancel-btn:hover{color:#d32f2f}.agent-input.recording{background:#fff5f5}.recording-status{flex:1;display:flex;align-items:center;gap:8px;padding:0 4px;font-size:13px;color:#616161}.rec-label{flex:1}.rec-time{font-variant-numeric:tabular-nums;color:#424242}.rec-dot{width:10px;height:10px;border-radius:50%;background:#e53935;animation:rec-pulse 1.2s ease-in-out infinite}@keyframes rec-pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.85)}}.pending-bubble{opacity:.6;font-style:italic}.agent-attachment{display:flex;align-items:center;gap:8px;padding:6px 12px;font-size:12px;color:#1565c0;background:#e8f1fb;border-top:1px solid #cfe0f3}.attach-icon{font-size:18px;width:18px;height:18px}.attach-label{flex:1}.attach-remove{width:24px;height:24px;min-width:0;padding:0;flex:0 0 auto;line-height:24px;display:inline-flex;align-items:center;justify-content:center;color:#1565c0}.attach-remove .mat-icon{font-size:16px;width:16px;height:16px;line-height:16px}.agent-transcribing{display:flex;align-items:center;gap:8px;padding:6px 12px;font-size:12px;color:#616161;background:#f5f5f5;border-top:1px solid #e0e0e0}.transcribing-icon{font-size:18px;width:18px;height:18px;animation:rec-pulse 1.2s ease-in-out infinite}.stop-btn{color:#e53935}.agent-media-error{display:flex;align-items:center;gap:8px;padding:8px 12px;font-size:12px;color:#c62828;background:#fdecea;border-top:1px solid #f5c6c2}.media-error-icon{font-size:18px;width:18px;height:18px}.mic-btn{transition:transform .12s ease,color .12s ease}.mic-btn.arming{transform:scale(1.25);color:#1565c0}.agent-voice{display:flex;flex-direction:column;align-items:center;gap:12px;padding:20px 16px 18px;border-top:1px solid rgba(0,0,0,.08);background:#fafafa}.voice-state{display:flex;align-items:center;gap:10px;font-size:20px;font-weight:600;letter-spacing:.3px}.voice-state-icon{font-size:28px;width:28px;height:28px}.voice-state.is-starting{color:#00000073}.voice-state.is-listening{color:#2e7d32}.voice-state.is-thinking{color:#ef6c00}.voice-state.is-speaking{color:#1565c0}.voice-state.is-listening .voice-state-icon{animation:voicePulse 1.4s ease-in-out infinite}@keyframes voicePulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.55;transform:scale(1.12)}}.voice-meter{width:100%;max-width:260px;height:6px;border-radius:3px;background:#00000014;overflow:hidden}.voice-meter-fill{height:100%;background:#2e7d32;border-radius:3px;transition:width 80ms linear}.voice-hint{margin:0;font-size:13px;opacity:.6}.voice-exit{width:100%;max-width:280px;height:48px;font-size:15px}.voice-exit mat-icon{margin-right:6px}@media (max-width: 600px){.agent-window{inset:0 0 auto;width:100%;height:100%;height:100dvh;height:var(--agent-vh, 100dvh);border-radius:0}.agent-fab{bottom:72px;right:16px}}.agent-window.page-mode{position:relative;inset:auto;width:100%;max-width:100%;height:calc(var(--agent-vh, 100dvh) - var(--agent-page-offset, 140px));border-radius:12px;border:1px solid #e0e0e0;box-shadow:none;z-index:auto}@media (max-width: 600px){.agent-window.page-mode{height:calc(var(--agent-vh, 100dvh) - var(--agent-page-offset, 72px));border-radius:0;border:none}}.agent-conversations{max-height:180px;overflow-y:auto;border-bottom:1px solid #e0e0e0;background:#fafafa}.conversation-row{display:flex;align-items:center;gap:8px;padding:8px 14px;cursor:pointer;font-size:13px}.conversation-row:hover{background:#eef4fb}.conversation-row.current{background:#e3f0fd;font-weight:500}.conversation-icon{font-size:18px;width:18px;height:18px;opacity:.6}.conversation-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.conversation-empty{padding:10px 14px;font-size:12px;opacity:.6}.agent-session-banner{display:flex;align-items:center;flex-wrap:wrap;gap:6px;padding:6px 14px;background:#fff8e1;border-bottom:1px solid #ffe082;font-size:12px}.session-icon{font-size:16px;width:16px;height:16px;opacity:.7}.session-text{font-weight:500}.session-missing{opacity:.75}.conversation-divider{display:flex;align-items:center;gap:8px;margin:14px 4px;opacity:.55}.conversation-divider:before,.conversation-divider:after{content:\"\";flex:1;height:1px;background:#c8c8c8}.divider-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em}.session-chip{display:inline-block;padding:1px 8px;border-radius:10px;background:#ffe082;font-size:11px;white-space:nowrap}.agent-bubble .md-heading{display:block;margin:4px 0 2px;font-size:13px}.agent-bubble .md-bullet{opacity:.6;margin-right:2px}.agent-bubble code{padding:1px 4px;border-radius:3px;background:#0000000f;font-size:12px}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$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: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i4$4.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i4$4.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i4$4.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatFabButton, selector: "button[mat-fab], a[mat-fab], button[matFab], a[matFab]", inputs: ["extended"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4$2.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }, { kind: "pipe", type: AgentMarkdownPipe, name: "agentMarkdown" }], animations: [
19909
19992
  trigger('slideUp', [
19910
19993
  // Changed: page mode enters in normal flow and must not slide up — a page that animates in like a
19911
19994
  // dialog still reads as a dialog. Both empty transitions are declared BEFORE :enter/:leave because the
@@ -20032,7 +20115,7 @@ class OfflineIndicatorComponent {
20032
20115
  [ngStyle]="{ color: s.online ? '#4CAF50' : '#9E9E9E' }">{{ s.online ? 'cloud_done' : 'cloud_off' }}</mat-icon>
20033
20116
  </button>
20034
20117
  </ng-container>
20035
- `, isInline: true, styles: [":host{display:inline-flex;align-items:center;vertical-align:middle}:host .mat-mdc-icon-button{display:inline-flex;align-items:center;justify-content:center}\n"], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i4$4.MatBadge, selector: "[matBadge]", inputs: ["matBadgeColor", "matBadgeOverlap", "matBadgeDisabled", "matBadgePosition", "matBadge", "matBadgeDescription", "matBadgeSize", "matBadgeHidden"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }] }); }
20118
+ `, isInline: true, styles: [":host{display:inline-flex;align-items:center;vertical-align:middle}:host .mat-mdc-icon-button{display:inline-flex;align-items:center;justify-content:center}\n"], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i4$5.MatBadge, selector: "[matBadge]", inputs: ["matBadgeColor", "matBadgeOverlap", "matBadgeDisabled", "matBadgePosition", "matBadge", "matBadgeDescription", "matBadgeSize", "matBadgeHidden"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }] }); }
20036
20119
  }
20037
20120
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: OfflineIndicatorComponent, decorators: [{
20038
20121
  type: Component,
@@ -20329,9 +20412,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
20329
20412
  }] } });
20330
20413
 
20331
20414
  class StepsComponent {
20332
- constructor(breakpointObserver, dataService) {
20415
+ constructor(breakpointObserver, dataService, apiErrorService) {
20333
20416
  this.breakpointObserver = breakpointObserver;
20334
20417
  this.dataService = dataService;
20418
+ this.apiErrorService = apiErrorService;
20335
20419
  this.value = "";
20336
20420
  this.activeIndex = null; // Added: host-driven current step (wizard hosts advance it with their own Next/Back buttons) — steps stay a read-only indicator, never clickable
20337
20421
  this.selectedIndex = 0;
@@ -20340,7 +20424,7 @@ class StepsComponent {
20340
20424
  // Changed: materialized — getVisibleSteps() feeds the *ngFor of mat-step and was allocating a new array
20341
20425
  // on every change-detection cycle. It now returns a stable reference rebuilt only on real changes.
20342
20426
  this.visibleSteps = [];
20343
- }
20427
+ } // Changed: injected ApiErrorService so a failed step load is no longer silent
20344
20428
  ngOnInit() {
20345
20429
  this.loadSteps();
20346
20430
  this.observeScreenSize();
@@ -20371,6 +20455,9 @@ class StepsComponent {
20371
20455
  this.buildVisibleSteps(); // Added: steps arrived from the API — rebuild the materialized list
20372
20456
  this.updateSelectedStep();
20373
20457
  }
20458
+ else {
20459
+ this.apiErrorService.presentAppFailure(response, 'load', this.config.loadAction.url); // Added: was a silent no-op — a steps strip that failed to load simply never appeared; 'load' dedupes with the page's other loads
20460
+ }
20374
20461
  });
20375
20462
  }
20376
20463
  else {
@@ -20421,17 +20508,17 @@ class StepsComponent {
20421
20508
  }
20422
20509
  // New method to filter hidden steps
20423
20510
  getVisibleSteps() { return this.visibleSteps; } // Changed: returns the materialized field (internal callers and the template both use this)
20424
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: StepsComponent, deps: [{ token: i1$4.BreakpointObserver }, { token: DataServiceLib }], target: i0.ɵɵFactoryTarget.Component }); }
20511
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: StepsComponent, deps: [{ token: i1$4.BreakpointObserver }, { token: DataServiceLib }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
20425
20512
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: StepsComponent, isStandalone: false, selector: "spa-steps", inputs: { value: "value", config: "config", data: "data", activeIndex: "activeIndex" }, providers: [{
20426
20513
  provide: STEPPER_GLOBAL_OPTIONS, useValue: { displayDefaultIndicatorType: false }
20427
- }], viewQueries: [{ propertyName: "stepper", first: true, predicate: ["stepper"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "\n<mat-horizontal-stepper class=\"transparent\" [labelPosition]=\"shouldShowLabels ? 'bottom' : 'end'\" #stepper [selectedIndex]=\"selectedIndex\">\n\n <mat-step *ngFor=\"let step of getVisibleSteps()\"\n [editable]=\"false\" [label]=\"shouldShowLabels ? step.name : ''\" [state]=\"step.icon ?? 'number'\">\n </mat-step>\n\n</mat-horizontal-stepper>\n", styles: [".transparent{background-color:#0000}:host ::ng-deep .mat-step-header{padding:5px!important}:host ::ng-deep .mat-stepper-horizontal-line{min-width:5px!important}:host ::ng-deep .mat-horizontal-content-container{padding:0!important}:host ::ng-deep .mat-horizontal-stepper-header{pointer-events:none!important}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: i4$5.MatStep, selector: "mat-step", inputs: ["color"], exportAs: ["matStep"] }, { kind: "component", type: i4$5.MatStepper, selector: "mat-stepper, mat-vertical-stepper, mat-horizontal-stepper, [matStepper]", inputs: ["disableRipple", "color", "labelPosition", "headerPosition", "animationDuration"], outputs: ["animationDone"], exportAs: ["matStepper", "matVerticalStepper", "matHorizontalStepper"] }] }); }
20514
+ }], viewQueries: [{ propertyName: "stepper", first: true, predicate: ["stepper"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "\n<mat-horizontal-stepper class=\"transparent\" [labelPosition]=\"shouldShowLabels ? 'bottom' : 'end'\" #stepper [selectedIndex]=\"selectedIndex\">\n\n <mat-step *ngFor=\"let step of getVisibleSteps()\"\n [editable]=\"false\" [label]=\"shouldShowLabels ? step.name : ''\" [state]=\"step.icon ?? 'number'\">\n </mat-step>\n\n</mat-horizontal-stepper>\n", styles: [".transparent{background-color:#0000}:host ::ng-deep .mat-step-header{padding:5px!important}:host ::ng-deep .mat-stepper-horizontal-line{min-width:5px!important}:host ::ng-deep .mat-horizontal-content-container{padding:0!important}:host ::ng-deep .mat-horizontal-stepper-header{pointer-events:none!important}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: i5$3.MatStep, selector: "mat-step", inputs: ["color"], exportAs: ["matStep"] }, { kind: "component", type: i5$3.MatStepper, selector: "mat-stepper, mat-vertical-stepper, mat-horizontal-stepper, [matStepper]", inputs: ["disableRipple", "color", "labelPosition", "headerPosition", "animationDuration"], outputs: ["animationDone"], exportAs: ["matStepper", "matVerticalStepper", "matHorizontalStepper"] }] }); }
20428
20515
  }
20429
20516
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: StepsComponent, decorators: [{
20430
20517
  type: Component,
20431
20518
  args: [{ selector: 'spa-steps', providers: [{
20432
20519
  provide: STEPPER_GLOBAL_OPTIONS, useValue: { displayDefaultIndicatorType: false }
20433
20520
  }], standalone: false, template: "\n<mat-horizontal-stepper class=\"transparent\" [labelPosition]=\"shouldShowLabels ? 'bottom' : 'end'\" #stepper [selectedIndex]=\"selectedIndex\">\n\n <mat-step *ngFor=\"let step of getVisibleSteps()\"\n [editable]=\"false\" [label]=\"shouldShowLabels ? step.name : ''\" [state]=\"step.icon ?? 'number'\">\n </mat-step>\n\n</mat-horizontal-stepper>\n", styles: [".transparent{background-color:#0000}:host ::ng-deep .mat-step-header{padding:5px!important}:host ::ng-deep .mat-stepper-horizontal-line{min-width:5px!important}:host ::ng-deep .mat-horizontal-content-container{padding:0!important}:host ::ng-deep .mat-horizontal-stepper-header{pointer-events:none!important}\n"] }]
20434
- }], ctorParameters: () => [{ type: i1$4.BreakpointObserver }, { type: DataServiceLib }], propDecorators: { stepper: [{
20521
+ }], ctorParameters: () => [{ type: i1$4.BreakpointObserver }, { type: DataServiceLib }, { type: ApiErrorService }], propDecorators: { stepper: [{
20435
20522
  type: ViewChild,
20436
20523
  args: ['stepper']
20437
20524
  }], value: [{
@@ -20638,7 +20725,7 @@ class BottomTabsComponent {
20638
20725
  return tab.name;
20639
20726
  }
20640
20727
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BottomTabsComponent, deps: [{ token: i1$1.Router }, { token: DataServiceLib }, { token: NotificationsService }, { token: SetupService }], target: i0.ɵɵFactoryTarget.Component }); }
20641
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: BottomTabsComponent, isStandalone: false, selector: "spa-bottom-tabs", ngImport: i0, template: "<nav class=\"bt-bar\" role=\"navigation\" aria-label=\"Primary\">\n\n <ng-container *ngFor=\"let tab of tabs; trackBy: trackByName\">\n\n <button type=\"button\" class=\"bt-tab\" *ngIf=\"isTabVisible(tab)\" [class.active]=\"activeTab == tab.name\" [attr.aria-current]=\"activeTab == tab.name ? 'page' : null\" [attr.aria-label]=\"tab.display\" (click)=\"navigate(tab)\">\n\n <span class=\"bt-pill\">\n <mat-icon\n [matBadge]=\"tab.badge == 'notifications' ? (notificationCount$ | async) : null\"\n [matBadgeHidden]=\"tab.badge != 'notifications' || (notificationCount$ | async) === 0\"\n matBadgeColor=\"warn\"\n matBadgeSize=\"small\">{{tab.icon}}</mat-icon>\n </span>\n\n <span class=\"bt-label\">{{tab.display}}</span>\n\n </button>\n\n </ng-container>\n\n</nav>\n", styles: [":host{display:block}@media (min-width: 601px){:host{display:none}}.bt-bar{position:fixed;bottom:0;left:0;right:0;display:flex;align-items:stretch;height:64px;height:calc(64px + env(safe-area-inset-bottom,0px));padding-bottom:env(safe-area-inset-bottom,0px);box-sizing:border-box;background:#fffffff0;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border-top:1px solid rgba(0,0,0,.08);box-shadow:0 -2px 16px #0000000f;z-index:900}.bt-tab{flex:1 1 0;min-width:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;border:0;padding:0;background:transparent;color:#5f6368;font-family:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent;transition:color .2s cubic-bezier(.4,0,.2,1)}.bt-tab.active{color:#1976d2}.bt-pill{position:relative;display:flex;align-items:center;justify-content:center;width:56px;height:32px;border-radius:16px}.bt-pill:before{content:\"\";position:absolute;inset:0;border-radius:16px;background:#1976d21f;opacity:0;transform:scale(.6);transition:transform .2s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1)}.bt-tab.active .bt-pill:before{opacity:1;transform:scale(1)}.bt-pill mat-icon{position:relative;font-size:24px;width:24px;height:24px;line-height:24px}.bt-label{font-size:11px;font-weight:500;line-height:1;letter-spacing:.2px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bt-tab:active .bt-pill:before{opacity:1;transform:scale(1);background:#1976d22e}.bt-tab:focus-visible .bt-pill{outline:2px solid #1976d2;outline-offset:2px}@media (prefers-reduced-motion: reduce){.bt-tab,.bt-pill:before{transition:none}}:host-context(html.keyboard-open) .bt-bar{display:none}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i4$4.MatBadge, selector: "[matBadge]", inputs: ["matBadgeColor", "matBadgeOverlap", "matBadgeDisabled", "matBadgePosition", "matBadge", "matBadgeDescription", "matBadgeSize", "matBadgeHidden"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }] }); }
20728
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: BottomTabsComponent, isStandalone: false, selector: "spa-bottom-tabs", ngImport: i0, template: "<nav class=\"bt-bar\" role=\"navigation\" aria-label=\"Primary\">\n\n <ng-container *ngFor=\"let tab of tabs; trackBy: trackByName\">\n\n <button type=\"button\" class=\"bt-tab\" *ngIf=\"isTabVisible(tab)\" [class.active]=\"activeTab == tab.name\" [attr.aria-current]=\"activeTab == tab.name ? 'page' : null\" [attr.aria-label]=\"tab.display\" (click)=\"navigate(tab)\">\n\n <span class=\"bt-pill\">\n <mat-icon\n [matBadge]=\"tab.badge == 'notifications' ? (notificationCount$ | async) : null\"\n [matBadgeHidden]=\"tab.badge != 'notifications' || (notificationCount$ | async) === 0\"\n matBadgeColor=\"warn\"\n matBadgeSize=\"small\">{{tab.icon}}</mat-icon>\n </span>\n\n <span class=\"bt-label\">{{tab.display}}</span>\n\n </button>\n\n </ng-container>\n\n</nav>\n", styles: [":host{display:block}@media (min-width: 601px){:host{display:none}}.bt-bar{position:fixed;bottom:0;left:0;right:0;display:flex;align-items:stretch;height:64px;height:calc(64px + env(safe-area-inset-bottom,0px));padding-bottom:env(safe-area-inset-bottom,0px);box-sizing:border-box;background:#fffffff0;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border-top:1px solid rgba(0,0,0,.08);box-shadow:0 -2px 16px #0000000f;z-index:900}.bt-tab{flex:1 1 0;min-width:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;border:0;padding:0;background:transparent;color:#5f6368;font-family:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent;transition:color .2s cubic-bezier(.4,0,.2,1)}.bt-tab.active{color:#1976d2}.bt-pill{position:relative;display:flex;align-items:center;justify-content:center;width:56px;height:32px;border-radius:16px}.bt-pill:before{content:\"\";position:absolute;inset:0;border-radius:16px;background:#1976d21f;opacity:0;transform:scale(.6);transition:transform .2s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1)}.bt-tab.active .bt-pill:before{opacity:1;transform:scale(1)}.bt-pill mat-icon{position:relative;font-size:24px;width:24px;height:24px;line-height:24px}.bt-label{font-size:11px;font-weight:500;line-height:1;letter-spacing:.2px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bt-tab:active .bt-pill:before{opacity:1;transform:scale(1);background:#1976d22e}.bt-tab:focus-visible .bt-pill{outline:2px solid #1976d2;outline-offset:2px}@media (prefers-reduced-motion: reduce){.bt-tab,.bt-pill:before{transition:none}}:host-context(html.keyboard-open) .bt-bar{display:none}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i4$5.MatBadge, selector: "[matBadge]", inputs: ["matBadgeColor", "matBadgeOverlap", "matBadgeDisabled", "matBadgePosition", "matBadge", "matBadgeDescription", "matBadgeSize", "matBadgeHidden"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }] }); }
20642
20729
  }
20643
20730
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BottomTabsComponent, decorators: [{
20644
20731
  type: Component,
@@ -20849,7 +20936,7 @@ class NavMenuComponent {
20849
20936
  return !this.isMiniSidebar || this.isMiniHovered;
20850
20937
  }
20851
20938
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: NavMenuComponent, deps: [{ token: i1$1.Router }, { token: AuthService }, { token: StorageService }, { token: NotificationsService }, { token: i1$4.BreakpointObserver }, { token: DataServiceLib }, { token: i4.MatDialog }, { token: SubscriptionService }, { token: SetupService }, { token: LastRouteService }, { token: OfflineService }], target: i0.ɵɵFactoryTarget.Component }); }
20852
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: NavMenuComponent, isStandalone: false, selector: "spa-nav-menu", inputs: { appConfig: "appConfig", footer: "footer" }, host: { listeners: { "window:scroll": "onWindowScroll()" } }, ngImport: i0, template: "<header *ngIf=\"loggedin && dataService.appConfig.navigation == 'top'\">\n\n <!-- Changed: Removed mb-3 class to eliminate gap between toolbar and content -->\n <nav class=\"toolbar navbar navbar-expand-sm navbar-toggleable-sm navbar-light border-bottom box-shadow\" style=\"padding-right: 10px;\">\n\n\n <div class=\"container-fluid\" style=\"padding-right: 0px;\">\n\n <img *ngIf=\"appConfig.logo!=''\" [src]=\"appConfig.logo\" style=\"height: 50px; margin-right: 2em\" />\n\n <div>\n <!-- <div style=\"font-size: 20px;\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px;\">\n {{tenantName}}\n </div> -->\n\n <div *ngIf=\"!dataService.appConfig.multitenant\" style=\"font-size: 22px;\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant\" style=\"font-size: 20px; ; font-weight: 400;\" [ngStyle]=\"{'margin-top': dataService.appConfig.multitenant ? '12px' : ''}\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px; margin-bottom: 5px;\">\n {{tenantName}}\n </div>\n\n </div>\n\n\n\n <button class=\"navbar-toggler\" type=\"button\" data-toggle=\"collapse\" data-target=\".navbar-collapse\" aria-label=\"Toggle navigation\" [attr.aria-expanded]=\"isExpanded\" (click)=\"toggle()\">\n <span class=\"navbar-toggler-icon\"></span>\n </button>\n\n <div *ngIf=\"myRole\" class=\" navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse stack-top\" style=\"margin-right: 0px;\" [ngClass]=\"{ show: isExpanded, navitems: isExpanded }\" >\n\n <button mat-icon-button (click)=\"logoff()\" > <mat-icon>logout</mat-icon> </button>\n\n <div *ngIf=\"dataService.appConfig.multitenant\">\n\n <button mat-icon-button (click)=\"redirectTo('home/tenancy/settings')\" > <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon> </button>\n\n <!-- Removed: Support icon \u2014 replaced by floating assistant chat widget -->\n </div>\n\n\n <button id=\"btnUser\" mat-button [matMenuTriggerFor]=\"profileMenu\" ><mat-icon style=\"font-size: 24px;\">account_circle</mat-icon> &nbsp;{{loggedUserFullName}}</button>\n\n <mat-menu #profileMenu=\"matMenu\">\n <button id=\"btnProfile\" mat-menu-item (click)=\"redirectTo('home/user/profile')\" >Profile</button>\n <button id=\"btnLogOff\" mat-menu-item (click)=\"logoff()\">Log Off</button>\n </mat-menu>\n\n <div *ngFor=\"let item of reversedCapItems\">\n\n <!-- Menu Item \u2014 Added: isFeatureAllowed check for plan-based gating -->\n <button id=\"btnMenu\" *ngIf=\"myRole[item.name] && !item.capSubItems && item.showMenu && isFeatureAllowed(item)\" mat-button (click)=\"redirectTo(item.link)\">{{item.display}}</button>\n\n <!-- Menu Item with Sub items ignored \u2014 Added: isFeatureAllowed check -->\n <button id=\"btnMenu\" *ngIf=\"myRole[item.name] && item.capSubItems && item.showMenu && item.ignoreSubsDisplay && isFeatureAllowed(item)\" mat-button (click)=\"redirectTo(item.link)\">{{item.display}}</button>\n\n <!-- Menu Item with Sub items to display \u2014 Added: isFeatureAllowed check -->\n <button id=\"btnMenu\" *ngIf=\"myRole[item.name] && item.capSubItems && item.showMenu && !item.ignoreSubsDisplay && isFeatureAllowed(item)\" mat-button [matMenuTriggerFor]=\"adminMenu\">{{item.display}}</button>\n\n\n <!-- Sub Menu Items \u2014 Added: isFeatureAllowed check on sub-items -->\n <mat-menu #adminMenu=\"matMenu\">\n\n <div *ngFor=\"let subItem of item.capSubItems\">\n\n <button *ngIf=\"myRole[subItem.name] && subItem.showMenu && isFeatureAllowed(subItem)\" mat-menu-item (click)=\"redirectTo(subItem.link)\">{{subItem.display}}</button>\n\n </div>\n\n </mat-menu>\n\n </div>\n\n </div>\n\n\n </div>\n\n </nav>\n\n</header>\n\n<!-- Changed: Removed top/bottom padding to eliminate gaps, but kept left/right padding for content spacing -->\n<div class=\"container-fluid tin-bg-image\" [class.has-bottom-tabs]=\"loggedin && smallScreen\" *ngIf=\"dataService.appConfig.navigation == 'top'\" style=\"padding: 12px 12px; margin: 0;\"> <!-- Changed: bottom padding so the fixed tab bar cannot cover the last row -->\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n\n\n\n<!-- ============================================================ -->\n<!-- TOP-MODERN (navigation == 'top-modern') -->\n<!-- Changed: Modernised horizontal top navigation -->\n<!-- ============================================================ -->\n<header *ngIf=\"loggedin && dataService.appConfig.navigation == 'top-modern'\">\n\n <nav class=\"tm-navbar\" [class.tm-scrolled]=\"topbarScrolled\">\n <div class=\"tm-bar\">\n\n <!-- Brand -->\n <div class=\"tm-brand\" (click)=\"modernNavigate('')\">\n <img *ngIf=\"appConfig.logo\" [src]=\"appConfig.logo\" class=\"tm-logo\" alt=\"logo\" />\n <div class=\"tm-brand-text\">\n <div class=\"tm-app-name\">{{appConfig.appName}}</div>\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" class=\"tm-tenant\">{{tenantName}}</div>\n </div>\n </div>\n\n <!-- Mobile toggle -->\n <button class=\"tm-toggler\" type=\"button\" aria-label=\"Toggle navigation\"\n [attr.aria-expanded]=\"isExpanded\" (click)=\"toggle()\">\n <mat-icon>{{isExpanded ? 'close' : 'menu'}}</mat-icon>\n </button>\n\n <!-- Menu items -->\n <div class=\"tm-menu\" [class.tm-menu-open]=\"isExpanded\">\n\n <ng-container *ngFor=\"let item of dataService.appConfig.capItems\">\n <div class=\"tm-item-wrap\"\n *ngIf=\"myRole[item.name] && item.showMenu && isFeatureAllowed(item)\">\n\n <!-- Simple item (no sub-items, or sub-items ignored for display) -->\n <button *ngIf=\"!item.capSubItems || item.ignoreSubsDisplay\"\n class=\"tm-item\"\n [class.tm-item-active]=\"isActiveRoute(item.link)\"\n (click)=\"modernNavigate(item.link)\">\n <mat-icon *ngIf=\"item.icon && item.icon != 'navigate_next'\" class=\"tm-item-icon\">{{item.icon}}</mat-icon>\n <span class=\"tm-item-text\">{{item.display}}</span>\n </button>\n\n <!-- Parent item with displayed sub-items -->\n <ng-container *ngIf=\"item.capSubItems && !item.ignoreSubsDisplay\">\n <button class=\"tm-item tm-item-parent\"\n [class.tm-item-active]=\"isParentActive(item)\"\n [matMenuTriggerFor]=\"tmSubMenu\">\n <mat-icon *ngIf=\"item.icon && item.icon != 'navigate_next'\" class=\"tm-item-icon\">{{item.icon}}</mat-icon>\n <span class=\"tm-item-text\">{{item.display}}</span>\n <!-- Changed: Caret signals this item has more items beneath it -->\n <mat-icon class=\"tm-item-caret\">expand_more</mat-icon>\n </button>\n\n <mat-menu #tmSubMenu=\"matMenu\" class=\"tm-submenu-panel\" [overlapTrigger]=\"false\" yPosition=\"below\">\n <ng-container *ngFor=\"let sub of getSubItems(item)\">\n <button *ngIf=\"myRole[sub.name] && sub.showMenu && isFeatureAllowed(sub)\"\n mat-menu-item\n [class.tm-sub-active]=\"isActiveRoute(sub.link)\"\n (click)=\"modernNavigate(sub.link)\">\n <mat-icon *ngIf=\"sub.icon && sub.icon != 'navigate_next'\">{{sub.icon}}</mat-icon>\n <span>{{sub.display}}</span>\n </button>\n </ng-container>\n </mat-menu>\n </ng-container>\n\n </div>\n </ng-container>\n\n </div>\n\n <!-- Right-side actions -->\n <div class=\"tm-actions\" [class.tm-actions-open]=\"isExpanded\">\n\n <ng-container *ngIf=\"dataService.appConfig.multitenant\">\n <button mat-icon-button class=\"tm-action-btn\" (click)=\"modernNavigate('home/tenancy/settings')\" matTooltip=\"Organisation Settings\">\n <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon>\n </button>\n <button *ngIf=\"setupService.enabled && ((setupCount$ | async) || 0) > 0 && !smallScreen\" mat-icon-button class=\"tm-action-btn\" (click)=\"modernNavigate('home/setup')\" matTooltip=\"Getting Started\"> <!-- Added: Setup readiness badge \u2014 hidden when complete or unconfigured -->\n <mat-icon [matBadge]=\"setupCount$ | async\" matBadgeColor=\"warn\" matBadgeSize=\"small\">rocket_launch</mat-icon>\n </button>\n <button *ngIf=\"!smallScreen\" mat-icon-button class=\"tm-action-btn\" (click)=\"modernNavigate('home/workflow/notifications')\" matTooltip=\"Notifications\">\n <mat-icon [matBadge]=\"notificationCount$ | async\" [matBadgeHidden]=\"(notificationCount$ | async) === 0\" matBadgeColor=\"warn\" matBadgeSize=\"small\">notifications</mat-icon>\n </button>\n <spa-offline-indicator *ngIf=\"!smallScreen\"></spa-offline-indicator> <!-- Changed: TinSync connection + pending-sync indicator -->\n </ng-container>\n\n <span class=\"tm-divider-v\"></span>\n\n <!-- Profile -->\n <button mat-button class=\"tm-user-btn\" [matMenuTriggerFor]=\"tmProfileMenu\">\n <mat-icon class=\"tm-user-icon\">account_circle</mat-icon>\n <span class=\"tm-user-name\">{{loggedUserFullName}}</span>\n </button>\n\n <mat-menu #tmProfileMenu=\"matMenu\" [overlapTrigger]=\"false\" yPosition=\"below\">\n <button mat-menu-item (click)=\"modernNavigate('home/user/profile')\">\n <mat-icon>person</mat-icon><span>Profile</span>\n </button>\n <mat-divider></mat-divider>\n <button mat-menu-item (click)=\"logoff()\">\n <mat-icon>logout</mat-icon><span>Log Off</span>\n </button>\n </mat-menu>\n\n <!-- Sign out \u2014 Changed: aligned via flex-centred action button -->\n <button mat-icon-button class=\"tm-action-btn tm-signout\" (click)=\"logoff()\" matTooltip=\"Sign Out\">\n <mat-icon>logout</mat-icon>\n </button>\n\n </div>\n\n </div>\n </nav>\n\n</header>\n\n<!-- Top-modern page content \u2014 Changed: use original 'top' background (tin-bg-image), no footer bar -->\n<div class=\"container-fluid tin-bg-image\" [class.has-bottom-tabs]=\"loggedin && smallScreen\" *ngIf=\"loggedin && dataService.appConfig.navigation == 'top-modern'\" style=\"padding: 12px 12px; margin: 0;\"> <!-- Changed: bottom padding so the fixed tab bar cannot cover the last row -->\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n<!-- Not logged in fallback for top-modern -->\n<div class=\"tin-bg-image\" *ngIf=\"!loggedin && dataService.appConfig.navigation == 'top-modern'\">\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n\n\n\n<!-- SIDE -->\n<mat-toolbar class=\"tin-bg-image-toolbar\" *ngIf=\"loggedin && dataService.appConfig.navigation == 'side'\" style=\"padding: 0px 8px;\">\n\n <button mat-icon-button (click)=\"toggle()\" matTooltip=\"Menu\">\n <mat-icon>menu</mat-icon>\n </button>\n\n <img [src]=\"dataService.appConfig.logo\" style=\"height: 50px;\" />\n\n <div style=\"padding-left: 10px; \">\n\n <div style=\"font-size: 22px; font-weight: 400;\">\n {{appConfig.appName}}\n </div>\n\n <!-- <div style=\"font-size: 20px; height: 25px; font-weight: 400;\" [ngStyle]=\"{'margin-top': dataService.appConfig.multitenant ? '12px' : ''}\">\n {{appConfig.appName}}\n </div> -->\n\n <!-- <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px; margin-bottom: 5px;\">\n {{tenantName}}\n </div> -->\n\n </div>\n\n\n\n <span class=\"toolbar-item-spacer\"></span>\n\n <!-- buttons -->\n\n <div *ngIf=\"dataService.appConfig.multitenant\" style=\"display: flex; align-items: center;\">\n\n <!-- <label style=\"font-size: 14px;\">Hi, {{loggedUserFullName}}</label> -->\n\n <button mat-icon-button (click)=\"redirectTo('home/tenancy/settings')\" matTooltip=\"Organisation Settings\">\n <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon>\n </button>\n <label style=\"font-size: 14px;margin-right: 20px;\">{{tenantName}}</label>\n\n <!-- Changed: Support/help icon removed \u2014 replaced by floating agent chat widget -->\n\n <button *ngIf=\"setupService.enabled && ((setupCount$ | async) || 0) > 0 && !smallScreen\" mat-icon-button (click)=\"redirectTo('home/setup')\" matTooltip=\"Getting Started\"> <!-- Added: Setup readiness badge \u2014 hidden when complete or unconfigured -->\n <mat-icon [matBadge]=\"setupCount$ | async\" matBadgeColor=\"warn\" matBadgeSize=\"small\">rocket_launch</mat-icon>\n </button>\n <button *ngIf=\"!smallScreen\" mat-icon-button (click)=\"redirectTo('home/workflow/notifications')\" matTooltip=\"Notifications\">\n <mat-icon [matBadge]=\"notificationCount$ | async\" [matBadgeHidden]=\"(notificationCount$ | async) === 0\" matBadgeColor=\"warn\" matBadgeSize=\"small\">notifications</mat-icon>\n </button>\n\n <spa-offline-indicator *ngIf=\"!smallScreen\"></spa-offline-indicator> <!-- Changed: TinSync connection + pending-sync indicator -->\n\n </div>\n\n\n\n <button mat-icon-button matTooltip=\"My Account\" [matMenuTriggerFor]=\"userAccountMenu\"><mat-icon>account_circle</mat-icon></button>\n <label style=\"font-size: 14px;\">{{loggedUserFullName}}</label>\n\n <button *ngIf=\"!smallScreen\" mat-icon-button (click)=\"logoff()\" matTooltip=\"Signout\">\n <mat-icon>logout</mat-icon>\n </button>\n\n\n <!-- my account menu -->\n <mat-menu #userAccountMenu [overlapTrigger]=\"false\" yPosition=\"below\">\n\n\n <button mat-menu-item routerLink=\"home/user/profile\">\n <mat-icon>person</mat-icon><span>Profile</span>\n </button>\n\n <!-- Removed: Help menu item \u2014 replaced by floating assistant chat widget -->\n\n <mat-divider></mat-divider>\n\n <button mat-menu-item (click)=\"logoff()\">\n <mat-icon>logout</mat-icon>Logout\n </button>\n\n </mat-menu>\n\n</mat-toolbar>\n\n\n\n\n<mat-sidenav-container class=\"app-container\" [hasBackdrop]=\"smallScreen\" *ngIf=\"loggedin && dataService.appConfig.navigation == 'side'\">\n\n <mat-sidenav #sidenav [mode]=\"smallScreen ? 'over' : 'side'\" [class.mat-elevation-z4]=\"true\" [opened]=\"isExpanded\" class=\"app-sidenav side-color\" style=\"height: 100%;\"\n [ngStyle]=\"{'width': dataService.appConfig.navWidth}\">\n <mat-nav-list >\n\n <ng-container *ngFor=\"let cap of dataService.appConfig.capItems\" >\n\n <!-- Menu item \u2014 Added: isFeatureAllowed check for plan-based gating -->\n <mat-list-item [routerLink]=\"cap.link\" *ngIf=\"myRole[cap.name] && cap.showMenu && (!cap.capSubItems || cap.capSubItems && cap.ignoreSubsDisplay) && isFeatureAllowed(cap)\" style=\"height: 40px;font-size: 15px;\"\n (click)=\"smallScreen ? toggle() : null\">\n <mat-icon [ngStyle]=\"{'color': cap.color}\" style=\"margin-right: 5px;\">{{cap.icon}}</mat-icon>{{cap.display}}\n </mat-list-item>\n\n <!-- Menu With Sub items \u2014 Added: isFeatureAllowed check -->\n <mat-expansion-panel class=\"side-color\" [class.mat-elevation-z0]=\"true\" *ngIf=\"myRole[cap.name] && cap.showMenu && cap.capSubItems && !cap.ignoreSubsDisplay && isFeatureAllowed(cap)\">\n\n <mat-expansion-panel-header style=\"height: 40px;padding-left: 15px;\">\n <mat-icon [ngStyle]=\"{'color': cap.color}\" style=\"margin-right: 5px;\">{{cap.icon != 'navigate_next' ? cap.icon : 'fiber_manual_record' }}</mat-icon>{{cap.display}}\n </mat-expansion-panel-header>\n\n <!-- Sub items - Changed: Use ng-container to avoid blank spaces for hidden items -->\n <mat-nav-list>\n <ng-container *ngFor=\"let capSub of getSubItems(cap)\">\n <mat-list-item [routerLink]=\"capSub.link\" style=\"height: 30px; font-size: 15px; padding-left: 4px; padding-right: 10px; margin-bottom: 5px;\" (click)=\"smallScreen ? toggle() : null\" *ngIf=\"myRole[capSub.name] && capSub.showMenu && isFeatureAllowed(capSub)\" [matTooltip]=\"capSub.display\" matTooltipPosition=\"right\">\n <mat-icon [ngStyle]=\"{'color': capSub.color}\" style=\"margin-right: 5px;\">{{capSub.icon}}</mat-icon>{{capSub.display}}\n </mat-list-item>\n </ng-container>\n </mat-nav-list>\n\n </mat-expansion-panel>\n\n </ng-container>\n\n </mat-nav-list>\n </mat-sidenav>\n\n\n\n <mat-sidenav-content class=\"tin-bg-image\" [class.has-bottom-tabs]=\"loggedin && smallScreen\" style=\"padding: 0px 12px;\" *ngIf=\"loggedin && dataService.appConfig.navigation == 'side'\"> <!-- Changed: bottom padding so the fixed tab bar cannot cover the last row -->\n <hr style=\"margin-top: 0px;\">\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n </mat-sidenav-content>\n\n</mat-sidenav-container>\n\n\n<!-- footer -->\n<!-- Changed: hidden on mobile \u2014 the bottom tab bar owns the bottom edge there, and a footer under a fixed\n bar is either invisible or a second competing strip of chrome -->\n<div class=\"tin-center\" *ngIf=\"loggedin && !smallScreen && dataService.appConfig.navigation == 'side'\">\n <label style=\"text-align: center; font-size: 12px;\">&copy; {{nowDate | date : 'yyyy'}} <a color=\"primary\" class=\"terms-link\" [href]=\"appConfig.siteUrl\" target=\"_blank\">{{footer}}</a> | <a color=\"primary\" class=\"terms-link\" style=\"cursor: pointer;\" (click)=\"openTerms()\">Terms</a> | <a color=\"primary\" class=\"terms-link\" style=\"cursor: pointer;\" (click)=\"openPrivacy()\">Privacy Policy</a></label>\n</div>\n\n\n<div class=\"tin-bg-image\" *ngIf=\"!loggedin && dataService.appConfig.navigation == 'side'\">\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n\n<!-- SIDE-MODERN -->\n\n<!-- Changed: Side-modern navigation layout -->\n<div class=\"sm-layout\"\n *ngIf=\"loggedin && dataService.appConfig.navigation == 'side-modern'\"\n [class.sm-mini]=\"isMiniSidebar && !isMiniHovered\"\n [class.sm-mini-hovered]=\"isMiniSidebar && isMiniHovered\"\n [class.sm-mobile-open]=\"smallScreen && isExpanded\">\n\n <!-- Sidebar -->\n <aside class=\"sm-sidebar\"\n (mouseenter)=\"onMiniMouseEnter()\"\n (mouseleave)=\"onMiniMouseLeave()\">\n\n <!-- Background layers -->\n <div class=\"sm-sidebar-bg\">\n <div class=\"sm-sidebar-bg-image\" *ngIf=\"appConfig.navImage\" [ngStyle]=\"{'background-image': 'url(' + appConfig.navImage + ')'}\"></div>\n <div class=\"sm-sidebar-bg-overlay\" [ngStyle]=\"{'background-color': appConfig.navColor}\"></div>\n </div>\n\n <!-- Sidebar content -->\n <div class=\"sm-sidebar-content\">\n\n <!-- Brand -->\n <div class=\"sm-brand\">\n <img *ngIf=\"appConfig.logo\" [src]=\"appConfig.logo\" alt=\"logo\" />\n <span class=\"sm-brand-name\">{{appConfig.appName}}</span>\n </div>\n\n <mat-divider></mat-divider>\n\n <!-- Profile -->\n <div class=\"sm-profile\">\n <mat-icon class=\"sm-profile-icon\">account_circle</mat-icon>\n <div class=\"sm-profile-info\">\n <div class=\"sm-profile-name\">{{loggedUserFullName}}</div>\n <div class=\"sm-profile-role\">{{tenantName || 'User'}}</div>\n </div>\n </div>\n\n <mat-divider></mat-divider>\n\n <!-- Scrollable menu -->\n <div class=\"sm-menu-scroll\">\n\n <ng-container *ngFor=\"let cap of dataService.appConfig.capItems\">\n\n <!-- Simple menu item (no sub-items or ignoring sub display) \u2014 Added: isFeatureAllowed check -->\n <div *ngIf=\"myRole[cap.name] && cap.showMenu && (!cap.capSubItems || cap.ignoreSubsDisplay) && isFeatureAllowed(cap)\"\n class=\"sm-menu-item\"\n [class.sm-active]=\"isActiveRoute(cap.link)\"\n (click)=\"modernNavigate(cap.link)\">\n <mat-icon class=\"sm-menu-icon\">{{cap.icon != 'navigate_next' ? cap.icon : 'dashboard'}}</mat-icon>\n <span class=\"sm-menu-text\">{{cap.display}}</span>\n </div>\n\n <!-- Parent menu item with sub-items \u2014 Added: isFeatureAllowed check -->\n <ng-container *ngIf=\"myRole[cap.name] && cap.showMenu && cap.capSubItems && !cap.ignoreSubsDisplay && isFeatureAllowed(cap)\">\n\n <!-- Parent item (toggles sub-menu) -->\n <div class=\"sm-menu-item\"\n [class.sm-active]=\"isParentActive(cap) && !isMenuOpen(cap.name)\"\n (click)=\"toggleModernMenu(cap.name)\">\n <mat-icon class=\"sm-menu-icon\">{{cap.icon != 'navigate_next' ? cap.icon : 'dashboard'}}</mat-icon>\n <span class=\"sm-menu-text\">{{cap.display}}</span>\n <mat-icon class=\"sm-caret\" [class.sm-caret-open]=\"isMenuOpen(cap.name)\">expand_more</mat-icon>\n </div>\n\n <!-- Sub-menu container (animated) -->\n <div class=\"sm-submenu\" [class.sm-submenu-open]=\"isMenuOpen(cap.name)\">\n <ng-container *ngFor=\"let sub of getSubItems(cap)\">\n <div *ngIf=\"myRole[sub.name] && sub.showMenu && isFeatureAllowed(sub)\"\n class=\"sm-submenu-item\"\n [class.sm-active]=\"isActiveRoute(sub.link)\"\n (click)=\"modernNavigate(sub.link)\">\n <mat-icon *ngIf=\"sub.icon && sub.icon != 'navigate_next'\" class=\"sm-sub-icon\">{{sub.icon}}</mat-icon>\n <span *ngIf=\"!sub.icon || sub.icon == 'navigate_next'\" class=\"sm-initials\">{{getInitials(sub.display)}}</span>\n <span class=\"sm-menu-text\">{{sub.display}}</span>\n </div>\n </ng-container>\n </div>\n\n </ng-container>\n\n </ng-container>\n\n </div>\n\n </div>\n </aside>\n\n <!-- Mobile backdrop -->\n <div class=\"sm-backdrop\" (click)=\"isExpanded = false\"></div>\n\n <!-- Main content -->\n <div class=\"sm-main\">\n\n <!-- Top bar - Changed: Added scroll class for frosted glass effect -->\n <div class=\"sm-topbar\" [class.sm-topbar-scrolled]=\"topbarScrolled\">\n <button mat-icon-button (click)=\"smallScreen ? toggle() : toggleMiniSidebar()\" matTooltip=\"Menu\">\n <mat-icon>menu</mat-icon>\n </button>\n\n <!-- Changed: Mobile branding - show logo + app name when sidebar is hidden on small screens -->\n <img *ngIf=\"smallScreen && appConfig.logo\" [src]=\"appConfig.logo\" alt=\"logo\" class=\"sm-topbar-logo\" />\n <span *ngIf=\"smallScreen\" class=\"sm-topbar-brand\">{{appConfig.appName}}</span>\n\n <span class=\"sm-topbar-spacer\"></span>\n\n <!-- Multitenant buttons -->\n <div *ngIf=\"dataService.appConfig.multitenant\" style=\"display: flex; align-items: center;\">\n <button mat-icon-button (click)=\"redirectTo('home/tenancy/settings')\" matTooltip=\"Organisation Settings\">\n <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon>\n </button>\n <span class=\"sm-topbar-label\">{{tenantName}}</span>\n\n <!-- Changed: Support/help icon removed \u2014 replaced by floating agent chat widget -->\n\n <button *ngIf=\"setupService.enabled && ((setupCount$ | async) || 0) > 0 && !smallScreen\" mat-icon-button (click)=\"redirectTo('home/setup')\" matTooltip=\"Getting Started\"> <!-- Added: Setup readiness badge \u2014 hidden when complete or unconfigured -->\n <mat-icon [matBadge]=\"setupCount$ | async\" matBadgeColor=\"warn\" matBadgeSize=\"small\">rocket_launch</mat-icon>\n </button>\n <button *ngIf=\"!smallScreen\" mat-icon-button (click)=\"redirectTo('home/workflow/notifications')\" matTooltip=\"Notifications\">\n <mat-icon [matBadge]=\"notificationCount$ | async\" [matBadgeHidden]=\"(notificationCount$ | async) === 0\" matBadgeColor=\"warn\" matBadgeSize=\"small\">notifications</mat-icon>\n </button>\n <spa-offline-indicator *ngIf=\"!smallScreen\"></spa-offline-indicator> <!-- Changed: TinSync connection + pending-sync indicator -->\n </div>\n\n <!-- Profile menu -->\n <button mat-icon-button matTooltip=\"My Account\" [matMenuTriggerFor]=\"smProfileMenu\">\n <mat-icon>account_circle</mat-icon>\n </button>\n <span class=\"sm-topbar-label\">{{loggedUserFullName}}</span>\n\n <mat-menu #smProfileMenu=\"matMenu\" [overlapTrigger]=\"false\" yPosition=\"below\">\n <button mat-menu-item routerLink=\"home/user/profile\">\n <mat-icon>person</mat-icon><span>Profile</span>\n </button>\n <!-- Changed: Help menu item removed \u2014 replaced by floating agent chat widget -->\n <mat-divider></mat-divider>\n <button mat-menu-item (click)=\"logoff()\">\n <mat-icon>logout</mat-icon>Logout\n </button>\n </mat-menu>\n\n <button *ngIf=\"!smallScreen\" mat-icon-button (click)=\"logoff()\" matTooltip=\"Signout\">\n <mat-icon>logout</mat-icon>\n </button>\n </div>\n\n <!-- Page content - Changed: Replaced tin-bg-image with sm-content modern texture -->\n <div class=\"sm-content\" [class.has-bottom-tabs]=\"loggedin && smallScreen\"> <!-- Changed: bottom padding so the fixed tab bar cannot cover the last row -->\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n </div>\n\n <!-- Footer -->\n <div class=\"sm-footer\">\n &copy; {{nowDate | date : 'yyyy'}} <a [href]=\"appConfig.siteUrl\" target=\"_blank\">{{footer}}</a> | <a (click)=\"openTerms()\">Terms</a> | <a (click)=\"openPrivacy()\">Privacy Policy</a>\n </div>\n\n </div>\n\n</div>\n\n<!-- Not logged in fallback for side-modern -->\n<div class=\"tin-bg-image\" *ngIf=\"!loggedin && dataService.appConfig.navigation == 'side-modern'\">\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n<!-- Changed: Cascading toast notifications for real-time entity changes \u2014 visible in all layouts -->\n<spa-toast *ngIf=\"loggedin && dataService.appConfig.multitenant\"></spa-toast>\n\n<!-- Changed: Floating agent chat widget \u2014 renamed from spa-assistant -->\n<!-- Changed (Setup v3): also gated on the \"agent\" module. The widget is floating, not a nav item, so\n CapItem.moduleKey could never reach it \u2014 a tenant who switched the assistant off in Getting Started\n still had it hovering over every page. -->\n<spa-agent *ngIf=\"loggedin && dataService.appConfig.multitenant && setupService.isModuleEnabled('agent')\"></spa-agent>\n\n<!-- Added: mobile bottom tab bar. Mounted ONCE here, outside every layout block, because the bar is\n position: fixed and so serves top, top-modern, side and side-modern from this single instance.\n smallScreen is nav-menu's existing (max-width: 600px) BreakpointObserver \u2014 the one source of truth\n for this feature, so no second breakpoint is introduced. Never renders on desktop. -->\n<spa-bottom-tabs *ngIf=\"loggedin && smallScreen\"></spa-bottom-tabs>\n", styles: ["a.navbar-brand{white-space:normal;text-align:center;word-break:break-all}html{font-size:14px}.box-shadow{box-shadow:0 .25rem .75rem #0000000d}.toolbar-item-spacer{flex:1 1 auto}.toolbar{height:60px;display:flex;align-items:center;background-color:#03a;color:#fff;margin-bottom:0!important}.toolbar button,.toolbar .mat-mdc-button,.toolbar .mat-mdc-icon-button{color:#fff!important}.toolbar mat-icon{color:#fff!important}.stack-top{z-index:9;margin:20px}.navitems{background-color:#03a}.app-container{height:90%;margin:0}.app-sidenav{width:200px;border:1px solid rgb(192,190,199)}.side-color{background-color:#e6f4ff}.app-sidenav mat-list-item{display:flex!important;align-items:center!important}.app-sidenav mat-icon{display:inline-flex!important;align-items:center!important;vertical-align:middle!important}.app-sidenav mat-expansion-panel-header mat-icon{display:inline-flex!important;align-items:center!important;vertical-align:middle!important}::ng-deep .app-sidenav .mat-expansion-panel-body{padding-bottom:5px!important;padding-right:5px!important}::ng-deep .app-sidenav .mdc-list{padding-bottom:0!important}.sm-layout{display:flex;min-height:100vh;position:relative}.sm-sidebar{position:fixed;top:0;left:0;bottom:0;width:260px;z-index:1030;overflow:hidden;transition:width .3s cubic-bezier(.4,0,.2,1)}.sm-sidebar-bg{position:absolute;inset:0;z-index:0}.sm-sidebar-bg-image{position:absolute;inset:0;background-size:cover;background-position:center}.sm-sidebar-bg-overlay{position:absolute;inset:0}.sm-sidebar-content{position:relative;z-index:1;display:flex;flex-direction:column;height:100%;color:#fff}.sm-brand{display:flex;align-items:center;padding:18px 15px 10px;min-height:60px;text-decoration:none;white-space:nowrap;overflow:hidden}.sm-brand img{height:34px;width:34px;object-fit:contain;margin-right:12px;flex-shrink:0}.sm-brand-name{font-size:16px;font-weight:500;letter-spacing:.5px;color:#fff;overflow:hidden;text-overflow:ellipsis;transition:opacity .2s ease}.sm-profile{display:flex;align-items:center;padding:12px 15px;white-space:nowrap;overflow:hidden}.sm-profile-icon{font-size:34px!important;width:34px!important;height:34px!important;margin-right:12px;flex-shrink:0;color:#fffc}.sm-profile-info{overflow:hidden;transition:opacity .2s ease}.sm-profile-name{font-size:14px;font-weight:500;color:#fff;line-height:1.3;overflow:hidden;text-overflow:ellipsis}.sm-profile-role{font-size:11px;color:#fff9;line-height:1.3;overflow:hidden;text-overflow:ellipsis}.sm-sidebar mat-divider{border-color:#ffffff26!important;margin:0 15px}.sm-menu-scroll{flex:1;overflow-y:auto;overflow-x:hidden;padding:8px 0}.sm-menu-scroll::-webkit-scrollbar{width:4px}.sm-menu-scroll::-webkit-scrollbar-track{background:transparent}.sm-menu-scroll::-webkit-scrollbar-thumb{background:#fff3;border-radius:2px}.sm-menu-item{display:flex;align-items:center;padding:10px 15px;margin:2px 15px;border-radius:4px;cursor:pointer;color:#fff;font-size:13px;font-weight:400;letter-spacing:.3px;transition:all .15s ease;text-decoration:none;white-space:nowrap;overflow:hidden}.sm-menu-item:hover{background:#ffffff1f}.sm-menu-item.sm-active{background-color:#fff;color:#3c4858;box-shadow:0 4px 20px #00000024,0 7px 10px -5px #0003;font-weight:500}.sm-menu-item.sm-active .sm-menu-icon{color:#3c4858}.sm-menu-icon{font-size:20px!important;width:24px!important;height:24px!important;display:inline-flex!important;align-items:center;justify-content:center;margin-right:12px;flex-shrink:0;color:#fffc;transition:color .15s ease}.sm-menu-text{flex:1;overflow:hidden;text-overflow:ellipsis;transition:opacity .2s ease}.sm-caret{font-size:18px!important;width:18px!important;height:18px!important;transition:transform .3s cubic-bezier(.4,0,.2,1);flex-shrink:0;color:#fff9}.sm-caret.sm-caret-open{transform:rotate(180deg)}.sm-active .sm-caret{color:#3c4858}.sm-submenu{max-height:0;overflow:hidden;transition:max-height .35s cubic-bezier(.4,0,.2,1)}.sm-submenu.sm-submenu-open{max-height:1000px}.sm-submenu-item{display:flex;align-items:center;padding:8px 15px 8px 30px;margin:1px 15px;border-radius:4px;cursor:pointer;color:#fffc;font-size:12px;font-weight:400;transition:all .15s ease;white-space:nowrap;overflow:hidden}.sm-submenu-item:hover{background:#ffffff1f;color:#fff}.sm-submenu-item.sm-active{background-color:#fff;color:#3c4858;box-shadow:0 4px 20px #00000024,0 7px 10px -5px #0003;font-weight:500}.sm-submenu-item.sm-active .sm-sub-icon{color:#3c4858}.sm-sub-icon{font-size:16px!important;width:20px!important;height:20px!important;display:inline-flex!important;align-items:center;justify-content:center;margin-right:10px;flex-shrink:0;color:#fff9}.sm-initials{width:20px;height:20px;border-radius:50%;background:#ffffff26;display:inline-flex;align-items:center;justify-content:center;font-size:9px;font-weight:600;margin-right:10px;flex-shrink:0;color:#fffc}.sm-active .sm-initials{background:#3c48581f;color:#3c4858}.sm-main{flex:1;min-width:0;margin-left:260px;min-height:100vh;display:flex;flex-direction:column;transition:margin-left .3s cubic-bezier(.4,0,.2,1);background-color:#eef2f7}.sm-topbar{display:flex;align-items:center;padding:8px 16px;min-height:56px;background-color:#eef2f7;background-image:radial-gradient(circle,#d5dbe3 1px,transparent 1px);background-size:16px 16px;border-bottom:1px solid rgba(0,0,0,.08);position:sticky;top:0;z-index:1020;transition:background .3s ease,backdrop-filter .3s ease}.sm-topbar-scrolled{background-color:#eef2f78c;background-image:none;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);box-shadow:0 1px 3px #0000000f}.sm-topbar-spacer{flex:1 1 auto}.sm-topbar-logo{height:32px;width:32px;object-fit:contain;margin-right:8px}.sm-topbar-brand{font-size:18px;font-weight:500;margin-right:8px;white-space:nowrap}.sm-topbar-label{font-size:14px;margin-right:4px;display:inline-flex;align-items:center;align-self:center;height:40px;line-height:1}.sm-topbar .mat-mdc-icon-button{display:inline-flex!important;align-items:center!important;justify-content:center!important}.sm-content{flex:1;padding:12px;min-width:0;background-color:#e5eaf2;background-image:radial-gradient(ellipse at 50% 45%,#fffffff2,#fff6 35%,#fff0 60%),radial-gradient(circle,#bec7d4 1px,transparent 1px);background-size:100% 100%,16px 16px;min-height:calc(100vh - 104px)}.sm-footer{padding:12px 16px;text-align:center;font-size:12px;color:#999;border-top:1px solid #e0e0e0;background:#fff}.sm-footer a{color:inherit;cursor:pointer}.sm-footer a:hover{text-decoration:underline}.sm-backdrop{display:none;position:fixed;inset:0;background:#00000080;z-index:1025}.sm-layout.sm-mini .sm-sidebar{width:80px}.sm-layout.sm-mini .sm-main{margin-left:80px}.sm-layout.sm-mini .sm-brand-name,.sm-layout.sm-mini .sm-profile-info,.sm-layout.sm-mini .sm-menu-text,.sm-layout.sm-mini .sm-caret,.sm-layout.sm-mini .sm-submenu{display:none}.sm-layout.sm-mini .sm-sidebar mat-divider{margin:0 10px}.sm-layout.sm-mini .sm-brand{justify-content:center;padding:18px 0 10px}.sm-layout.sm-mini .sm-brand img{margin-right:0}.sm-layout.sm-mini .sm-profile{justify-content:center;padding:12px 0}.sm-layout.sm-mini .sm-profile-icon{margin-right:0}.sm-layout.sm-mini .sm-menu-item{justify-content:center;padding:12px 0;margin:2px 0}.sm-layout.sm-mini .sm-menu-icon{margin-right:0;font-size:22px!important}.sm-layout.sm-mini-hovered .sm-sidebar{width:260px;box-shadow:4px 0 20px #0000004d}.sm-layout.sm-mini-hovered .sm-main{margin-left:80px}.sm-layout.sm-mini-hovered .sm-brand-name,.sm-layout.sm-mini-hovered .sm-profile-info,.sm-layout.sm-mini-hovered .sm-menu-text,.sm-layout.sm-mini-hovered .sm-caret{display:initial}.sm-layout.sm-mini-hovered .sm-submenu{display:block}.sm-layout.sm-mini-hovered .sm-sidebar mat-divider{margin:0 15px}.sm-layout.sm-mini-hovered .sm-brand{justify-content:flex-start;padding:18px 15px 10px}.sm-layout.sm-mini-hovered .sm-brand img{margin-right:12px}.sm-layout.sm-mini-hovered .sm-profile{justify-content:flex-start;padding:12px 15px}.sm-layout.sm-mini-hovered .sm-profile-icon{margin-right:12px}.sm-layout.sm-mini-hovered .sm-menu-item{justify-content:flex-start;padding:10px 15px;margin:2px 15px}.sm-layout.sm-mini-hovered .sm-menu-icon{margin-right:12px;font-size:20px!important}@media (max-width: 600px){.sm-sidebar{transform:translate(-100%);transition:transform .3s cubic-bezier(.4,0,.2,1);width:260px!important}.sm-layout.sm-mobile-open .sm-sidebar{transform:translate(0)}.sm-layout.sm-mobile-open .sm-backdrop{display:block}.sm-main{margin-left:0!important}.sm-layout.sm-mini .sm-sidebar{width:260px!important}.sm-layout.sm-mini .sm-brand-name,.sm-layout.sm-mini .sm-profile-info,.sm-layout.sm-mini .sm-menu-text,.sm-layout.sm-mini .sm-caret{display:initial}.sm-layout.sm-mini .sm-submenu{display:block}.sm-layout.sm-mini .sm-sidebar mat-divider{margin:0 15px}.sm-layout.sm-mini .sm-menu-item{justify-content:flex-start;padding:10px 15px;margin:2px 15px}.sm-layout.sm-mini .sm-menu-icon{margin-right:12px;font-size:20px!important}.sm-layout.sm-mini .sm-brand{justify-content:flex-start;padding:18px 15px 10px}.sm-layout.sm-mini .sm-brand img{margin-right:12px}.sm-layout.sm-mini .sm-profile{justify-content:flex-start;padding:12px 15px}.sm-layout.sm-mini .sm-profile-icon{margin-right:12px}}.tm-navbar{position:sticky;top:0;z-index:1030;background-color:#03a;color:#fff;box-shadow:0 2px 12px #0000001f;transition:background-color .3s ease,backdrop-filter .3s ease,box-shadow .3s ease}.tm-navbar.tm-scrolled{background-color:#0033aad9;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);box-shadow:0 4px 18px #0000002e}.tm-bar{display:flex;align-items:center;flex-wrap:nowrap;min-height:60px;padding:6px 16px;gap:4px}.tm-brand{display:flex;align-items:center;gap:12px;flex-shrink:0;margin-right:18px;cursor:pointer;-webkit-user-select:none;user-select:none}.tm-logo{height:40px;width:auto;object-fit:contain}.tm-app-name{font-size:20px;font-weight:500;line-height:1.2;white-space:nowrap}.tm-tenant{font-size:12px;font-weight:400;color:#ffffffb3;line-height:1.2}.tm-toggler{display:none;margin-left:auto;background:transparent;border:1px solid rgba(255,255,255,.4);border-radius:8px;color:#fff;cursor:pointer;align-items:center;justify-content:center;width:40px;height:40px}.tm-toggler mat-icon{color:#fff}.tm-menu{display:flex;align-items:center;flex-wrap:wrap;flex:1 1 auto;min-width:0;row-gap:4px;justify-content:flex-end}.tm-item-wrap{display:flex;align-items:center;position:relative}.tm-item-wrap:not(:first-child):before{content:\"\";width:1px;height:18px;background:#ffffff2e;margin:0 2px;flex-shrink:0}.tm-item{position:relative;display:inline-flex;align-items:center;gap:6px;height:40px;padding:0 14px;margin:0 2px;background:transparent;border:none;border-radius:8px;color:#ffffffeb;font-size:14px;font-weight:400;letter-spacing:.2px;white-space:nowrap;cursor:pointer;transition:background .18s ease,color .18s ease}.tm-item:after{content:\"\";position:absolute;left:12px;right:12px;bottom:5px;height:1px;border-radius:1px;background:#ffffff80;transform:scaleX(0);transform-origin:center;transition:transform .25s cubic-bezier(.4,0,.2,1)}.tm-item:hover{color:#fff}.tm-item:hover:after{transform:scaleX(1)}.tm-item.tm-item-active:after{transform:scaleX(1)}.tm-item-icon{font-size:19px!important;width:19px!important;height:19px!important;display:inline-flex!important;align-items:center;justify-content:center;color:inherit!important}.tm-item-caret{font-size:18px!important;width:18px!important;height:18px!important;display:inline-flex!important;align-items:center;justify-content:center;margin-left:-2px;margin-right:-4px;color:#ffffffb3!important;transition:transform .2s ease}.tm-item:hover .tm-item-caret,.tm-item-active .tm-item-caret{color:#fff!important}.tm-actions{display:flex;align-items:center;gap:2px;flex-shrink:0;margin-left:auto}.tm-actions .mat-mdc-icon-button,.tm-action-btn{display:inline-flex!important;align-items:center!important;justify-content:center!important;color:#fff!important}.tm-actions mat-icon{color:#fff!important}.tm-divider-v{width:1px;height:24px;background:#ffffff38;margin:0 6px;flex-shrink:0}.tm-user-btn{display:inline-flex!important;align-items:center!important;gap:6px;height:40px;color:#fff!important;border-radius:8px;transition:background .18s ease}.tm-user-btn:hover{background:#ffffff1f}.tm-user-icon{font-size:24px!important;width:24px!important;height:24px!important;color:#fff!important}.tm-user-name{font-size:14px;font-weight:400;white-space:nowrap}::ng-deep .tm-submenu-panel .tm-sub-active{background:#0033aa14;font-weight:600;color:#03a}::ng-deep .tm-submenu-panel .tm-sub-active .mat-icon{color:#03a}@media (max-width: 991px){.tm-toggler{display:inline-flex}.tm-menu,.tm-actions{display:none;position:absolute;left:0;right:0;top:100%;flex-direction:column;align-items:stretch;background:#03a;padding:8px 12px;box-shadow:0 8px 18px #0003;z-index:1029}.tm-menu.tm-menu-open{display:flex}.tm-actions.tm-actions-open{display:flex;top:100%;border-top:1px solid rgba(255,255,255,.12)}.tm-item-wrap{width:100%}.tm-item-wrap:not(:first-child):before{width:100%;height:1px;margin:2px 0}.tm-item{width:100%;justify-content:flex-start;height:44px;margin:0}.tm-item:after{inset:8px auto 8px 0;width:4px;height:auto;transform:scaleY(0);transform-origin:center}.tm-item:hover:after,.tm-item.tm-item-active:after{transform:scaleY(1)}.tm-item-caret{margin-left:auto}.tm-divider-v{display:none}.tm-user-btn{justify-content:flex-start;width:100%}}@media (max-width: 600px){.has-bottom-tabs{padding-bottom:calc(64px + env(safe-area-inset-bottom,0px))!important}.sm-footer{display:none}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i7$2.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7$2.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7$2.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "directive", type: i4$4.MatBadge, selector: "[matBadge]", inputs: ["matBadgeColor", "matBadgeOverlap", "matBadgeDisabled", "matBadgePosition", "matBadge", "matBadgeDescription", "matBadgeSize", "matBadgeHidden"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i17.MatNavList, selector: "mat-nav-list", exportAs: ["matNavList"] }, { kind: "component", type: i17.MatListItem, selector: "mat-list-item, a[mat-list-item], button[mat-list-item]", inputs: ["activated"], exportAs: ["matListItem"] }, { kind: "component", type: i17.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: i19$1.MatSidenav, selector: "mat-sidenav", inputs: ["fixedInViewport", "fixedTopGap", "fixedBottomGap"], exportAs: ["matSidenav"] }, { kind: "component", type: i19$1.MatSidenavContainer, selector: "mat-sidenav-container", exportAs: ["matSidenavContainer"] }, { kind: "component", type: i19$1.MatSidenavContent, selector: "mat-sidenav-content" }, { kind: "component", type: i20.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { kind: "directive", type: i1$1.RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "directive", type: i1$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: i21.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i21.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "component", type: LoaderComponent, selector: "spa-loader", inputs: ["logo"] }, { kind: "component", type: ToastComponent, selector: "spa-toast" }, { kind: "component", type: AgentComponent, selector: "spa-agent", inputs: ["pageMode"] }, { kind: "component", type: BottomTabsComponent, selector: "spa-bottom-tabs" }, { kind: "component", type: OfflineIndicatorComponent, selector: "spa-offline-indicator" }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }] }); }
20939
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: NavMenuComponent, isStandalone: false, selector: "spa-nav-menu", inputs: { appConfig: "appConfig", footer: "footer" }, host: { listeners: { "window:scroll": "onWindowScroll()" } }, ngImport: i0, template: "<header *ngIf=\"loggedin && dataService.appConfig.navigation == 'top'\">\n\n <!-- Changed: Removed mb-3 class to eliminate gap between toolbar and content -->\n <nav class=\"toolbar navbar navbar-expand-sm navbar-toggleable-sm navbar-light border-bottom box-shadow\" style=\"padding-right: 10px;\">\n\n\n <div class=\"container-fluid\" style=\"padding-right: 0px;\">\n\n <img *ngIf=\"appConfig.logo!=''\" [src]=\"appConfig.logo\" style=\"height: 50px; margin-right: 2em\" />\n\n <div>\n <!-- <div style=\"font-size: 20px;\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px;\">\n {{tenantName}}\n </div> -->\n\n <div *ngIf=\"!dataService.appConfig.multitenant\" style=\"font-size: 22px;\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant\" style=\"font-size: 20px; ; font-weight: 400;\" [ngStyle]=\"{'margin-top': dataService.appConfig.multitenant ? '12px' : ''}\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px; margin-bottom: 5px;\">\n {{tenantName}}\n </div>\n\n </div>\n\n\n\n <button class=\"navbar-toggler\" type=\"button\" data-toggle=\"collapse\" data-target=\".navbar-collapse\" aria-label=\"Toggle navigation\" [attr.aria-expanded]=\"isExpanded\" (click)=\"toggle()\">\n <span class=\"navbar-toggler-icon\"></span>\n </button>\n\n <div *ngIf=\"myRole\" class=\" navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse stack-top\" style=\"margin-right: 0px;\" [ngClass]=\"{ show: isExpanded, navitems: isExpanded }\" >\n\n <button mat-icon-button (click)=\"logoff()\" > <mat-icon>logout</mat-icon> </button>\n\n <div *ngIf=\"dataService.appConfig.multitenant\">\n\n <button mat-icon-button (click)=\"redirectTo('home/tenancy/settings')\" > <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon> </button>\n\n <!-- Removed: Support icon \u2014 replaced by floating assistant chat widget -->\n </div>\n\n\n <button id=\"btnUser\" mat-button [matMenuTriggerFor]=\"profileMenu\" ><mat-icon style=\"font-size: 24px;\">account_circle</mat-icon> &nbsp;{{loggedUserFullName}}</button>\n\n <mat-menu #profileMenu=\"matMenu\">\n <button id=\"btnProfile\" mat-menu-item (click)=\"redirectTo('home/user/profile')\" >Profile</button>\n <button id=\"btnLogOff\" mat-menu-item (click)=\"logoff()\">Log Off</button>\n </mat-menu>\n\n <div *ngFor=\"let item of reversedCapItems\">\n\n <!-- Menu Item \u2014 Added: isFeatureAllowed check for plan-based gating -->\n <button id=\"btnMenu\" *ngIf=\"myRole[item.name] && !item.capSubItems && item.showMenu && isFeatureAllowed(item)\" mat-button (click)=\"redirectTo(item.link)\">{{item.display}}</button>\n\n <!-- Menu Item with Sub items ignored \u2014 Added: isFeatureAllowed check -->\n <button id=\"btnMenu\" *ngIf=\"myRole[item.name] && item.capSubItems && item.showMenu && item.ignoreSubsDisplay && isFeatureAllowed(item)\" mat-button (click)=\"redirectTo(item.link)\">{{item.display}}</button>\n\n <!-- Menu Item with Sub items to display \u2014 Added: isFeatureAllowed check -->\n <button id=\"btnMenu\" *ngIf=\"myRole[item.name] && item.capSubItems && item.showMenu && !item.ignoreSubsDisplay && isFeatureAllowed(item)\" mat-button [matMenuTriggerFor]=\"adminMenu\">{{item.display}}</button>\n\n\n <!-- Sub Menu Items \u2014 Added: isFeatureAllowed check on sub-items -->\n <mat-menu #adminMenu=\"matMenu\">\n\n <div *ngFor=\"let subItem of item.capSubItems\">\n\n <button *ngIf=\"myRole[subItem.name] && subItem.showMenu && isFeatureAllowed(subItem)\" mat-menu-item (click)=\"redirectTo(subItem.link)\">{{subItem.display}}</button>\n\n </div>\n\n </mat-menu>\n\n </div>\n\n </div>\n\n\n </div>\n\n </nav>\n\n</header>\n\n<!-- Changed: Removed top/bottom padding to eliminate gaps, but kept left/right padding for content spacing -->\n<div class=\"container-fluid tin-bg-image\" [class.has-bottom-tabs]=\"loggedin && smallScreen\" *ngIf=\"dataService.appConfig.navigation == 'top'\" style=\"padding: 12px 12px; margin: 0;\"> <!-- Changed: bottom padding so the fixed tab bar cannot cover the last row -->\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n\n\n\n<!-- ============================================================ -->\n<!-- TOP-MODERN (navigation == 'top-modern') -->\n<!-- Changed: Modernised horizontal top navigation -->\n<!-- ============================================================ -->\n<header *ngIf=\"loggedin && dataService.appConfig.navigation == 'top-modern'\">\n\n <nav class=\"tm-navbar\" [class.tm-scrolled]=\"topbarScrolled\">\n <div class=\"tm-bar\">\n\n <!-- Brand -->\n <div class=\"tm-brand\" (click)=\"modernNavigate('')\">\n <img *ngIf=\"appConfig.logo\" [src]=\"appConfig.logo\" class=\"tm-logo\" alt=\"logo\" />\n <div class=\"tm-brand-text\">\n <div class=\"tm-app-name\">{{appConfig.appName}}</div>\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" class=\"tm-tenant\">{{tenantName}}</div>\n </div>\n </div>\n\n <!-- Mobile toggle -->\n <button class=\"tm-toggler\" type=\"button\" aria-label=\"Toggle navigation\"\n [attr.aria-expanded]=\"isExpanded\" (click)=\"toggle()\">\n <mat-icon>{{isExpanded ? 'close' : 'menu'}}</mat-icon>\n </button>\n\n <!-- Menu items -->\n <div class=\"tm-menu\" [class.tm-menu-open]=\"isExpanded\">\n\n <ng-container *ngFor=\"let item of dataService.appConfig.capItems\">\n <div class=\"tm-item-wrap\"\n *ngIf=\"myRole[item.name] && item.showMenu && isFeatureAllowed(item)\">\n\n <!-- Simple item (no sub-items, or sub-items ignored for display) -->\n <button *ngIf=\"!item.capSubItems || item.ignoreSubsDisplay\"\n class=\"tm-item\"\n [class.tm-item-active]=\"isActiveRoute(item.link)\"\n (click)=\"modernNavigate(item.link)\">\n <mat-icon *ngIf=\"item.icon && item.icon != 'navigate_next'\" class=\"tm-item-icon\">{{item.icon}}</mat-icon>\n <span class=\"tm-item-text\">{{item.display}}</span>\n </button>\n\n <!-- Parent item with displayed sub-items -->\n <ng-container *ngIf=\"item.capSubItems && !item.ignoreSubsDisplay\">\n <button class=\"tm-item tm-item-parent\"\n [class.tm-item-active]=\"isParentActive(item)\"\n [matMenuTriggerFor]=\"tmSubMenu\">\n <mat-icon *ngIf=\"item.icon && item.icon != 'navigate_next'\" class=\"tm-item-icon\">{{item.icon}}</mat-icon>\n <span class=\"tm-item-text\">{{item.display}}</span>\n <!-- Changed: Caret signals this item has more items beneath it -->\n <mat-icon class=\"tm-item-caret\">expand_more</mat-icon>\n </button>\n\n <mat-menu #tmSubMenu=\"matMenu\" class=\"tm-submenu-panel\" [overlapTrigger]=\"false\" yPosition=\"below\">\n <ng-container *ngFor=\"let sub of getSubItems(item)\">\n <button *ngIf=\"myRole[sub.name] && sub.showMenu && isFeatureAllowed(sub)\"\n mat-menu-item\n [class.tm-sub-active]=\"isActiveRoute(sub.link)\"\n (click)=\"modernNavigate(sub.link)\">\n <mat-icon *ngIf=\"sub.icon && sub.icon != 'navigate_next'\">{{sub.icon}}</mat-icon>\n <span>{{sub.display}}</span>\n </button>\n </ng-container>\n </mat-menu>\n </ng-container>\n\n </div>\n </ng-container>\n\n </div>\n\n <!-- Right-side actions -->\n <div class=\"tm-actions\" [class.tm-actions-open]=\"isExpanded\">\n\n <ng-container *ngIf=\"dataService.appConfig.multitenant\">\n <button mat-icon-button class=\"tm-action-btn\" (click)=\"modernNavigate('home/tenancy/settings')\" matTooltip=\"Organisation Settings\">\n <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon>\n </button>\n <button *ngIf=\"setupService.enabled && ((setupCount$ | async) || 0) > 0 && !smallScreen\" mat-icon-button class=\"tm-action-btn\" (click)=\"modernNavigate('home/setup')\" matTooltip=\"Getting Started\"> <!-- Added: Setup readiness badge \u2014 hidden when complete or unconfigured -->\n <mat-icon [matBadge]=\"setupCount$ | async\" matBadgeColor=\"warn\" matBadgeSize=\"small\">rocket_launch</mat-icon>\n </button>\n <button *ngIf=\"!smallScreen\" mat-icon-button class=\"tm-action-btn\" (click)=\"modernNavigate('home/workflow/notifications')\" matTooltip=\"Notifications\">\n <mat-icon [matBadge]=\"notificationCount$ | async\" [matBadgeHidden]=\"(notificationCount$ | async) === 0\" matBadgeColor=\"warn\" matBadgeSize=\"small\">notifications</mat-icon>\n </button>\n <spa-offline-indicator *ngIf=\"!smallScreen\"></spa-offline-indicator> <!-- Changed: TinSync connection + pending-sync indicator -->\n </ng-container>\n\n <span class=\"tm-divider-v\"></span>\n\n <!-- Profile -->\n <button mat-button class=\"tm-user-btn\" [matMenuTriggerFor]=\"tmProfileMenu\">\n <mat-icon class=\"tm-user-icon\">account_circle</mat-icon>\n <span class=\"tm-user-name\">{{loggedUserFullName}}</span>\n </button>\n\n <mat-menu #tmProfileMenu=\"matMenu\" [overlapTrigger]=\"false\" yPosition=\"below\">\n <button mat-menu-item (click)=\"modernNavigate('home/user/profile')\">\n <mat-icon>person</mat-icon><span>Profile</span>\n </button>\n <mat-divider></mat-divider>\n <button mat-menu-item (click)=\"logoff()\">\n <mat-icon>logout</mat-icon><span>Log Off</span>\n </button>\n </mat-menu>\n\n <!-- Sign out \u2014 Changed: aligned via flex-centred action button -->\n <button mat-icon-button class=\"tm-action-btn tm-signout\" (click)=\"logoff()\" matTooltip=\"Sign Out\">\n <mat-icon>logout</mat-icon>\n </button>\n\n </div>\n\n </div>\n </nav>\n\n</header>\n\n<!-- Top-modern page content \u2014 Changed: use original 'top' background (tin-bg-image), no footer bar -->\n<div class=\"container-fluid tin-bg-image\" [class.has-bottom-tabs]=\"loggedin && smallScreen\" *ngIf=\"loggedin && dataService.appConfig.navigation == 'top-modern'\" style=\"padding: 12px 12px; margin: 0;\"> <!-- Changed: bottom padding so the fixed tab bar cannot cover the last row -->\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n<!-- Not logged in fallback for top-modern -->\n<div class=\"tin-bg-image\" *ngIf=\"!loggedin && dataService.appConfig.navigation == 'top-modern'\">\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n\n\n\n<!-- SIDE -->\n<mat-toolbar class=\"tin-bg-image-toolbar\" *ngIf=\"loggedin && dataService.appConfig.navigation == 'side'\" style=\"padding: 0px 8px;\">\n\n <button mat-icon-button (click)=\"toggle()\" matTooltip=\"Menu\">\n <mat-icon>menu</mat-icon>\n </button>\n\n <img [src]=\"dataService.appConfig.logo\" style=\"height: 50px;\" />\n\n <div style=\"padding-left: 10px; \">\n\n <div style=\"font-size: 22px; font-weight: 400;\">\n {{appConfig.appName}}\n </div>\n\n <!-- <div style=\"font-size: 20px; height: 25px; font-weight: 400;\" [ngStyle]=\"{'margin-top': dataService.appConfig.multitenant ? '12px' : ''}\">\n {{appConfig.appName}}\n </div> -->\n\n <!-- <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px; margin-bottom: 5px;\">\n {{tenantName}}\n </div> -->\n\n </div>\n\n\n\n <span class=\"toolbar-item-spacer\"></span>\n\n <!-- buttons -->\n\n <div *ngIf=\"dataService.appConfig.multitenant\" style=\"display: flex; align-items: center;\">\n\n <!-- <label style=\"font-size: 14px;\">Hi, {{loggedUserFullName}}</label> -->\n\n <button mat-icon-button (click)=\"redirectTo('home/tenancy/settings')\" matTooltip=\"Organisation Settings\">\n <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon>\n </button>\n <label style=\"font-size: 14px;margin-right: 20px;\">{{tenantName}}</label>\n\n <!-- Changed: Support/help icon removed \u2014 replaced by floating agent chat widget -->\n\n <button *ngIf=\"setupService.enabled && ((setupCount$ | async) || 0) > 0 && !smallScreen\" mat-icon-button (click)=\"redirectTo('home/setup')\" matTooltip=\"Getting Started\"> <!-- Added: Setup readiness badge \u2014 hidden when complete or unconfigured -->\n <mat-icon [matBadge]=\"setupCount$ | async\" matBadgeColor=\"warn\" matBadgeSize=\"small\">rocket_launch</mat-icon>\n </button>\n <button *ngIf=\"!smallScreen\" mat-icon-button (click)=\"redirectTo('home/workflow/notifications')\" matTooltip=\"Notifications\">\n <mat-icon [matBadge]=\"notificationCount$ | async\" [matBadgeHidden]=\"(notificationCount$ | async) === 0\" matBadgeColor=\"warn\" matBadgeSize=\"small\">notifications</mat-icon>\n </button>\n\n <spa-offline-indicator *ngIf=\"!smallScreen\"></spa-offline-indicator> <!-- Changed: TinSync connection + pending-sync indicator -->\n\n </div>\n\n\n\n <button mat-icon-button matTooltip=\"My Account\" [matMenuTriggerFor]=\"userAccountMenu\"><mat-icon>account_circle</mat-icon></button>\n <label style=\"font-size: 14px;\">{{loggedUserFullName}}</label>\n\n <button *ngIf=\"!smallScreen\" mat-icon-button (click)=\"logoff()\" matTooltip=\"Signout\">\n <mat-icon>logout</mat-icon>\n </button>\n\n\n <!-- my account menu -->\n <mat-menu #userAccountMenu [overlapTrigger]=\"false\" yPosition=\"below\">\n\n\n <button mat-menu-item routerLink=\"home/user/profile\">\n <mat-icon>person</mat-icon><span>Profile</span>\n </button>\n\n <!-- Removed: Help menu item \u2014 replaced by floating assistant chat widget -->\n\n <mat-divider></mat-divider>\n\n <button mat-menu-item (click)=\"logoff()\">\n <mat-icon>logout</mat-icon>Logout\n </button>\n\n </mat-menu>\n\n</mat-toolbar>\n\n\n\n\n<mat-sidenav-container class=\"app-container\" [hasBackdrop]=\"smallScreen\" *ngIf=\"loggedin && dataService.appConfig.navigation == 'side'\">\n\n <mat-sidenav #sidenav [mode]=\"smallScreen ? 'over' : 'side'\" [class.mat-elevation-z4]=\"true\" [opened]=\"isExpanded\" class=\"app-sidenav side-color\" style=\"height: 100%;\"\n [ngStyle]=\"{'width': dataService.appConfig.navWidth}\">\n <mat-nav-list >\n\n <ng-container *ngFor=\"let cap of dataService.appConfig.capItems\" >\n\n <!-- Menu item \u2014 Added: isFeatureAllowed check for plan-based gating -->\n <mat-list-item [routerLink]=\"cap.link\" *ngIf=\"myRole[cap.name] && cap.showMenu && (!cap.capSubItems || cap.capSubItems && cap.ignoreSubsDisplay) && isFeatureAllowed(cap)\" style=\"height: 40px;font-size: 15px;\"\n (click)=\"smallScreen ? toggle() : null\">\n <mat-icon [ngStyle]=\"{'color': cap.color}\" style=\"margin-right: 5px;\">{{cap.icon}}</mat-icon>{{cap.display}}\n </mat-list-item>\n\n <!-- Menu With Sub items \u2014 Added: isFeatureAllowed check -->\n <mat-expansion-panel class=\"side-color\" [class.mat-elevation-z0]=\"true\" *ngIf=\"myRole[cap.name] && cap.showMenu && cap.capSubItems && !cap.ignoreSubsDisplay && isFeatureAllowed(cap)\">\n\n <mat-expansion-panel-header style=\"height: 40px;padding-left: 15px;\">\n <mat-icon [ngStyle]=\"{'color': cap.color}\" style=\"margin-right: 5px;\">{{cap.icon != 'navigate_next' ? cap.icon : 'fiber_manual_record' }}</mat-icon>{{cap.display}}\n </mat-expansion-panel-header>\n\n <!-- Sub items - Changed: Use ng-container to avoid blank spaces for hidden items -->\n <mat-nav-list>\n <ng-container *ngFor=\"let capSub of getSubItems(cap)\">\n <mat-list-item [routerLink]=\"capSub.link\" style=\"height: 30px; font-size: 15px; padding-left: 4px; padding-right: 10px; margin-bottom: 5px;\" (click)=\"smallScreen ? toggle() : null\" *ngIf=\"myRole[capSub.name] && capSub.showMenu && isFeatureAllowed(capSub)\" [matTooltip]=\"capSub.display\" matTooltipPosition=\"right\">\n <mat-icon [ngStyle]=\"{'color': capSub.color}\" style=\"margin-right: 5px;\">{{capSub.icon}}</mat-icon>{{capSub.display}}\n </mat-list-item>\n </ng-container>\n </mat-nav-list>\n\n </mat-expansion-panel>\n\n </ng-container>\n\n </mat-nav-list>\n </mat-sidenav>\n\n\n\n <mat-sidenav-content class=\"tin-bg-image\" [class.has-bottom-tabs]=\"loggedin && smallScreen\" style=\"padding: 0px 12px;\" *ngIf=\"loggedin && dataService.appConfig.navigation == 'side'\"> <!-- Changed: bottom padding so the fixed tab bar cannot cover the last row -->\n <hr style=\"margin-top: 0px;\">\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n </mat-sidenav-content>\n\n</mat-sidenav-container>\n\n\n<!-- footer -->\n<!-- Changed: hidden on mobile \u2014 the bottom tab bar owns the bottom edge there, and a footer under a fixed\n bar is either invisible or a second competing strip of chrome -->\n<div class=\"tin-center\" *ngIf=\"loggedin && !smallScreen && dataService.appConfig.navigation == 'side'\">\n <label style=\"text-align: center; font-size: 12px;\">&copy; {{nowDate | date : 'yyyy'}} <a color=\"primary\" class=\"terms-link\" [href]=\"appConfig.siteUrl\" target=\"_blank\">{{footer}}</a> | <a color=\"primary\" class=\"terms-link\" style=\"cursor: pointer;\" (click)=\"openTerms()\">Terms</a> | <a color=\"primary\" class=\"terms-link\" style=\"cursor: pointer;\" (click)=\"openPrivacy()\">Privacy Policy</a></label>\n</div>\n\n\n<div class=\"tin-bg-image\" *ngIf=\"!loggedin && dataService.appConfig.navigation == 'side'\">\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n\n<!-- SIDE-MODERN -->\n\n<!-- Changed: Side-modern navigation layout -->\n<div class=\"sm-layout\"\n *ngIf=\"loggedin && dataService.appConfig.navigation == 'side-modern'\"\n [class.sm-mini]=\"isMiniSidebar && !isMiniHovered\"\n [class.sm-mini-hovered]=\"isMiniSidebar && isMiniHovered\"\n [class.sm-mobile-open]=\"smallScreen && isExpanded\">\n\n <!-- Sidebar -->\n <aside class=\"sm-sidebar\"\n (mouseenter)=\"onMiniMouseEnter()\"\n (mouseleave)=\"onMiniMouseLeave()\">\n\n <!-- Background layers -->\n <div class=\"sm-sidebar-bg\">\n <div class=\"sm-sidebar-bg-image\" *ngIf=\"appConfig.navImage\" [ngStyle]=\"{'background-image': 'url(' + appConfig.navImage + ')'}\"></div>\n <div class=\"sm-sidebar-bg-overlay\" [ngStyle]=\"{'background-color': appConfig.navColor}\"></div>\n </div>\n\n <!-- Sidebar content -->\n <div class=\"sm-sidebar-content\">\n\n <!-- Brand -->\n <div class=\"sm-brand\">\n <img *ngIf=\"appConfig.logo\" [src]=\"appConfig.logo\" alt=\"logo\" />\n <span class=\"sm-brand-name\">{{appConfig.appName}}</span>\n </div>\n\n <mat-divider></mat-divider>\n\n <!-- Profile -->\n <div class=\"sm-profile\">\n <mat-icon class=\"sm-profile-icon\">account_circle</mat-icon>\n <div class=\"sm-profile-info\">\n <div class=\"sm-profile-name\">{{loggedUserFullName}}</div>\n <div class=\"sm-profile-role\">{{tenantName || 'User'}}</div>\n </div>\n </div>\n\n <mat-divider></mat-divider>\n\n <!-- Scrollable menu -->\n <div class=\"sm-menu-scroll\">\n\n <ng-container *ngFor=\"let cap of dataService.appConfig.capItems\">\n\n <!-- Simple menu item (no sub-items or ignoring sub display) \u2014 Added: isFeatureAllowed check -->\n <div *ngIf=\"myRole[cap.name] && cap.showMenu && (!cap.capSubItems || cap.ignoreSubsDisplay) && isFeatureAllowed(cap)\"\n class=\"sm-menu-item\"\n [class.sm-active]=\"isActiveRoute(cap.link)\"\n (click)=\"modernNavigate(cap.link)\">\n <mat-icon class=\"sm-menu-icon\">{{cap.icon != 'navigate_next' ? cap.icon : 'dashboard'}}</mat-icon>\n <span class=\"sm-menu-text\">{{cap.display}}</span>\n </div>\n\n <!-- Parent menu item with sub-items \u2014 Added: isFeatureAllowed check -->\n <ng-container *ngIf=\"myRole[cap.name] && cap.showMenu && cap.capSubItems && !cap.ignoreSubsDisplay && isFeatureAllowed(cap)\">\n\n <!-- Parent item (toggles sub-menu) -->\n <div class=\"sm-menu-item\"\n [class.sm-active]=\"isParentActive(cap) && !isMenuOpen(cap.name)\"\n (click)=\"toggleModernMenu(cap.name)\">\n <mat-icon class=\"sm-menu-icon\">{{cap.icon != 'navigate_next' ? cap.icon : 'dashboard'}}</mat-icon>\n <span class=\"sm-menu-text\">{{cap.display}}</span>\n <mat-icon class=\"sm-caret\" [class.sm-caret-open]=\"isMenuOpen(cap.name)\">expand_more</mat-icon>\n </div>\n\n <!-- Sub-menu container (animated) -->\n <div class=\"sm-submenu\" [class.sm-submenu-open]=\"isMenuOpen(cap.name)\">\n <ng-container *ngFor=\"let sub of getSubItems(cap)\">\n <div *ngIf=\"myRole[sub.name] && sub.showMenu && isFeatureAllowed(sub)\"\n class=\"sm-submenu-item\"\n [class.sm-active]=\"isActiveRoute(sub.link)\"\n (click)=\"modernNavigate(sub.link)\">\n <mat-icon *ngIf=\"sub.icon && sub.icon != 'navigate_next'\" class=\"sm-sub-icon\">{{sub.icon}}</mat-icon>\n <span *ngIf=\"!sub.icon || sub.icon == 'navigate_next'\" class=\"sm-initials\">{{getInitials(sub.display)}}</span>\n <span class=\"sm-menu-text\">{{sub.display}}</span>\n </div>\n </ng-container>\n </div>\n\n </ng-container>\n\n </ng-container>\n\n </div>\n\n </div>\n </aside>\n\n <!-- Mobile backdrop -->\n <div class=\"sm-backdrop\" (click)=\"isExpanded = false\"></div>\n\n <!-- Main content -->\n <div class=\"sm-main\">\n\n <!-- Top bar - Changed: Added scroll class for frosted glass effect -->\n <div class=\"sm-topbar\" [class.sm-topbar-scrolled]=\"topbarScrolled\">\n <button mat-icon-button (click)=\"smallScreen ? toggle() : toggleMiniSidebar()\" matTooltip=\"Menu\">\n <mat-icon>menu</mat-icon>\n </button>\n\n <!-- Changed: Mobile branding - show logo + app name when sidebar is hidden on small screens -->\n <img *ngIf=\"smallScreen && appConfig.logo\" [src]=\"appConfig.logo\" alt=\"logo\" class=\"sm-topbar-logo\" />\n <span *ngIf=\"smallScreen\" class=\"sm-topbar-brand\">{{appConfig.appName}}</span>\n\n <span class=\"sm-topbar-spacer\"></span>\n\n <!-- Multitenant buttons -->\n <div *ngIf=\"dataService.appConfig.multitenant\" style=\"display: flex; align-items: center;\">\n <button mat-icon-button (click)=\"redirectTo('home/tenancy/settings')\" matTooltip=\"Organisation Settings\">\n <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon>\n </button>\n <span class=\"sm-topbar-label\">{{tenantName}}</span>\n\n <!-- Changed: Support/help icon removed \u2014 replaced by floating agent chat widget -->\n\n <button *ngIf=\"setupService.enabled && ((setupCount$ | async) || 0) > 0 && !smallScreen\" mat-icon-button (click)=\"redirectTo('home/setup')\" matTooltip=\"Getting Started\"> <!-- Added: Setup readiness badge \u2014 hidden when complete or unconfigured -->\n <mat-icon [matBadge]=\"setupCount$ | async\" matBadgeColor=\"warn\" matBadgeSize=\"small\">rocket_launch</mat-icon>\n </button>\n <button *ngIf=\"!smallScreen\" mat-icon-button (click)=\"redirectTo('home/workflow/notifications')\" matTooltip=\"Notifications\">\n <mat-icon [matBadge]=\"notificationCount$ | async\" [matBadgeHidden]=\"(notificationCount$ | async) === 0\" matBadgeColor=\"warn\" matBadgeSize=\"small\">notifications</mat-icon>\n </button>\n <spa-offline-indicator *ngIf=\"!smallScreen\"></spa-offline-indicator> <!-- Changed: TinSync connection + pending-sync indicator -->\n </div>\n\n <!-- Profile menu -->\n <button mat-icon-button matTooltip=\"My Account\" [matMenuTriggerFor]=\"smProfileMenu\">\n <mat-icon>account_circle</mat-icon>\n </button>\n <span class=\"sm-topbar-label\">{{loggedUserFullName}}</span>\n\n <mat-menu #smProfileMenu=\"matMenu\" [overlapTrigger]=\"false\" yPosition=\"below\">\n <button mat-menu-item routerLink=\"home/user/profile\">\n <mat-icon>person</mat-icon><span>Profile</span>\n </button>\n <!-- Changed: Help menu item removed \u2014 replaced by floating agent chat widget -->\n <mat-divider></mat-divider>\n <button mat-menu-item (click)=\"logoff()\">\n <mat-icon>logout</mat-icon>Logout\n </button>\n </mat-menu>\n\n <button *ngIf=\"!smallScreen\" mat-icon-button (click)=\"logoff()\" matTooltip=\"Signout\">\n <mat-icon>logout</mat-icon>\n </button>\n </div>\n\n <!-- Page content - Changed: Replaced tin-bg-image with sm-content modern texture -->\n <div class=\"sm-content\" [class.has-bottom-tabs]=\"loggedin && smallScreen\"> <!-- Changed: bottom padding so the fixed tab bar cannot cover the last row -->\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n </div>\n\n <!-- Footer -->\n <div class=\"sm-footer\">\n &copy; {{nowDate | date : 'yyyy'}} <a [href]=\"appConfig.siteUrl\" target=\"_blank\">{{footer}}</a> | <a (click)=\"openTerms()\">Terms</a> | <a (click)=\"openPrivacy()\">Privacy Policy</a>\n </div>\n\n </div>\n\n</div>\n\n<!-- Not logged in fallback for side-modern -->\n<div class=\"tin-bg-image\" *ngIf=\"!loggedin && dataService.appConfig.navigation == 'side-modern'\">\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n<!-- Changed: Cascading toast notifications for real-time entity changes \u2014 visible in all layouts -->\n<spa-toast *ngIf=\"loggedin && dataService.appConfig.multitenant\"></spa-toast>\n\n<!-- Changed: Floating agent chat widget \u2014 renamed from spa-assistant -->\n<!-- Changed (Setup v3): also gated on the \"agent\" module. The widget is floating, not a nav item, so\n CapItem.moduleKey could never reach it \u2014 a tenant who switched the assistant off in Getting Started\n still had it hovering over every page. -->\n<spa-agent *ngIf=\"loggedin && dataService.appConfig.multitenant && setupService.isModuleEnabled('agent')\"></spa-agent>\n\n<!-- Added: mobile bottom tab bar. Mounted ONCE here, outside every layout block, because the bar is\n position: fixed and so serves top, top-modern, side and side-modern from this single instance.\n smallScreen is nav-menu's existing (max-width: 600px) BreakpointObserver \u2014 the one source of truth\n for this feature, so no second breakpoint is introduced. Never renders on desktop. -->\n<spa-bottom-tabs *ngIf=\"loggedin && smallScreen\"></spa-bottom-tabs>\n", styles: ["a.navbar-brand{white-space:normal;text-align:center;word-break:break-all}html{font-size:14px}.box-shadow{box-shadow:0 .25rem .75rem #0000000d}.toolbar-item-spacer{flex:1 1 auto}.toolbar{height:60px;display:flex;align-items:center;background-color:#03a;color:#fff;margin-bottom:0!important}.toolbar button,.toolbar .mat-mdc-button,.toolbar .mat-mdc-icon-button{color:#fff!important}.toolbar mat-icon{color:#fff!important}.stack-top{z-index:9;margin:20px}.navitems{background-color:#03a}.app-container{height:90%;margin:0}.app-sidenav{width:200px;border:1px solid rgb(192,190,199)}.side-color{background-color:#e6f4ff}.app-sidenav mat-list-item{display:flex!important;align-items:center!important}.app-sidenav mat-icon{display:inline-flex!important;align-items:center!important;vertical-align:middle!important}.app-sidenav mat-expansion-panel-header mat-icon{display:inline-flex!important;align-items:center!important;vertical-align:middle!important}::ng-deep .app-sidenav .mat-expansion-panel-body{padding-bottom:5px!important;padding-right:5px!important}::ng-deep .app-sidenav .mdc-list{padding-bottom:0!important}.sm-layout{display:flex;min-height:100vh;position:relative}.sm-sidebar{position:fixed;top:0;left:0;bottom:0;width:260px;z-index:1030;overflow:hidden;transition:width .3s cubic-bezier(.4,0,.2,1)}.sm-sidebar-bg{position:absolute;inset:0;z-index:0}.sm-sidebar-bg-image{position:absolute;inset:0;background-size:cover;background-position:center}.sm-sidebar-bg-overlay{position:absolute;inset:0}.sm-sidebar-content{position:relative;z-index:1;display:flex;flex-direction:column;height:100%;color:#fff}.sm-brand{display:flex;align-items:center;padding:18px 15px 10px;min-height:60px;text-decoration:none;white-space:nowrap;overflow:hidden}.sm-brand img{height:34px;width:34px;object-fit:contain;margin-right:12px;flex-shrink:0}.sm-brand-name{font-size:16px;font-weight:500;letter-spacing:.5px;color:#fff;overflow:hidden;text-overflow:ellipsis;transition:opacity .2s ease}.sm-profile{display:flex;align-items:center;padding:12px 15px;white-space:nowrap;overflow:hidden}.sm-profile-icon{font-size:34px!important;width:34px!important;height:34px!important;margin-right:12px;flex-shrink:0;color:#fffc}.sm-profile-info{overflow:hidden;transition:opacity .2s ease}.sm-profile-name{font-size:14px;font-weight:500;color:#fff;line-height:1.3;overflow:hidden;text-overflow:ellipsis}.sm-profile-role{font-size:11px;color:#fff9;line-height:1.3;overflow:hidden;text-overflow:ellipsis}.sm-sidebar mat-divider{border-color:#ffffff26!important;margin:0 15px}.sm-menu-scroll{flex:1;overflow-y:auto;overflow-x:hidden;padding:8px 0}.sm-menu-scroll::-webkit-scrollbar{width:4px}.sm-menu-scroll::-webkit-scrollbar-track{background:transparent}.sm-menu-scroll::-webkit-scrollbar-thumb{background:#fff3;border-radius:2px}.sm-menu-item{display:flex;align-items:center;padding:10px 15px;margin:2px 15px;border-radius:4px;cursor:pointer;color:#fff;font-size:13px;font-weight:400;letter-spacing:.3px;transition:all .15s ease;text-decoration:none;white-space:nowrap;overflow:hidden}.sm-menu-item:hover{background:#ffffff1f}.sm-menu-item.sm-active{background-color:#fff;color:#3c4858;box-shadow:0 4px 20px #00000024,0 7px 10px -5px #0003;font-weight:500}.sm-menu-item.sm-active .sm-menu-icon{color:#3c4858}.sm-menu-icon{font-size:20px!important;width:24px!important;height:24px!important;display:inline-flex!important;align-items:center;justify-content:center;margin-right:12px;flex-shrink:0;color:#fffc;transition:color .15s ease}.sm-menu-text{flex:1;overflow:hidden;text-overflow:ellipsis;transition:opacity .2s ease}.sm-caret{font-size:18px!important;width:18px!important;height:18px!important;transition:transform .3s cubic-bezier(.4,0,.2,1);flex-shrink:0;color:#fff9}.sm-caret.sm-caret-open{transform:rotate(180deg)}.sm-active .sm-caret{color:#3c4858}.sm-submenu{max-height:0;overflow:hidden;transition:max-height .35s cubic-bezier(.4,0,.2,1)}.sm-submenu.sm-submenu-open{max-height:1000px}.sm-submenu-item{display:flex;align-items:center;padding:8px 15px 8px 30px;margin:1px 15px;border-radius:4px;cursor:pointer;color:#fffc;font-size:12px;font-weight:400;transition:all .15s ease;white-space:nowrap;overflow:hidden}.sm-submenu-item:hover{background:#ffffff1f;color:#fff}.sm-submenu-item.sm-active{background-color:#fff;color:#3c4858;box-shadow:0 4px 20px #00000024,0 7px 10px -5px #0003;font-weight:500}.sm-submenu-item.sm-active .sm-sub-icon{color:#3c4858}.sm-sub-icon{font-size:16px!important;width:20px!important;height:20px!important;display:inline-flex!important;align-items:center;justify-content:center;margin-right:10px;flex-shrink:0;color:#fff9}.sm-initials{width:20px;height:20px;border-radius:50%;background:#ffffff26;display:inline-flex;align-items:center;justify-content:center;font-size:9px;font-weight:600;margin-right:10px;flex-shrink:0;color:#fffc}.sm-active .sm-initials{background:#3c48581f;color:#3c4858}.sm-main{flex:1;min-width:0;margin-left:260px;min-height:100vh;display:flex;flex-direction:column;transition:margin-left .3s cubic-bezier(.4,0,.2,1);background-color:#eef2f7}.sm-topbar{display:flex;align-items:center;padding:8px 16px;min-height:56px;background-color:#eef2f7;background-image:radial-gradient(circle,#d5dbe3 1px,transparent 1px);background-size:16px 16px;border-bottom:1px solid rgba(0,0,0,.08);position:sticky;top:0;z-index:1020;transition:background .3s ease,backdrop-filter .3s ease}.sm-topbar-scrolled{background-color:#eef2f78c;background-image:none;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);box-shadow:0 1px 3px #0000000f}.sm-topbar-spacer{flex:1 1 auto}.sm-topbar-logo{height:32px;width:32px;object-fit:contain;margin-right:8px}.sm-topbar-brand{font-size:18px;font-weight:500;margin-right:8px;white-space:nowrap}.sm-topbar-label{font-size:14px;margin-right:4px;display:inline-flex;align-items:center;align-self:center;height:40px;line-height:1}.sm-topbar .mat-mdc-icon-button{display:inline-flex!important;align-items:center!important;justify-content:center!important}.sm-content{flex:1;padding:12px;min-width:0;background-color:#e5eaf2;background-image:radial-gradient(ellipse at 50% 45%,#fffffff2,#fff6 35%,#fff0 60%),radial-gradient(circle,#bec7d4 1px,transparent 1px);background-size:100% 100%,16px 16px;min-height:calc(100vh - 104px)}.sm-footer{padding:12px 16px;text-align:center;font-size:12px;color:#999;border-top:1px solid #e0e0e0;background:#fff}.sm-footer a{color:inherit;cursor:pointer}.sm-footer a:hover{text-decoration:underline}.sm-backdrop{display:none;position:fixed;inset:0;background:#00000080;z-index:1025}.sm-layout.sm-mini .sm-sidebar{width:80px}.sm-layout.sm-mini .sm-main{margin-left:80px}.sm-layout.sm-mini .sm-brand-name,.sm-layout.sm-mini .sm-profile-info,.sm-layout.sm-mini .sm-menu-text,.sm-layout.sm-mini .sm-caret,.sm-layout.sm-mini .sm-submenu{display:none}.sm-layout.sm-mini .sm-sidebar mat-divider{margin:0 10px}.sm-layout.sm-mini .sm-brand{justify-content:center;padding:18px 0 10px}.sm-layout.sm-mini .sm-brand img{margin-right:0}.sm-layout.sm-mini .sm-profile{justify-content:center;padding:12px 0}.sm-layout.sm-mini .sm-profile-icon{margin-right:0}.sm-layout.sm-mini .sm-menu-item{justify-content:center;padding:12px 0;margin:2px 0}.sm-layout.sm-mini .sm-menu-icon{margin-right:0;font-size:22px!important}.sm-layout.sm-mini-hovered .sm-sidebar{width:260px;box-shadow:4px 0 20px #0000004d}.sm-layout.sm-mini-hovered .sm-main{margin-left:80px}.sm-layout.sm-mini-hovered .sm-brand-name,.sm-layout.sm-mini-hovered .sm-profile-info,.sm-layout.sm-mini-hovered .sm-menu-text,.sm-layout.sm-mini-hovered .sm-caret{display:initial}.sm-layout.sm-mini-hovered .sm-submenu{display:block}.sm-layout.sm-mini-hovered .sm-sidebar mat-divider{margin:0 15px}.sm-layout.sm-mini-hovered .sm-brand{justify-content:flex-start;padding:18px 15px 10px}.sm-layout.sm-mini-hovered .sm-brand img{margin-right:12px}.sm-layout.sm-mini-hovered .sm-profile{justify-content:flex-start;padding:12px 15px}.sm-layout.sm-mini-hovered .sm-profile-icon{margin-right:12px}.sm-layout.sm-mini-hovered .sm-menu-item{justify-content:flex-start;padding:10px 15px;margin:2px 15px}.sm-layout.sm-mini-hovered .sm-menu-icon{margin-right:12px;font-size:20px!important}@media (max-width: 600px){.sm-sidebar{transform:translate(-100%);transition:transform .3s cubic-bezier(.4,0,.2,1);width:260px!important}.sm-layout.sm-mobile-open .sm-sidebar{transform:translate(0)}.sm-layout.sm-mobile-open .sm-backdrop{display:block}.sm-main{margin-left:0!important}.sm-layout.sm-mini .sm-sidebar{width:260px!important}.sm-layout.sm-mini .sm-brand-name,.sm-layout.sm-mini .sm-profile-info,.sm-layout.sm-mini .sm-menu-text,.sm-layout.sm-mini .sm-caret{display:initial}.sm-layout.sm-mini .sm-submenu{display:block}.sm-layout.sm-mini .sm-sidebar mat-divider{margin:0 15px}.sm-layout.sm-mini .sm-menu-item{justify-content:flex-start;padding:10px 15px;margin:2px 15px}.sm-layout.sm-mini .sm-menu-icon{margin-right:12px;font-size:20px!important}.sm-layout.sm-mini .sm-brand{justify-content:flex-start;padding:18px 15px 10px}.sm-layout.sm-mini .sm-brand img{margin-right:12px}.sm-layout.sm-mini .sm-profile{justify-content:flex-start;padding:12px 15px}.sm-layout.sm-mini .sm-profile-icon{margin-right:12px}}.tm-navbar{position:sticky;top:0;z-index:1030;background-color:#03a;color:#fff;box-shadow:0 2px 12px #0000001f;transition:background-color .3s ease,backdrop-filter .3s ease,box-shadow .3s ease}.tm-navbar.tm-scrolled{background-color:#0033aad9;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);box-shadow:0 4px 18px #0000002e}.tm-bar{display:flex;align-items:center;flex-wrap:nowrap;min-height:60px;padding:6px 16px;gap:4px}.tm-brand{display:flex;align-items:center;gap:12px;flex-shrink:0;margin-right:18px;cursor:pointer;-webkit-user-select:none;user-select:none}.tm-logo{height:40px;width:auto;object-fit:contain}.tm-app-name{font-size:20px;font-weight:500;line-height:1.2;white-space:nowrap}.tm-tenant{font-size:12px;font-weight:400;color:#ffffffb3;line-height:1.2}.tm-toggler{display:none;margin-left:auto;background:transparent;border:1px solid rgba(255,255,255,.4);border-radius:8px;color:#fff;cursor:pointer;align-items:center;justify-content:center;width:40px;height:40px}.tm-toggler mat-icon{color:#fff}.tm-menu{display:flex;align-items:center;flex-wrap:wrap;flex:1 1 auto;min-width:0;row-gap:4px;justify-content:flex-end}.tm-item-wrap{display:flex;align-items:center;position:relative}.tm-item-wrap:not(:first-child):before{content:\"\";width:1px;height:18px;background:#ffffff2e;margin:0 2px;flex-shrink:0}.tm-item{position:relative;display:inline-flex;align-items:center;gap:6px;height:40px;padding:0 14px;margin:0 2px;background:transparent;border:none;border-radius:8px;color:#ffffffeb;font-size:14px;font-weight:400;letter-spacing:.2px;white-space:nowrap;cursor:pointer;transition:background .18s ease,color .18s ease}.tm-item:after{content:\"\";position:absolute;left:12px;right:12px;bottom:5px;height:1px;border-radius:1px;background:#ffffff80;transform:scaleX(0);transform-origin:center;transition:transform .25s cubic-bezier(.4,0,.2,1)}.tm-item:hover{color:#fff}.tm-item:hover:after{transform:scaleX(1)}.tm-item.tm-item-active:after{transform:scaleX(1)}.tm-item-icon{font-size:19px!important;width:19px!important;height:19px!important;display:inline-flex!important;align-items:center;justify-content:center;color:inherit!important}.tm-item-caret{font-size:18px!important;width:18px!important;height:18px!important;display:inline-flex!important;align-items:center;justify-content:center;margin-left:-2px;margin-right:-4px;color:#ffffffb3!important;transition:transform .2s ease}.tm-item:hover .tm-item-caret,.tm-item-active .tm-item-caret{color:#fff!important}.tm-actions{display:flex;align-items:center;gap:2px;flex-shrink:0;margin-left:auto}.tm-actions .mat-mdc-icon-button,.tm-action-btn{display:inline-flex!important;align-items:center!important;justify-content:center!important;color:#fff!important}.tm-actions mat-icon{color:#fff!important}.tm-divider-v{width:1px;height:24px;background:#ffffff38;margin:0 6px;flex-shrink:0}.tm-user-btn{display:inline-flex!important;align-items:center!important;gap:6px;height:40px;color:#fff!important;border-radius:8px;transition:background .18s ease}.tm-user-btn:hover{background:#ffffff1f}.tm-user-icon{font-size:24px!important;width:24px!important;height:24px!important;color:#fff!important}.tm-user-name{font-size:14px;font-weight:400;white-space:nowrap}::ng-deep .tm-submenu-panel .tm-sub-active{background:#0033aa14;font-weight:600;color:#03a}::ng-deep .tm-submenu-panel .tm-sub-active .mat-icon{color:#03a}@media (max-width: 991px){.tm-toggler{display:inline-flex}.tm-menu,.tm-actions{display:none;position:absolute;left:0;right:0;top:100%;flex-direction:column;align-items:stretch;background:#03a;padding:8px 12px;box-shadow:0 8px 18px #0003;z-index:1029}.tm-menu.tm-menu-open{display:flex}.tm-actions.tm-actions-open{display:flex;top:100%;border-top:1px solid rgba(255,255,255,.12)}.tm-item-wrap{width:100%}.tm-item-wrap:not(:first-child):before{width:100%;height:1px;margin:2px 0}.tm-item{width:100%;justify-content:flex-start;height:44px;margin:0}.tm-item:after{inset:8px auto 8px 0;width:4px;height:auto;transform:scaleY(0);transform-origin:center}.tm-item:hover:after,.tm-item.tm-item-active:after{transform:scaleY(1)}.tm-item-caret{margin-left:auto}.tm-divider-v{display:none}.tm-user-btn{justify-content:flex-start;width:100%}}@media (max-width: 600px){.has-bottom-tabs{padding-bottom:calc(64px + env(safe-area-inset-bottom,0px))!important}.sm-footer{display:none}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i4$4.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i4$4.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i4$4.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "directive", type: i4$5.MatBadge, selector: "[matBadge]", inputs: ["matBadgeColor", "matBadgeOverlap", "matBadgeDisabled", "matBadgePosition", "matBadge", "matBadgeDescription", "matBadgeSize", "matBadgeHidden"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i17.MatNavList, selector: "mat-nav-list", exportAs: ["matNavList"] }, { kind: "component", type: i17.MatListItem, selector: "mat-list-item, a[mat-list-item], button[mat-list-item]", inputs: ["activated"], exportAs: ["matListItem"] }, { kind: "component", type: i17.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: i19$1.MatSidenav, selector: "mat-sidenav", inputs: ["fixedInViewport", "fixedTopGap", "fixedBottomGap"], exportAs: ["matSidenav"] }, { kind: "component", type: i19$1.MatSidenavContainer, selector: "mat-sidenav-container", exportAs: ["matSidenavContainer"] }, { kind: "component", type: i19$1.MatSidenavContent, selector: "mat-sidenav-content" }, { kind: "component", type: i20.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { kind: "directive", type: i1$1.RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "directive", type: i1$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: i21.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i21.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "component", type: LoaderComponent, selector: "spa-loader", inputs: ["logo"] }, { kind: "component", type: ToastComponent, selector: "spa-toast" }, { kind: "component", type: AgentComponent, selector: "spa-agent", inputs: ["pageMode"] }, { kind: "component", type: BottomTabsComponent, selector: "spa-bottom-tabs" }, { kind: "component", type: OfflineIndicatorComponent, selector: "spa-offline-indicator" }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }] }); }
20853
20940
  }
20854
20941
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: NavMenuComponent, decorators: [{
20855
20942
  type: Component,
@@ -21233,7 +21320,7 @@ class TabsComponent {
21233
21320
  return this.reload || this.tableReloads[index];
21234
21321
  }
21235
21322
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TabsComponent, deps: [{ token: TabService }], target: i0.ɵɵFactoryTarget.Component }); }
21236
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TabsComponent, isStandalone: false, selector: "spa-tabs", inputs: { tableConfigs: "tableConfigs", reload: "reload", reloadTab: "reloadTab", parentDetails: "parentDetails", localMode: "localMode", nestingLevel: "nestingLevel" }, outputs: { formRefresh: "formRefresh", actionSuccess: "actionSuccess" }, usesOnChanges: true, ngImport: i0, template: "<mat-tab-group (selectedTabChange)=\"onTabChange($event)\" [selectedIndex]=\"selectedTabIndex\">\n\n <!-- Changed: Use cached visibleTabs property to prevent infinite change detection loop -->\n <ng-container *ngFor=\"let tab of visibleTabs; let i = index\">\n <mat-tab><!-- TS-11: visibleTabs is already filtered via getVisibleTabs; dropped per-tab isTabVisible() call per CD -->\n\n <!-- Tab label with count pill.\n Changed: was matBadge, which renders as a faint superscript unless the consuming app includes\n Material's badge theme \u2014 several do not, so the number was effectively invisible. This pill is\n styled entirely in this component, so every app gets the same legible count.\n An empty tab still shows its count, muted rather than accented: saying \"nothing in here\" before\n the tab is opened is the whole point, and it saves the round trip of opening it. -->\n <ng-template matTabLabel>\n <span class=\"tab-label\">\n {{getTabTitle(tab.config)}}\n <span *ngIf=\"shouldShowBadge(tab.originalIndex)\"\n class=\"tab-count\"\n [class.tab-count-zero]=\"getTabCount(tab.originalIndex) === 0\">{{getTabCount(tab.originalIndex)}}</span>\n </span>\n </ng-template>\n\n <!-- Tab content: custom template when provided (tabTemplate), otherwise the lazy-loaded table.\n Templates keep the same lazy semantics \u2014 rendered only once the tab has been activated. -->\n <div *ngIf=\"shouldLoadTabData(i) && tab.config.tabTemplate\" class=\"tab-content\">\n <ng-container [ngTemplateOutlet]=\"tab.config.tabTemplate\"></ng-container>\n </div>\n\n <div *ngIf=\"shouldLoadTabData(i) && !tab.config.tabTemplate\" class=\"tab-content\">\n <spa-table\n [config]=\"tab.config\"\n [reload]=\"getReloadSubject(tab.originalIndex)\"\n [inTab]=\"true\"\n [activeTab]=\"selectedTabIndex === i\"\n [nestingLevel]=\"nestingLevel\"\n [localMode]=\"localMode\"\n [parentDetails]=\"parentDetails\"\n (totalChange)=\"onTabTotal(tab.originalIndex, $event)\"\n (actionSuccess)=\"onTableActionSuccess(tab.originalIndex, $event)\">\n </spa-table><!-- Changed: badge adopts the grid's filtered total so it can never show a global/unfiltered count --><!-- Changed: localMode + parentDetails passed through for one-step create local tables -->\n </div>\n\n <!-- Placeholder for non-loaded tabs -->\n <div *ngIf=\"!shouldLoadTabData(i)\" class=\"tab-placeholder\">\n <!-- Empty placeholder - content will load when tab is activated -->\n </div>\n\n </mat-tab>\n </ng-container>\n\n</mat-tab-group>", styles: [":host{flex:1;display:flex;flex-direction:column}:host ::ng-deep .mat-mdc-tab-group{flex:1;display:flex;flex-direction:column}:host ::ng-deep .mat-mdc-tab-body-wrapper{flex:1}.tab-content{padding-top:16px;overflow-x:auto}.tab-placeholder{min-height:100px;display:flex;align-items:center;justify-content:center}.tab-label{display:inline-flex;align-items:center;gap:8px}.tab-count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:18px;padding:0 6px;border-radius:9px;background-color:#1976d2;color:#fff;font-size:11px;font-weight:600;line-height:1}.tab-count-zero{background-color:#d5d9de;color:#6b7280}.badge{background-color:#2196f3;color:#fff;border-radius:12px;padding:2px 8px;margin-left:8px;font-size:12px}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i5$3.MatTabLabel, selector: "[mat-tab-label], [matTabLabel]" }, { kind: "component", type: i5$3.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i5$3.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }] }); }
21323
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TabsComponent, isStandalone: false, selector: "spa-tabs", inputs: { tableConfigs: "tableConfigs", reload: "reload", reloadTab: "reloadTab", parentDetails: "parentDetails", localMode: "localMode", nestingLevel: "nestingLevel" }, outputs: { formRefresh: "formRefresh", actionSuccess: "actionSuccess" }, usesOnChanges: true, ngImport: i0, template: "<mat-tab-group (selectedTabChange)=\"onTabChange($event)\" [selectedIndex]=\"selectedTabIndex\">\n\n <!-- Changed: Use cached visibleTabs property to prevent infinite change detection loop -->\n <ng-container *ngFor=\"let tab of visibleTabs; let i = index\">\n <mat-tab><!-- TS-11: visibleTabs is already filtered via getVisibleTabs; dropped per-tab isTabVisible() call per CD -->\n\n <!-- Tab label with count pill.\n Changed: was matBadge, which renders as a faint superscript unless the consuming app includes\n Material's badge theme \u2014 several do not, so the number was effectively invisible. This pill is\n styled entirely in this component, so every app gets the same legible count.\n An empty tab still shows its count, muted rather than accented: saying \"nothing in here\" before\n the tab is opened is the whole point, and it saves the round trip of opening it. -->\n <ng-template matTabLabel>\n <span class=\"tab-label\">\n {{getTabTitle(tab.config)}}\n <span *ngIf=\"shouldShowBadge(tab.originalIndex)\"\n class=\"tab-count\"\n [class.tab-count-zero]=\"getTabCount(tab.originalIndex) === 0\">{{getTabCount(tab.originalIndex)}}</span>\n </span>\n </ng-template>\n\n <!-- Tab content: custom template when provided (tabTemplate), otherwise the lazy-loaded table.\n Templates keep the same lazy semantics \u2014 rendered only once the tab has been activated. -->\n <div *ngIf=\"shouldLoadTabData(i) && tab.config.tabTemplate\" class=\"tab-content\">\n <ng-container [ngTemplateOutlet]=\"tab.config.tabTemplate\"></ng-container>\n </div>\n\n <div *ngIf=\"shouldLoadTabData(i) && !tab.config.tabTemplate\" class=\"tab-content\">\n <spa-table\n [config]=\"tab.config\"\n [reload]=\"getReloadSubject(tab.originalIndex)\"\n [inTab]=\"true\"\n [activeTab]=\"selectedTabIndex === i\"\n [nestingLevel]=\"nestingLevel\"\n [localMode]=\"localMode\"\n [parentDetails]=\"parentDetails\"\n (totalChange)=\"onTabTotal(tab.originalIndex, $event)\"\n (actionSuccess)=\"onTableActionSuccess(tab.originalIndex, $event)\">\n </spa-table><!-- Changed: badge adopts the grid's filtered total so it can never show a global/unfiltered count --><!-- Changed: localMode + parentDetails passed through for one-step create local tables -->\n </div>\n\n <!-- Placeholder for non-loaded tabs -->\n <div *ngIf=\"!shouldLoadTabData(i)\" class=\"tab-placeholder\">\n <!-- Empty placeholder - content will load when tab is activated -->\n </div>\n\n </mat-tab>\n </ng-container>\n\n</mat-tab-group>", styles: [":host{flex:1;display:flex;flex-direction:column}:host ::ng-deep .mat-mdc-tab-group{flex:1;display:flex;flex-direction:column}:host ::ng-deep .mat-mdc-tab-body-wrapper{flex:1}.tab-content{padding-top:16px;overflow-x:auto}.tab-placeholder{min-height:100px;display:flex;align-items:center;justify-content:center}.tab-label{display:inline-flex;align-items:center;gap:8px}.tab-count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:18px;padding:0 6px;border-radius:9px;background-color:#1976d2;color:#fff;font-size:11px;font-weight:600;line-height:1}.tab-count-zero{background-color:#d5d9de;color:#6b7280}.badge{background-color:#2196f3;color:#fff;border-radius:12px;padding:2px 8px;margin-left:8px;font-size:12px}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i5$4.MatTabLabel, selector: "[mat-tab-label], [matTabLabel]" }, { kind: "component", type: i5$4.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i5$4.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }] }); }
21237
21324
  }
21238
21325
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TabsComponent, decorators: [{
21239
21326
  type: Component,
@@ -21992,11 +22079,11 @@ class DetailsDialog {
21992
22079
  }
21993
22080
  }
21994
22081
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DetailsDialog, deps: [{ token: i1$4.BreakpointObserver }, { token: LoaderService }, { token: DataServiceLib }, { token: MessageService }, { token: i4.MatDialogRef }, { token: MAT_DIALOG_DATA }, { token: ButtonService }, { token: DialogService }, { token: AuthService }, { token: TableConfigService }, { token: i0.NgZone }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
21995
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: DetailsDialog, isStandalone: false, selector: "spa-detailsDialog", outputs: { inputChange: "inputChange" }, viewQueries: [{ propertyName: "formComponent", first: true, predicate: FormComponent, descendants: true }], ngImport: i0, template: "<!-- Changed: dialog-has-tables mirrors the service's hasTables (tableConfigs?.length)\n which decides height '90%' vs 'auto' \u2014 used to scope scroll behaviour per case -->\n<div class=\"dialog-container\" [class.dialog-has-tables]=\"detailsConfig.tableConfigs?.length > 0\">\n\n <div class=\"dialog-content\">\n\n <!-- Changed: was <mat-progress-bar mode=\"indeterminate\">, which looped at a constant speed forever and so\n only ever said \"busy\". This is the same markup and the same PerceivedProgress curve the table stage\n uses, so a dialog and the table behind it move identically. It tracks the FORM's record load \u2014 nested\n tab tables run their own stages. Controlled by appConfig.quietLoading. -->\n <div *ngIf=\"showProgressLine\" class=\"tin-load-line\" aria-hidden=\"true\">\n <span class=\"tin-load-line-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n\n <!-- Changed: the header's padding and the title's type were inline styles, which always beat a class and so\n made a second presentation impossible. Moved into dlg-header-classic / dlg-title-classic, which restate\n those exact values, so the default look is unchanged while `modern` can swap in the banded header. -->\n <div class=\"d-flex justify-content-between align-items-center dialog-header\"\n [class.mt-2]=\"!modern\" [class.dlg-header-classic]=\"!modern\" [class.tin-dlg-head]=\"modern\">\n\n <div class=\"dialog-header-titles\">\n <label [class.dlg-title-classic]=\"!modern\" [class.tin-dlg-title]=\"modern\">{{titleAction | titlecase}} {{formConfig?.title}}</label>\n </div>\n\n <div class=\"d-flex align-items-center\" style=\"gap: 8px;\">\n\n <!-- Changed: Auto Refresh icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.autoRefreshConfig\" mat-icon-button\n [matTooltip]=\"autoRefreshEnabled ? 'Click to disable auto refresh' : 'Click to enable auto refresh'\" matTooltipPosition=\"above\"\n [style.color]=\"autoRefreshEnabled ? '#4caf50' : '#9e9e9e'\"\n (click)=\"autoRefreshEnabled = !autoRefreshEnabled; toggleAutoRefresh()\">\n <mat-icon>autorenew</mat-icon>\n </button>\n <!-- Changed: Keep Open icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.allowUserKeepOpen\" mat-icon-button\n [matTooltip]=\"userKeepOpen ? 'Click to disable keep open' : 'Click to enable keep open'\" matTooltipPosition=\"above\"\n [style.color]=\"userKeepOpen ? '#4caf50' : '#9e9e9e'\"\n (click)=\"userKeepOpen = !userKeepOpen\">\n <mat-icon>push_pin</mat-icon>\n </button>\n\n <!-- TS-16: bind precomputed editButtonVM instead of testVisible/testDisabled (which cloned objects) per CD -->\n <div *ngIf=\"formConfig.mode=='view' && editButton && editButtonVM.visible\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Edit\" color=\"primary\" (click)=\"setMode('edit')\" [disabled]=\"editButtonVM.disabled\"><mat-icon>edit</mat-icon></button>\n </div>\n\n <!-- Changed (Quiet Loading): every [disabled] below now ORs in entityLoading. A quiet read is silent, so it\n never flips LoaderService.isLoading and isProcessing alone would leave these buttons live during the\n load \u2014 a user could submit an empty form. With the flag off entityLoading simply tracks the same\n window isProcessing already covered, so nothing changes. -->\n <!-- Changed: the icon now spins while the form's record reloads, matching the table's refresh button.\n Same .tin-spin class, so the two surfaces use one idiom rather than each inventing its own. -->\n <button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"loadByAction\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Refresh\" color=\"primary\" (click)=\"loadData(formConfig.loadAction, detailsConfig.causeTableRefresh)\"><mat-icon class=\"refreshIcon\" [class.tin-spin]=\"entityLoading\">cached</mat-icon></button>\n \n <!-- Added: Top close button when position is 'top' -->\n <button *ngIf=\"shouldShowTopClose()\" [disabled]=\"isProcessing || entityLoading\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Close\" (click)=\"close()\"><mat-icon>close</mat-icon></button>\n\n </div>\n\n </div>\n\n <div style=\"padding-left: 24px; padding-right: 24px;\">\n <spa-steps *ngIf=\"stepConfig && details && stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n </div>\n\n <mat-dialog-content class=\"mat-typography dialog-scroll-content\">\n\n <spa-steps *ngIf=\"stepConfig && details && !stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && !statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && !formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n\n <div class=\"tin-input\" style=\"font-size:14px\">\n\n <!-- Changed (Quiet Loading, FSD D6): with the flag OFF this is the original bare \"Loading...\" line, unchanged.\n With it ON the text is replaced by a skeleton of the real form \u2014 and only once showSkeleton has been\n armed past the 150ms anti-flicker delay, so a cached record renders straight into the form with no flash. -->\n <p *ngIf=\"formConfig && !details && !effQuietLoading\"><em>Loading...</em></p>\n\n <spa-form-skeleton *ngIf=\"formConfig && !details && effQuietLoading && showSkeleton\" [config]=\"formConfig\"></spa-form-skeleton>\n\n <spa-form *ngIf=\"formConfig && details\" [files]=\"files\" [data]=\"details\" [config]=\"formConfig\" (inputChange)=\"inputChanged($event)\">\n <ng-template #dynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\" let-resolveLoadAction=\"resolveLoadAction\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"resolveLoadAction ? resolveLoadAction(field) : field.loadAction\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n </spa-form>\n\n <!-- Changed: Use unified spa-tabs with nestingLevel control \u2014 tabs hidden when nestingLevel >= 2 -->\n <spa-tabs\n *ngIf=\"showTabs && tableConfigs && !(detailsConfig.hideTablesInCreateMode && formConfig?.mode === 'create')\"\n [tableConfigs]=\"tableConfigs\"\n [reload]=\"tableReload\"\n [parentDetails]=\"details\"\n [localMode]=\"formConfig?.mode === 'create'\"\n [nestingLevel]=\"nestingLevel + 1\"\n (formRefresh)=\"loadData(formConfig.loadAction, false)\"\n (actionSuccess)=\"actionPerformed = true\">\n </spa-tabs><!-- Changed: nested-tab table actions flag actionPerformed so refreshOnCloseIfActioned refreshes the parent on close -->\n\n </div>\n\n </mat-dialog-content>\n\n\n </div>\n\n <mat-dialog-actions >\n\n <div>\n\n <button mat-raised-button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"formConfig.mode=='create' && createButton\" color=\"primary\"\n (click)=\"create()\" cdkFocusInitial>{{createButton.display ?? 'Submit'}}\n </button>\n\n <button mat-raised-button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"formConfig.mode=='edit' && editButton\" color=\"primary\"\n (click)=\"edit()\" cdkFocusInitial>{{editButton.display ?? 'Submit'}}\n </button>\n\n <!-- TS-16: bind precomputed extraButtonVMs instead of testVisible/testDisabled/getButtonColor (each cloning objects) per CD -->\n <ng-container *ngFor=\"let vm of extraButtonVMs\">\n <button *ngIf=\"formConfig.mode !== 'create' && vm.visible\" mat-stroked-button [disabled]=\"isProcessing || entityLoading || vm.disabled\" [ngStyle]=\"{'color': vm.color}\" (click)=\"custom(vm.button)\" cdkFocusInitial>\n <mat-icon *ngIf=\"vm.button.icon\" [ngStyle]=\"{'color': vm.color}\">{{vm.button.icon.name}}</mat-icon>\n {{vm.button.display ?? vm.button.name | titlecase}}\n </button>\n </ng-container>\n\n <!-- Changed: Bottom close button now uses conditional display and custom text -->\n <button *ngIf=\"shouldShowBottomClose()\" mat-stroked-button color=\"primary\" (click)=\"close()\">{{getCloseText()}}</button>\n\n </div>\n\n <div class=\"col d-flex justify-content-end\" *ngIf=\"smallScreen\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Delete\" [disabled]=\"isProcessing || entityLoading\" style=\"color: red;\" (click)=\"delete()\" *ngIf=\"formConfig.mode!='create' && deleteButton\"><mat-icon>delete</mat-icon></button>\n </div>\n\n\n </mat-dialog-actions>\n\n\n</div>\n\n\n\n\n\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}.dlg-header-classic{padding-left:24px;padding-right:24px}.dlg-title-classic{font-size:20px;font-weight:500;margin-top:10px;margin-bottom:5px}.dialog-header-titles{display:flex;flex-direction:column;justify-content:center;min-width:0}.tin-dlg-head .dialog-header-titles label{margin:0}.tbl-section-header{display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:8px;border:1px solid rgba(0,0,0,.08);border-radius:10px;background:transparent;cursor:pointer;transition:border-color .15s}.tbl-section-header:hover{border-color:#90a4ae}.tbl-section-header.tbl-section-static{cursor:default}.tbl-section-icon{color:#546e7a}.tbl-section-title{font-size:14px;font-weight:600;color:#000000d1}.tbl-section-count{background:#e3f2fd;color:#1565c0;border-radius:12px;padding:2px 10px;font-size:12px}.tbl-section-chip{font-size:12px;font-weight:500;color:#0009}.tbl-section-spacer{margin-left:auto}.tbl-section-chevron{color:#90a4ae}.tbl-section-more{padding:0 12px 8px}.tbl-section-link{background:none;border:none;color:#1565c0;cursor:pointer;font-size:13px;padding:0}.tbl-section-link:hover{text-decoration:underline}table.tin-no-col-headers tr.mat-mdc-header-row{display:none}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i4.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i4.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: StepsComponent, selector: "spa-steps", inputs: ["value", "config", "data", "activeIndex"] }, { kind: "component", type: FormComponent, selector: "spa-form", inputs: ["files", "data", "config"], outputs: ["buttonClick", "inputChange"] }, { kind: "component", type: FormSkeletonComponent, selector: "spa-form-skeleton", inputs: ["config"] }, { kind: "component", type: AlertComponent, selector: "spa-alert", inputs: ["config", "data"] }, { kind: "component", type: TabsComponent, selector: "spa-tabs", inputs: ["tableConfigs", "reload", "reloadTab", "parentDetails", "localMode", "nestingLevel"], outputs: ["formRefresh", "actionSuccess"] }, { kind: "component", type: StatusesComponent, selector: "spa-statuses", inputs: ["config", "data"] }, { kind: "pipe", type: i1$2.TitleCasePipe, name: "titlecase" }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
22082
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: DetailsDialog, isStandalone: false, selector: "spa-detailsDialog", outputs: { inputChange: "inputChange" }, viewQueries: [{ propertyName: "formComponent", first: true, predicate: FormComponent, descendants: true }], ngImport: i0, template: "<!-- Changed: dialog-has-tables mirrors the service's hasTables (tableConfigs?.length)\n which decides height '90%' vs 'auto' \u2014 used to scope scroll behaviour per case -->\n<div class=\"dialog-container\" [class.dialog-has-tables]=\"detailsConfig.tableConfigs?.length > 0\">\n\n <div class=\"dialog-content\">\n\n <!-- Changed: was <mat-progress-bar mode=\"indeterminate\">, which looped at a constant speed forever and so\n only ever said \"busy\". This is the same markup and the same PerceivedProgress curve the table stage\n uses, so a dialog and the table behind it move identically. It tracks the FORM's record load \u2014 nested\n tab tables run their own stages. Controlled by appConfig.quietLoading. -->\n <div *ngIf=\"showProgressLine\" class=\"tin-load-line\" aria-hidden=\"true\">\n <span class=\"tin-load-line-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n\n <!-- Changed: the header's padding and the title's type were inline styles, which always beat a class and so\n made a second presentation impossible. Moved into dlg-header-classic / dlg-title-classic, which restate\n those exact values, so the default look is unchanged while `modern` can swap in the banded header. -->\n <div class=\"d-flex justify-content-between align-items-center dialog-header\"\n [class.mt-2]=\"!modern\" [class.dlg-header-classic]=\"!modern\" [class.tin-dlg-head]=\"modern\">\n\n <div class=\"dialog-header-titles\">\n <label [class.dlg-title-classic]=\"!modern\" [class.tin-dlg-title]=\"modern\">{{titleAction | titlecase}} {{formConfig?.title}}</label>\n </div>\n\n <div class=\"d-flex align-items-center\" style=\"gap: 8px;\">\n\n <!-- Changed: Auto Refresh icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.autoRefreshConfig\" mat-icon-button\n [matTooltip]=\"autoRefreshEnabled ? 'Click to disable auto refresh' : 'Click to enable auto refresh'\" matTooltipPosition=\"above\"\n [style.color]=\"autoRefreshEnabled ? '#4caf50' : '#9e9e9e'\"\n (click)=\"autoRefreshEnabled = !autoRefreshEnabled; toggleAutoRefresh()\">\n <mat-icon>autorenew</mat-icon>\n </button>\n <!-- Changed: Keep Open icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.allowUserKeepOpen\" mat-icon-button\n [matTooltip]=\"userKeepOpen ? 'Click to disable keep open' : 'Click to enable keep open'\" matTooltipPosition=\"above\"\n [style.color]=\"userKeepOpen ? '#4caf50' : '#9e9e9e'\"\n (click)=\"userKeepOpen = !userKeepOpen\">\n <mat-icon>push_pin</mat-icon>\n </button>\n\n <!-- TS-16: bind precomputed editButtonVM instead of testVisible/testDisabled (which cloned objects) per CD -->\n <div *ngIf=\"formConfig.mode=='view' && editButton && editButtonVM.visible\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Edit\" color=\"primary\" (click)=\"setMode('edit')\" [disabled]=\"editButtonVM.disabled\"><mat-icon>edit</mat-icon></button>\n </div>\n\n <!-- Changed (Quiet Loading): every [disabled] below now ORs in entityLoading. A quiet read is silent, so it\n never flips LoaderService.isLoading and isProcessing alone would leave these buttons live during the\n load \u2014 a user could submit an empty form. With the flag off entityLoading simply tracks the same\n window isProcessing already covered, so nothing changes. -->\n <!-- Changed: the icon now spins while the form's record reloads, matching the table's refresh button.\n Same .tin-spin class, so the two surfaces use one idiom rather than each inventing its own. -->\n <button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"loadByAction\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Refresh\" color=\"primary\" (click)=\"loadData(formConfig.loadAction, detailsConfig.causeTableRefresh)\"><mat-icon class=\"refreshIcon\" [class.tin-spin]=\"entityLoading\">cached</mat-icon></button>\n \n <!-- Added: Top close button when position is 'top' -->\n <button *ngIf=\"shouldShowTopClose()\" [disabled]=\"isProcessing || entityLoading\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Close\" (click)=\"close()\"><mat-icon>close</mat-icon></button>\n\n </div>\n\n </div>\n\n <div style=\"padding-left: 24px; padding-right: 24px;\">\n <spa-steps *ngIf=\"stepConfig && details && stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n </div>\n\n <mat-dialog-content class=\"mat-typography dialog-scroll-content\">\n\n <spa-steps *ngIf=\"stepConfig && details && !stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && !statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && !formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n\n <div class=\"tin-input\" style=\"font-size:14px\">\n\n <!-- Changed (Quiet Loading, FSD D6): with the flag OFF this is the original bare \"Loading...\" line, unchanged.\n With it ON the text is replaced by a skeleton of the real form \u2014 and only once showSkeleton has been\n armed past the 150ms anti-flicker delay, so a cached record renders straight into the form with no flash. -->\n <p *ngIf=\"formConfig && !details && !effQuietLoading\"><em>Loading...</em></p>\n\n <spa-form-skeleton *ngIf=\"formConfig && !details && effQuietLoading && showSkeleton\" [config]=\"formConfig\"></spa-form-skeleton>\n\n <spa-form *ngIf=\"formConfig && details\" [files]=\"files\" [data]=\"details\" [config]=\"formConfig\" (inputChange)=\"inputChanged($event)\">\n <ng-template #dynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\" let-resolveLoadAction=\"resolveLoadAction\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"resolveLoadAction ? resolveLoadAction(field) : field.loadAction\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n </spa-form>\n\n <!-- Changed: Use unified spa-tabs with nestingLevel control \u2014 tabs hidden when nestingLevel >= 2 -->\n <spa-tabs\n *ngIf=\"showTabs && tableConfigs && !(detailsConfig.hideTablesInCreateMode && formConfig?.mode === 'create')\"\n [tableConfigs]=\"tableConfigs\"\n [reload]=\"tableReload\"\n [parentDetails]=\"details\"\n [localMode]=\"formConfig?.mode === 'create'\"\n [nestingLevel]=\"nestingLevel + 1\"\n (formRefresh)=\"loadData(formConfig.loadAction, false)\"\n (actionSuccess)=\"actionPerformed = true\">\n </spa-tabs><!-- Changed: nested-tab table actions flag actionPerformed so refreshOnCloseIfActioned refreshes the parent on close -->\n\n </div>\n\n </mat-dialog-content>\n\n\n </div>\n\n <mat-dialog-actions >\n\n <div>\n\n <button mat-raised-button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"formConfig.mode=='create' && createButton\" color=\"primary\"\n (click)=\"create()\" cdkFocusInitial>{{createButton.display ?? 'Submit'}}\n </button>\n\n <button mat-raised-button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"formConfig.mode=='edit' && editButton\" color=\"primary\"\n (click)=\"edit()\" cdkFocusInitial>{{editButton.display ?? 'Submit'}}\n </button>\n\n <!-- TS-16: bind precomputed extraButtonVMs instead of testVisible/testDisabled/getButtonColor (each cloning objects) per CD -->\n <ng-container *ngFor=\"let vm of extraButtonVMs\">\n <button *ngIf=\"formConfig.mode !== 'create' && vm.visible\" mat-stroked-button [disabled]=\"isProcessing || entityLoading || vm.disabled\" [ngStyle]=\"{'color': vm.color}\" (click)=\"custom(vm.button)\" cdkFocusInitial>\n <mat-icon *ngIf=\"vm.button.icon\" [ngStyle]=\"{'color': vm.color}\">{{vm.button.icon.name}}</mat-icon>\n {{vm.button.display ?? vm.button.name | titlecase}}\n </button>\n </ng-container>\n\n <!-- Changed: Bottom close button now uses conditional display and custom text -->\n <button *ngIf=\"shouldShowBottomClose()\" mat-stroked-button color=\"primary\" (click)=\"close()\">{{getCloseText()}}</button>\n\n </div>\n\n <div class=\"col d-flex justify-content-end\" *ngIf=\"smallScreen\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Delete\" [disabled]=\"isProcessing || entityLoading\" style=\"color: red;\" (click)=\"delete()\" *ngIf=\"formConfig.mode!='create' && deleteButton\"><mat-icon>delete</mat-icon></button>\n </div>\n\n\n </mat-dialog-actions>\n\n\n</div>\n\n\n\n\n\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}.dlg-header-classic{padding-left:24px;padding-right:24px}.dlg-title-classic{font-size:20px;font-weight:500;margin-top:10px;margin-bottom:5px}.dialog-header-titles{display:flex;flex-direction:column;justify-content:center;min-width:0}.tin-dlg-head .dialog-header-titles label{margin:0}.tbl-section-header{display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:8px;border:1px solid rgba(0,0,0,.08);border-radius:10px;background:transparent;cursor:pointer;transition:border-color .15s}.tbl-section-header:hover{border-color:#90a4ae}.tbl-section-header.tbl-section-static{cursor:default}.tbl-section-icon{color:#546e7a}.tbl-section-title{font-size:14px;font-weight:600;color:#000000d1}.tbl-section-count{background:#e3f2fd;color:#1565c0;border-radius:12px;padding:2px 10px;font-size:12px}.tbl-section-chip{font-size:12px;font-weight:500;color:#0009}.tbl-section-spacer{margin-left:auto}.tbl-section-chevron{color:#90a4ae}.tbl-section-caption{margin:-4px 12px 10px;font-size:12.5px;line-height:1.45;color:#0000008c;max-width:82ch}.tbl-section-more{padding:0 12px 8px}.tbl-section-link{background:none;border:none;color:#1565c0;cursor:pointer;font-size:13px;padding:0}.tbl-section-link:hover{text-decoration:underline}.tbl-section-icon,.tbl-section-chevron,.tbl-section-count{flex:0 0 auto}.tbl-section-title{flex:1 1 auto;min-width:0}@media (max-width: 700px){.tbl-section-header{gap:8px;padding:10px}.tbl-section-chip,.tbl-section-btn-text.has-icon{display:none}.tbl-section-header button{min-width:0;padding:0 10px}}table.tin-no-col-headers tr.mat-mdc-header-row{display:none}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i4.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i4.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: StepsComponent, selector: "spa-steps", inputs: ["value", "config", "data", "activeIndex"] }, { kind: "component", type: FormComponent, selector: "spa-form", inputs: ["files", "data", "config"], outputs: ["buttonClick", "inputChange"] }, { kind: "component", type: FormSkeletonComponent, selector: "spa-form-skeleton", inputs: ["config"] }, { kind: "component", type: AlertComponent, selector: "spa-alert", inputs: ["config", "data"] }, { kind: "component", type: TabsComponent, selector: "spa-tabs", inputs: ["tableConfigs", "reload", "reloadTab", "parentDetails", "localMode", "nestingLevel"], outputs: ["formRefresh", "actionSuccess"] }, { kind: "component", type: StatusesComponent, selector: "spa-statuses", inputs: ["config", "data"] }, { kind: "pipe", type: i1$2.TitleCasePipe, name: "titlecase" }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
21996
22083
  }
21997
22084
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DetailsDialog, decorators: [{
21998
22085
  type: Component,
21999
- args: [{ selector: 'spa-detailsDialog', standalone: false, template: "<!-- Changed: dialog-has-tables mirrors the service's hasTables (tableConfigs?.length)\n which decides height '90%' vs 'auto' \u2014 used to scope scroll behaviour per case -->\n<div class=\"dialog-container\" [class.dialog-has-tables]=\"detailsConfig.tableConfigs?.length > 0\">\n\n <div class=\"dialog-content\">\n\n <!-- Changed: was <mat-progress-bar mode=\"indeterminate\">, which looped at a constant speed forever and so\n only ever said \"busy\". This is the same markup and the same PerceivedProgress curve the table stage\n uses, so a dialog and the table behind it move identically. It tracks the FORM's record load \u2014 nested\n tab tables run their own stages. Controlled by appConfig.quietLoading. -->\n <div *ngIf=\"showProgressLine\" class=\"tin-load-line\" aria-hidden=\"true\">\n <span class=\"tin-load-line-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n\n <!-- Changed: the header's padding and the title's type were inline styles, which always beat a class and so\n made a second presentation impossible. Moved into dlg-header-classic / dlg-title-classic, which restate\n those exact values, so the default look is unchanged while `modern` can swap in the banded header. -->\n <div class=\"d-flex justify-content-between align-items-center dialog-header\"\n [class.mt-2]=\"!modern\" [class.dlg-header-classic]=\"!modern\" [class.tin-dlg-head]=\"modern\">\n\n <div class=\"dialog-header-titles\">\n <label [class.dlg-title-classic]=\"!modern\" [class.tin-dlg-title]=\"modern\">{{titleAction | titlecase}} {{formConfig?.title}}</label>\n </div>\n\n <div class=\"d-flex align-items-center\" style=\"gap: 8px;\">\n\n <!-- Changed: Auto Refresh icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.autoRefreshConfig\" mat-icon-button\n [matTooltip]=\"autoRefreshEnabled ? 'Click to disable auto refresh' : 'Click to enable auto refresh'\" matTooltipPosition=\"above\"\n [style.color]=\"autoRefreshEnabled ? '#4caf50' : '#9e9e9e'\"\n (click)=\"autoRefreshEnabled = !autoRefreshEnabled; toggleAutoRefresh()\">\n <mat-icon>autorenew</mat-icon>\n </button>\n <!-- Changed: Keep Open icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.allowUserKeepOpen\" mat-icon-button\n [matTooltip]=\"userKeepOpen ? 'Click to disable keep open' : 'Click to enable keep open'\" matTooltipPosition=\"above\"\n [style.color]=\"userKeepOpen ? '#4caf50' : '#9e9e9e'\"\n (click)=\"userKeepOpen = !userKeepOpen\">\n <mat-icon>push_pin</mat-icon>\n </button>\n\n <!-- TS-16: bind precomputed editButtonVM instead of testVisible/testDisabled (which cloned objects) per CD -->\n <div *ngIf=\"formConfig.mode=='view' && editButton && editButtonVM.visible\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Edit\" color=\"primary\" (click)=\"setMode('edit')\" [disabled]=\"editButtonVM.disabled\"><mat-icon>edit</mat-icon></button>\n </div>\n\n <!-- Changed (Quiet Loading): every [disabled] below now ORs in entityLoading. A quiet read is silent, so it\n never flips LoaderService.isLoading and isProcessing alone would leave these buttons live during the\n load \u2014 a user could submit an empty form. With the flag off entityLoading simply tracks the same\n window isProcessing already covered, so nothing changes. -->\n <!-- Changed: the icon now spins while the form's record reloads, matching the table's refresh button.\n Same .tin-spin class, so the two surfaces use one idiom rather than each inventing its own. -->\n <button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"loadByAction\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Refresh\" color=\"primary\" (click)=\"loadData(formConfig.loadAction, detailsConfig.causeTableRefresh)\"><mat-icon class=\"refreshIcon\" [class.tin-spin]=\"entityLoading\">cached</mat-icon></button>\n \n <!-- Added: Top close button when position is 'top' -->\n <button *ngIf=\"shouldShowTopClose()\" [disabled]=\"isProcessing || entityLoading\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Close\" (click)=\"close()\"><mat-icon>close</mat-icon></button>\n\n </div>\n\n </div>\n\n <div style=\"padding-left: 24px; padding-right: 24px;\">\n <spa-steps *ngIf=\"stepConfig && details && stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n </div>\n\n <mat-dialog-content class=\"mat-typography dialog-scroll-content\">\n\n <spa-steps *ngIf=\"stepConfig && details && !stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && !statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && !formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n\n <div class=\"tin-input\" style=\"font-size:14px\">\n\n <!-- Changed (Quiet Loading, FSD D6): with the flag OFF this is the original bare \"Loading...\" line, unchanged.\n With it ON the text is replaced by a skeleton of the real form \u2014 and only once showSkeleton has been\n armed past the 150ms anti-flicker delay, so a cached record renders straight into the form with no flash. -->\n <p *ngIf=\"formConfig && !details && !effQuietLoading\"><em>Loading...</em></p>\n\n <spa-form-skeleton *ngIf=\"formConfig && !details && effQuietLoading && showSkeleton\" [config]=\"formConfig\"></spa-form-skeleton>\n\n <spa-form *ngIf=\"formConfig && details\" [files]=\"files\" [data]=\"details\" [config]=\"formConfig\" (inputChange)=\"inputChanged($event)\">\n <ng-template #dynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\" let-resolveLoadAction=\"resolveLoadAction\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"resolveLoadAction ? resolveLoadAction(field) : field.loadAction\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n </spa-form>\n\n <!-- Changed: Use unified spa-tabs with nestingLevel control \u2014 tabs hidden when nestingLevel >= 2 -->\n <spa-tabs\n *ngIf=\"showTabs && tableConfigs && !(detailsConfig.hideTablesInCreateMode && formConfig?.mode === 'create')\"\n [tableConfigs]=\"tableConfigs\"\n [reload]=\"tableReload\"\n [parentDetails]=\"details\"\n [localMode]=\"formConfig?.mode === 'create'\"\n [nestingLevel]=\"nestingLevel + 1\"\n (formRefresh)=\"loadData(formConfig.loadAction, false)\"\n (actionSuccess)=\"actionPerformed = true\">\n </spa-tabs><!-- Changed: nested-tab table actions flag actionPerformed so refreshOnCloseIfActioned refreshes the parent on close -->\n\n </div>\n\n </mat-dialog-content>\n\n\n </div>\n\n <mat-dialog-actions >\n\n <div>\n\n <button mat-raised-button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"formConfig.mode=='create' && createButton\" color=\"primary\"\n (click)=\"create()\" cdkFocusInitial>{{createButton.display ?? 'Submit'}}\n </button>\n\n <button mat-raised-button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"formConfig.mode=='edit' && editButton\" color=\"primary\"\n (click)=\"edit()\" cdkFocusInitial>{{editButton.display ?? 'Submit'}}\n </button>\n\n <!-- TS-16: bind precomputed extraButtonVMs instead of testVisible/testDisabled/getButtonColor (each cloning objects) per CD -->\n <ng-container *ngFor=\"let vm of extraButtonVMs\">\n <button *ngIf=\"formConfig.mode !== 'create' && vm.visible\" mat-stroked-button [disabled]=\"isProcessing || entityLoading || vm.disabled\" [ngStyle]=\"{'color': vm.color}\" (click)=\"custom(vm.button)\" cdkFocusInitial>\n <mat-icon *ngIf=\"vm.button.icon\" [ngStyle]=\"{'color': vm.color}\">{{vm.button.icon.name}}</mat-icon>\n {{vm.button.display ?? vm.button.name | titlecase}}\n </button>\n </ng-container>\n\n <!-- Changed: Bottom close button now uses conditional display and custom text -->\n <button *ngIf=\"shouldShowBottomClose()\" mat-stroked-button color=\"primary\" (click)=\"close()\">{{getCloseText()}}</button>\n\n </div>\n\n <div class=\"col d-flex justify-content-end\" *ngIf=\"smallScreen\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Delete\" [disabled]=\"isProcessing || entityLoading\" style=\"color: red;\" (click)=\"delete()\" *ngIf=\"formConfig.mode!='create' && deleteButton\"><mat-icon>delete</mat-icon></button>\n </div>\n\n\n </mat-dialog-actions>\n\n\n</div>\n\n\n\n\n\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}.dlg-header-classic{padding-left:24px;padding-right:24px}.dlg-title-classic{font-size:20px;font-weight:500;margin-top:10px;margin-bottom:5px}.dialog-header-titles{display:flex;flex-direction:column;justify-content:center;min-width:0}.tin-dlg-head .dialog-header-titles label{margin:0}.tbl-section-header{display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:8px;border:1px solid rgba(0,0,0,.08);border-radius:10px;background:transparent;cursor:pointer;transition:border-color .15s}.tbl-section-header:hover{border-color:#90a4ae}.tbl-section-header.tbl-section-static{cursor:default}.tbl-section-icon{color:#546e7a}.tbl-section-title{font-size:14px;font-weight:600;color:#000000d1}.tbl-section-count{background:#e3f2fd;color:#1565c0;border-radius:12px;padding:2px 10px;font-size:12px}.tbl-section-chip{font-size:12px;font-weight:500;color:#0009}.tbl-section-spacer{margin-left:auto}.tbl-section-chevron{color:#90a4ae}.tbl-section-more{padding:0 12px 8px}.tbl-section-link{background:none;border:none;color:#1565c0;cursor:pointer;font-size:13px;padding:0}.tbl-section-link:hover{text-decoration:underline}table.tin-no-col-headers tr.mat-mdc-header-row{display:none}\n"] }]
22086
+ args: [{ selector: 'spa-detailsDialog', standalone: false, template: "<!-- Changed: dialog-has-tables mirrors the service's hasTables (tableConfigs?.length)\n which decides height '90%' vs 'auto' \u2014 used to scope scroll behaviour per case -->\n<div class=\"dialog-container\" [class.dialog-has-tables]=\"detailsConfig.tableConfigs?.length > 0\">\n\n <div class=\"dialog-content\">\n\n <!-- Changed: was <mat-progress-bar mode=\"indeterminate\">, which looped at a constant speed forever and so\n only ever said \"busy\". This is the same markup and the same PerceivedProgress curve the table stage\n uses, so a dialog and the table behind it move identically. It tracks the FORM's record load \u2014 nested\n tab tables run their own stages. Controlled by appConfig.quietLoading. -->\n <div *ngIf=\"showProgressLine\" class=\"tin-load-line\" aria-hidden=\"true\">\n <span class=\"tin-load-line-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n\n <!-- Changed: the header's padding and the title's type were inline styles, which always beat a class and so\n made a second presentation impossible. Moved into dlg-header-classic / dlg-title-classic, which restate\n those exact values, so the default look is unchanged while `modern` can swap in the banded header. -->\n <div class=\"d-flex justify-content-between align-items-center dialog-header\"\n [class.mt-2]=\"!modern\" [class.dlg-header-classic]=\"!modern\" [class.tin-dlg-head]=\"modern\">\n\n <div class=\"dialog-header-titles\">\n <label [class.dlg-title-classic]=\"!modern\" [class.tin-dlg-title]=\"modern\">{{titleAction | titlecase}} {{formConfig?.title}}</label>\n </div>\n\n <div class=\"d-flex align-items-center\" style=\"gap: 8px;\">\n\n <!-- Changed: Auto Refresh icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.autoRefreshConfig\" mat-icon-button\n [matTooltip]=\"autoRefreshEnabled ? 'Click to disable auto refresh' : 'Click to enable auto refresh'\" matTooltipPosition=\"above\"\n [style.color]=\"autoRefreshEnabled ? '#4caf50' : '#9e9e9e'\"\n (click)=\"autoRefreshEnabled = !autoRefreshEnabled; toggleAutoRefresh()\">\n <mat-icon>autorenew</mat-icon>\n </button>\n <!-- Changed: Keep Open icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.allowUserKeepOpen\" mat-icon-button\n [matTooltip]=\"userKeepOpen ? 'Click to disable keep open' : 'Click to enable keep open'\" matTooltipPosition=\"above\"\n [style.color]=\"userKeepOpen ? '#4caf50' : '#9e9e9e'\"\n (click)=\"userKeepOpen = !userKeepOpen\">\n <mat-icon>push_pin</mat-icon>\n </button>\n\n <!-- TS-16: bind precomputed editButtonVM instead of testVisible/testDisabled (which cloned objects) per CD -->\n <div *ngIf=\"formConfig.mode=='view' && editButton && editButtonVM.visible\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Edit\" color=\"primary\" (click)=\"setMode('edit')\" [disabled]=\"editButtonVM.disabled\"><mat-icon>edit</mat-icon></button>\n </div>\n\n <!-- Changed (Quiet Loading): every [disabled] below now ORs in entityLoading. A quiet read is silent, so it\n never flips LoaderService.isLoading and isProcessing alone would leave these buttons live during the\n load \u2014 a user could submit an empty form. With the flag off entityLoading simply tracks the same\n window isProcessing already covered, so nothing changes. -->\n <!-- Changed: the icon now spins while the form's record reloads, matching the table's refresh button.\n Same .tin-spin class, so the two surfaces use one idiom rather than each inventing its own. -->\n <button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"loadByAction\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Refresh\" color=\"primary\" (click)=\"loadData(formConfig.loadAction, detailsConfig.causeTableRefresh)\"><mat-icon class=\"refreshIcon\" [class.tin-spin]=\"entityLoading\">cached</mat-icon></button>\n \n <!-- Added: Top close button when position is 'top' -->\n <button *ngIf=\"shouldShowTopClose()\" [disabled]=\"isProcessing || entityLoading\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Close\" (click)=\"close()\"><mat-icon>close</mat-icon></button>\n\n </div>\n\n </div>\n\n <div style=\"padding-left: 24px; padding-right: 24px;\">\n <spa-steps *ngIf=\"stepConfig && details && stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n </div>\n\n <mat-dialog-content class=\"mat-typography dialog-scroll-content\">\n\n <spa-steps *ngIf=\"stepConfig && details && !stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && !statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && !formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n\n <div class=\"tin-input\" style=\"font-size:14px\">\n\n <!-- Changed (Quiet Loading, FSD D6): with the flag OFF this is the original bare \"Loading...\" line, unchanged.\n With it ON the text is replaced by a skeleton of the real form \u2014 and only once showSkeleton has been\n armed past the 150ms anti-flicker delay, so a cached record renders straight into the form with no flash. -->\n <p *ngIf=\"formConfig && !details && !effQuietLoading\"><em>Loading...</em></p>\n\n <spa-form-skeleton *ngIf=\"formConfig && !details && effQuietLoading && showSkeleton\" [config]=\"formConfig\"></spa-form-skeleton>\n\n <spa-form *ngIf=\"formConfig && details\" [files]=\"files\" [data]=\"details\" [config]=\"formConfig\" (inputChange)=\"inputChanged($event)\">\n <ng-template #dynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\" let-resolveLoadAction=\"resolveLoadAction\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"resolveLoadAction ? resolveLoadAction(field) : field.loadAction\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n </spa-form>\n\n <!-- Changed: Use unified spa-tabs with nestingLevel control \u2014 tabs hidden when nestingLevel >= 2 -->\n <spa-tabs\n *ngIf=\"showTabs && tableConfigs && !(detailsConfig.hideTablesInCreateMode && formConfig?.mode === 'create')\"\n [tableConfigs]=\"tableConfigs\"\n [reload]=\"tableReload\"\n [parentDetails]=\"details\"\n [localMode]=\"formConfig?.mode === 'create'\"\n [nestingLevel]=\"nestingLevel + 1\"\n (formRefresh)=\"loadData(formConfig.loadAction, false)\"\n (actionSuccess)=\"actionPerformed = true\">\n </spa-tabs><!-- Changed: nested-tab table actions flag actionPerformed so refreshOnCloseIfActioned refreshes the parent on close -->\n\n </div>\n\n </mat-dialog-content>\n\n\n </div>\n\n <mat-dialog-actions >\n\n <div>\n\n <button mat-raised-button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"formConfig.mode=='create' && createButton\" color=\"primary\"\n (click)=\"create()\" cdkFocusInitial>{{createButton.display ?? 'Submit'}}\n </button>\n\n <button mat-raised-button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"formConfig.mode=='edit' && editButton\" color=\"primary\"\n (click)=\"edit()\" cdkFocusInitial>{{editButton.display ?? 'Submit'}}\n </button>\n\n <!-- TS-16: bind precomputed extraButtonVMs instead of testVisible/testDisabled/getButtonColor (each cloning objects) per CD -->\n <ng-container *ngFor=\"let vm of extraButtonVMs\">\n <button *ngIf=\"formConfig.mode !== 'create' && vm.visible\" mat-stroked-button [disabled]=\"isProcessing || entityLoading || vm.disabled\" [ngStyle]=\"{'color': vm.color}\" (click)=\"custom(vm.button)\" cdkFocusInitial>\n <mat-icon *ngIf=\"vm.button.icon\" [ngStyle]=\"{'color': vm.color}\">{{vm.button.icon.name}}</mat-icon>\n {{vm.button.display ?? vm.button.name | titlecase}}\n </button>\n </ng-container>\n\n <!-- Changed: Bottom close button now uses conditional display and custom text -->\n <button *ngIf=\"shouldShowBottomClose()\" mat-stroked-button color=\"primary\" (click)=\"close()\">{{getCloseText()}}</button>\n\n </div>\n\n <div class=\"col d-flex justify-content-end\" *ngIf=\"smallScreen\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Delete\" [disabled]=\"isProcessing || entityLoading\" style=\"color: red;\" (click)=\"delete()\" *ngIf=\"formConfig.mode!='create' && deleteButton\"><mat-icon>delete</mat-icon></button>\n </div>\n\n\n </mat-dialog-actions>\n\n\n</div>\n\n\n\n\n\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}.dlg-header-classic{padding-left:24px;padding-right:24px}.dlg-title-classic{font-size:20px;font-weight:500;margin-top:10px;margin-bottom:5px}.dialog-header-titles{display:flex;flex-direction:column;justify-content:center;min-width:0}.tin-dlg-head .dialog-header-titles label{margin:0}.tbl-section-header{display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:8px;border:1px solid rgba(0,0,0,.08);border-radius:10px;background:transparent;cursor:pointer;transition:border-color .15s}.tbl-section-header:hover{border-color:#90a4ae}.tbl-section-header.tbl-section-static{cursor:default}.tbl-section-icon{color:#546e7a}.tbl-section-title{font-size:14px;font-weight:600;color:#000000d1}.tbl-section-count{background:#e3f2fd;color:#1565c0;border-radius:12px;padding:2px 10px;font-size:12px}.tbl-section-chip{font-size:12px;font-weight:500;color:#0009}.tbl-section-spacer{margin-left:auto}.tbl-section-chevron{color:#90a4ae}.tbl-section-caption{margin:-4px 12px 10px;font-size:12.5px;line-height:1.45;color:#0000008c;max-width:82ch}.tbl-section-more{padding:0 12px 8px}.tbl-section-link{background:none;border:none;color:#1565c0;cursor:pointer;font-size:13px;padding:0}.tbl-section-link:hover{text-decoration:underline}.tbl-section-icon,.tbl-section-chevron,.tbl-section-count{flex:0 0 auto}.tbl-section-title{flex:1 1 auto;min-width:0}@media (max-width: 700px){.tbl-section-header{gap:8px;padding:10px}.tbl-section-chip,.tbl-section-btn-text.has-icon{display:none}.tbl-section-header button{min-width:0;padding:0 10px}}table.tin-no-col-headers tr.mat-mdc-header-row{display:none}\n"] }]
22000
22087
  }], ctorParameters: () => [{ type: i1$4.BreakpointObserver }, { type: LoaderService }, { type: DataServiceLib }, { type: MessageService }, { type: i4.MatDialogRef }, { type: DetailsDialogConfig, decorators: [{
22001
22088
  type: Inject,
22002
22089
  args: [MAT_DIALOG_DATA]
@@ -25845,7 +25932,7 @@ class StatementComponent {
25845
25932
  </mat-card>
25846
25933
  </div>
25847
25934
  </ng-template>
25848
- `, isInline: true, dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i2$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: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i5$3.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i5$3.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4$2.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: i7$1.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: PageComponent, selector: "spa-page", inputs: ["config"], outputs: ["searchModeActivated", "searchModeDeactivated", "refreshClick", "actionClick", "actionResponse", "inputChange", "createClick", "searchClick", "dataLoad", "titleActionChange"] }, { kind: "pipe", type: i1$2.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }] }); }
25935
+ `, isInline: true, dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i2$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: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i5$4.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i5$4.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4$2.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: i7$1.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: PageComponent, selector: "spa-page", inputs: ["config"], outputs: ["searchModeActivated", "searchModeDeactivated", "refreshClick", "actionClick", "actionResponse", "inputChange", "createClick", "searchClick", "dataLoad", "titleActionChange"] }, { kind: "pipe", type: i1$2.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }] }); }
25849
25936
  }
25850
25937
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: StatementComponent, decorators: [{
25851
25938
  type: Component,
@@ -26036,7 +26123,7 @@ class ReportsComponent {
26036
26123
  </div>
26037
26124
 
26038
26125
  <spa-tabs [tableConfigs]="reportTabConfigs"></spa-tabs>
26039
- `, isInline: true, dependencies: [{ kind: "directive", type: i2$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: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i7$2.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7$2.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7$2.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4$2.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: TabsComponent, selector: "spa-tabs", inputs: ["tableConfigs", "reload", "reloadTab", "parentDetails", "localMode", "nestingLevel"], outputs: ["formRefresh", "actionSuccess"] }] }); }
26126
+ `, isInline: true, dependencies: [{ kind: "directive", type: i2$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: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i4$4.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i4$4.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i4$4.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4$2.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: TabsComponent, selector: "spa-tabs", inputs: ["tableConfigs", "reload", "reloadTab", "parentDetails", "localMode", "nestingLevel"], outputs: ["formRefresh", "actionSuccess"] }] }); }
26040
26127
  }
26041
26128
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ReportsComponent, decorators: [{
26042
26129
  type: Component,
@@ -26605,7 +26692,7 @@ class VatReturnComponent {
26605
26692
  </mat-card>
26606
26693
  </mat-tab>
26607
26694
  </mat-tab-group>
26608
- `, isInline: true, dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$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: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i5$3.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i5$3.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4$2.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "pipe", type: i1$2.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }] }); }
26695
+ `, isInline: true, dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$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: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i5$4.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i5$4.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4$2.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "pipe", type: i1$2.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }] }); }
26609
26696
  }
26610
26697
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: VatReturnComponent, decorators: [{
26611
26698
  type: Component,
@@ -30275,7 +30362,7 @@ class ApprovalsComponent {
30275
30362
  ngOnInit() {
30276
30363
  }
30277
30364
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ApprovalsComponent, deps: [{ token: DataServiceLib }], target: i0.ɵɵFactoryTarget.Component }); }
30278
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: ApprovalsComponent, isStandalone: false, selector: "spa-approvals", ngImport: i0, template: "<h4>Approvals</h4>\n<hr>\n\n<mat-tab-group>\n <mat-tab label=\"Received\">\n <div class=\"mt-3\">\n <spa-table [config]=\"dataService.receivedApprovalsTableConfig\"></spa-table>\n </div>\n </mat-tab>\n <mat-tab label=\"Sent\">\n <div class=\"mt-3\">\n <spa-table [config]=\"dataService.sentApprovalsTableConfig\"></spa-table>\n </div>\n </mat-tab>\n</mat-tab-group>\n", styles: [""], dependencies: [{ kind: "component", type: i5$3.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i5$3.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }] }); }
30365
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: ApprovalsComponent, isStandalone: false, selector: "spa-approvals", ngImport: i0, template: "<h4>Approvals</h4>\n<hr>\n\n<mat-tab-group>\n <mat-tab label=\"Received\">\n <div class=\"mt-3\">\n <spa-table [config]=\"dataService.receivedApprovalsTableConfig\"></spa-table>\n </div>\n </mat-tab>\n <mat-tab label=\"Sent\">\n <div class=\"mt-3\">\n <spa-table [config]=\"dataService.sentApprovalsTableConfig\"></spa-table>\n </div>\n </mat-tab>\n</mat-tab-group>\n", styles: [""], dependencies: [{ kind: "component", type: i5$4.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i5$4.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }] }); }
30279
30366
  }
30280
30367
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ApprovalsComponent, decorators: [{
30281
30368
  type: Component,
@@ -31391,21 +31478,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
31391
31478
  }]
31392
31479
  }] });
31393
31480
 
31394
- // Added: the Day Book page's own module it declares and EXPORTS <spa-day-book> plus the *spaDayBookSection
31395
- // row-template slot, so a consumer app can host the page in its own component and project per-section row
31396
- // markup (including inline editors) into it. Carries no routes and no providers, so importing it into an
31397
- // app feature module is free. Apps that need no app-drawn rows keep using the library route instead and
31398
- // import nothing. Named SpaDayBookModule because the DTO interface `SetupModule` already owns the plain
31399
- // `SetupModule` name in Classes.ts, which makes it unexportable from the public entry point.
31481
+ // Changed: the module now declares and exports <spa-day-book> and nothing else — the *spaDayBookSection /
31482
+ // *spaDayBookSectionHeader template slots are gone, and with them the reason an app ever had to host the page
31483
+ // in a component of its own. It is still a separate module (rather than folded away) for the two entry points
31484
+ // that remain: SpaHomeRoutingModule's route, and an app that wants the page inside its own shell both get
31485
+ // the same config-driven rendering now. Carries no routes and no providers, so importing it is free. Named
31486
+ // SpaDayBookModule because the DTO interface `SetupModule` already owns the plain `SetupModule` name in
31487
+ // Classes.ts, which makes it unexportable from the public entry point.
31400
31488
  class SpaDayBookModule {
31401
31489
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SpaDayBookModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
31402
- static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.14", ngImport: i0, type: SpaDayBookModule, declarations: [DayBookComponent,
31403
- DayBookSectionDirective,
31404
- DayBookSectionHeaderDirective // Added: the section-level (batch action / subtotal) slot
31405
- ], imports: [CommonModule,
31490
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.14", ngImport: i0, type: SpaDayBookModule, declarations: [DayBookComponent], imports: [CommonModule,
31406
31491
  SpaMatModule,
31407
- TinSpaModule], exports: [DayBookComponent,
31408
- DayBookSectionDirective] }); }
31492
+ TinSpaModule], exports: [DayBookComponent] }); }
31409
31493
  static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SpaDayBookModule, imports: [CommonModule,
31410
31494
  SpaMatModule,
31411
31495
  TinSpaModule] }); }
@@ -31414,9 +31498,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
31414
31498
  type: NgModule,
31415
31499
  args: [{
31416
31500
  declarations: [
31417
- DayBookComponent,
31418
- DayBookSectionDirective,
31419
- DayBookSectionHeaderDirective // Added: the section-level (batch action / subtotal) slot
31501
+ DayBookComponent
31420
31502
  ],
31421
31503
  imports: [
31422
31504
  CommonModule,
@@ -31424,8 +31506,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
31424
31506
  TinSpaModule
31425
31507
  ],
31426
31508
  exports: [
31427
- DayBookComponent,
31428
- DayBookSectionDirective
31509
+ DayBookComponent
31429
31510
  ]
31430
31511
  }]
31431
31512
  }] });
@@ -31566,5 +31647,5 @@ const ALSQUARE_SVG_WHITE = `<svg viewBox="0 0 80 80" fill="none" xmlns="http://w
31566
31647
  * Generated bundle index. Do not edit.
31567
31648
  */
31568
31649
 
31569
- export { ALSQUARE_SVG_DARK, ALSQUARE_SVG_WHITE, Account, AccountsComponent as AccountingAccountsComponent, AggregatesComponent as AccountingAggregatesComponent, AgingComponent as AccountingAgingComponent, CreditNotesComponent as AccountingCreditNotesComponent, CurrenciesComponent as AccountingCurrenciesComponent, AccountingDashboardComponent, InvoicesComponent as AccountingInvoicesComponent, AccountingModule, ReportsComponent as AccountingReportsComponent, AccountingService, StatementComponent as AccountingStatementComponent, TransactionTypesComponent as AccountingTransactionTypesComponent, TransactionsComponent as AccountingTransactionsComponent, VatReturnComponent as AccountingVatReturnComponent, Action, ActivityComponent, AdminModule, AgentComponent, AgentPageComponent, AgentService, AlertComponent, AlertConfig, AlertMessage, AnalyticsService, ApiErrorService, ApiResponse, AppConfig, AppConfigurationComponent, AppModelsComponent, AssetStatus, AssetsService, AttachComponent, AuthService, BankReconciliationComponent, BillingPageComponent, BottomTab, BrandsComponent, CacheConfig, CapItem, CapsulesComponent, CategoriesComponent, ChangePasswordComponent, ChangeUserPassword, ChartConfig, ChartsComponent, CheckComponent, ChecklistComponent, ChipsComponent, ConfigService, Constants, Core, CreateAccountComponent, CreditNoteStatus, CustomersComponent, DataServiceLib, DateComponent, DatetimeComponent, DayBookComponent, DayBookSectionDirective, DayBookSectionHeaderDirective, DepartmentsComponent, DetailsDialog, DetailsDialogConfig, DetailsDialogProcessor, DetailsSource, DialogService, EditorComponent, EmailComponent, EmployeesComponent, ExportService, FeatureDirective, FieldLoadIndicator, FilterComponent, FiscalPeriodStatus, FiscalPeriodsComponent, FormComponent, FormConfig, FormSkeletonComponent, GeneralModule, GeneralService, GradesComponent, GroupsComponent, HRModule, HtmlComponent, HttpService, ImportDialogComponent, IndexModule, InventoryDashboardComponent, InventoryModule, InventoryService, InvitationsTableComponent, InvoiceDashboardComponent, InvoiceItemType, InvoiceStatus, JournalEntryDialogComponent, LabelComponent, LastRouteService, ListDialogComponent, ListDialogConfig, LoaderComponent, LoaderService, LoanPaymentsComponent, LoanProductsComponent, LoansComponent, LoansModule, LoansService, LogLevel, LogService, LoginComponent, LogsComponent, ManufacturingModule, MembershipComponent, MessageService, MoneyComponent, MovementType, NavMenuComponent, NotesComponent, NotesConfig, NotificationsService, NumberComponent, OfflineIndicatorComponent, OfflineService, OnboardingComponent, OptionComponent, OverviewDashboardComponent, OverviewModule, PageComponent, PageConfig, PaginationConfig, PayrollDashboardComponent, PayrollModule, PayrollService, PerceivedProgress, PlansComponent, PositionsComponent, PrivacyDialogComponent, ProductionService, Profile, ProfileComponent, PurchaseStatus, PurchasingDashboardComponent, PurchasingModule, PurchasingService, PushNotificationService, QUIET_DELAY_MS, QUIET_MIN_MS, QuoteStatus, QuotesComponent, ReceiptDialogComponent, ReceiptStatus, ReceiptsComponent, ReconcileDialogComponent, RecoverAccountComponent, Register, RevenueScheduleDialogComponent, RevenueSchedulesComponent, Role, RoleAccess, RolesComponent, SalesDashboardComponent, SalesModule, SalesService, SearchComponent, SearchConfig, SecurityConfig, SelectBitwiseComponent, SelectComponent, SelectLiteComponent, SelectMultiComponent, SettingsComponent, SetupGuideComponent, SetupService, SignupComponent, SignupData, SpaAdminModule, SpaDayBookModule, SpaHomeModule, SpaIndexModule, SpaLandingComponent, SpaMatModule, SpaUserModule, StatementImportDialogComponent, StatusesComponent, Step, StepConfig, StepsComponent, StorageService, SubCategoriesComponent, SubscriptionPageComponent, SubscriptionService, SuppliersComponent, SyncComponent, TIN_SILENT_REQUEST, TIN_SPA_RUNTIME_CONFIG, TabService, TableComponent, TableConfig, TabsComponent, TasksComponent, TenancyModule, TenantsComponent, TermsDialogComponent, TextAreaComponent, TextComponent, TextMaskComponent, TextMultiComponent, TextSingleComponent, TileConfig, TilesComponent, TinSpaComponent, TinSpaModule, TinSpaService, TitleActionsComponent, TransactionTiming, UnitOfMeasure, UpdateService, User, UserModule, UsersComponent, ViewerComponent, WelcomeComponent, WorkflowModule, authGuard, dialogOptions, featureGuard, getTinSpaAppSettings, isSilentRequest, loadTinSpaAppSettings, loginConfig, messageDialog, provideTinSpaRuntime, resolveQuietLoading, silentContext, tinSpaLocationStrategyFactory, tinSpaMsalInstanceFactory, tinSpaRuntimeConfigFactory, viewerDialog };
31650
+ export { ALSQUARE_SVG_DARK, ALSQUARE_SVG_WHITE, Account, AccountsComponent as AccountingAccountsComponent, AggregatesComponent as AccountingAggregatesComponent, AgingComponent as AccountingAgingComponent, CreditNotesComponent as AccountingCreditNotesComponent, CurrenciesComponent as AccountingCurrenciesComponent, AccountingDashboardComponent, InvoicesComponent as AccountingInvoicesComponent, AccountingModule, ReportsComponent as AccountingReportsComponent, AccountingService, StatementComponent as AccountingStatementComponent, TransactionTypesComponent as AccountingTransactionTypesComponent, TransactionsComponent as AccountingTransactionsComponent, VatReturnComponent as AccountingVatReturnComponent, Action, ActivityComponent, AdminModule, AgentComponent, AgentPageComponent, AgentService, AlertComponent, AlertConfig, AlertMessage, AnalyticsService, ApiErrorService, ApiResponse, AppConfig, AppConfigurationComponent, AppModelsComponent, AssetStatus, AssetsService, AttachComponent, AuthService, BankReconciliationComponent, BillingPageComponent, BottomTab, BrandsComponent, CacheConfig, CapItem, CapsulesComponent, CategoriesComponent, ChangePasswordComponent, ChangeUserPassword, ChartConfig, ChartsComponent, CheckComponent, ChecklistComponent, ChipsComponent, ConfigService, Constants, Core, CreateAccountComponent, CreditNoteStatus, CustomersComponent, DataServiceLib, DateComponent, DatetimeComponent, DayBookComponent, DepartmentsComponent, DetailsDialog, DetailsDialogConfig, DetailsDialogProcessor, DetailsSource, DialogService, EditorComponent, EmailComponent, EmployeesComponent, ExportService, FeatureDirective, FieldLoadIndicator, FilterComponent, FiscalPeriodStatus, FiscalPeriodsComponent, FormComponent, FormConfig, FormSkeletonComponent, GeneralModule, GeneralService, GradesComponent, GroupsComponent, HRModule, HtmlComponent, HttpService, ImportDialogComponent, IndexModule, InventoryDashboardComponent, InventoryModule, InventoryService, InvitationsTableComponent, InvoiceDashboardComponent, InvoiceItemType, InvoiceStatus, JournalEntryDialogComponent, LabelComponent, LastRouteService, ListDialogComponent, ListDialogConfig, LoaderComponent, LoaderService, LoanPaymentsComponent, LoanProductsComponent, LoansComponent, LoansModule, LoansService, LogLevel, LogService, LoginComponent, LogsComponent, ManufacturingModule, MembershipComponent, MessageService, MoneyComponent, MovementType, NavMenuComponent, NotesComponent, NotesConfig, NotificationsService, NumberComponent, OfflineIndicatorComponent, OfflineService, OnboardingComponent, OptionComponent, OverviewDashboardComponent, OverviewModule, PageComponent, PageConfig, PaginationConfig, PayrollDashboardComponent, PayrollModule, PayrollService, PerceivedProgress, PlansComponent, PositionsComponent, PrivacyDialogComponent, ProductionService, Profile, ProfileComponent, PurchaseStatus, PurchasingDashboardComponent, PurchasingModule, PurchasingService, PushNotificationService, QUIET_DELAY_MS, QUIET_MIN_MS, QuoteStatus, QuotesComponent, ReceiptDialogComponent, ReceiptStatus, ReceiptsComponent, ReconcileDialogComponent, RecoverAccountComponent, Register, RevenueScheduleDialogComponent, RevenueSchedulesComponent, Role, RoleAccess, RolesComponent, SalesDashboardComponent, SalesModule, SalesService, SearchComponent, SearchConfig, SecurityConfig, SelectBitwiseComponent, SelectComponent, SelectLiteComponent, SelectMultiComponent, SettingsComponent, SetupGuideComponent, SetupService, SignupComponent, SignupData, SpaAdminModule, SpaDayBookModule, SpaHomeModule, SpaIndexModule, SpaLandingComponent, SpaMatModule, SpaUserModule, StatementImportDialogComponent, StatusesComponent, Step, StepConfig, StepsComponent, StorageService, SubCategoriesComponent, SubscriptionPageComponent, SubscriptionService, SuppliersComponent, SyncComponent, TIN_SILENT_REQUEST, TIN_SPA_RUNTIME_CONFIG, TabService, TableComponent, TableConfig, TabsComponent, TasksComponent, TenancyModule, TenantsComponent, TermsDialogComponent, TextAreaComponent, TextComponent, TextMaskComponent, TextMultiComponent, TextSingleComponent, TileConfig, TilesComponent, TinSpaComponent, TinSpaModule, TinSpaService, TitleActionsComponent, TransactionTiming, UnitOfMeasure, UpdateService, User, UserModule, UsersComponent, ViewerComponent, WelcomeComponent, WorkflowModule, authGuard, dialogOptions, featureGuard, getTinSpaAppSettings, isSilentRequest, loadTinSpaAppSettings, loginConfig, messageDialog, provideTinSpaRuntime, resolveQuietLoading, silentContext, tinSpaLocationStrategyFactory, tinSpaMsalInstanceFactory, tinSpaRuntimeConfigFactory, viewerDialog };
31570
31651
  //# sourceMappingURL=tin-spa.mjs.map