tin-spa 20.14.26 → 20.14.29

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, 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, NgZone, 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';
@@ -161,7 +161,18 @@ class Core {
161
161
  if (value.length == 0)
162
162
  return value;
163
163
  let v = value.charAt(0).toUpperCase() + value.substring(1);
164
- return v.replace(/([A-Z]+)*([A-Z][a-z])/g, "$1 $2");
164
+ // Changed: only split at a REAL camelCase boundary — an alphanumeric immediately followed by Capital+lowercase.
165
+ // The old /([A-Z]+)*([A-Z][a-z])/g inserted a space before EVERY TitleCase run regardless of what preceded it,
166
+ // so it also fired at position 0 and straight after a space, hyphen or bracket. Every call site is
167
+ // `field.alias ?? field.name | camelToWords`, which Angular parses as `(alias ?? name) | camelToWords` — so
168
+ // explicit aliases were being run through the camelCase splitter too. A leading/doubled space is invisible
169
+ // (HTML collapses whitespace), which is why this went unnoticed, but a space inserted after a non-space
170
+ // character does NOT collapse: alias 'Check-In Inspection' rendered as 'Check- In Inspection', and
171
+ // 'Year-To-Date (Net)' as 'Year- To- Date ( Net)'. The lookahead does not consume the preceding character,
172
+ // so consecutive boundaries still match ('serviceByDate' -> 'Service By Date'; a consuming capture group
173
+ // yields 'Service ByDate'). Verified against every field name in the config forms: output is identical to
174
+ // the old behaviour for camelCase names, and only the spurious spaces are gone.
175
+ return v.replace(/([a-zA-Z0-9])(?=[A-Z][a-z])/g, "$1 ");
165
176
  }
166
177
  static generateObject(fields) {
167
178
  let data = {};
@@ -4443,7 +4454,7 @@ class DataServiceLib {
4443
4454
  ]
4444
4455
  },
4445
4456
  { name: 'comments', type: 'text', },
4446
- { name: 'createdByName', type: 'text', alias: 'By' },
4457
+ { name: 'createdByName', type: 'monogram', alias: 'By' }, // Changed: monogram — the row still carries the full name, so filter/sort are untouched
4447
4458
  ],
4448
4459
  buttons: [
4449
4460
  { name: 'view', dialog: true }
@@ -4551,7 +4562,7 @@ class DataServiceLib {
4551
4562
  collapseButtons: true,
4552
4563
  columns: [
4553
4564
  { name: 'createdDate', type: 'datetime', alias: 'Date' },
4554
- { name: 'createdByName', alias: 'Request By', type: 'text' },
4565
+ { name: 'createdByName', alias: 'Request By', type: 'monogram' }, // Changed: monogram
4555
4566
  { name: 'message', type: 'text' },
4556
4567
  {
4557
4568
  name: 'typeName', alias: 'Type', type: 'chip',
@@ -4577,7 +4588,7 @@ class DataServiceLib {
4577
4588
  { name: 'schedule', color: '#FFC107', condition: x => x.status === 'Pending', tip: 'Pending' }
4578
4589
  ]
4579
4590
  },
4580
- { name: 'updatedByName', alias: 'Actioned By', type: 'text' },
4591
+ { name: 'updatedByName', alias: 'Actioned By', type: 'monogram' }, // Changed: monogram
4581
4592
  { name: 'updatedDateDisplay', type: 'datetime', alias: 'Actioned Date' },
4582
4593
  ],
4583
4594
  buttons: [
@@ -5358,6 +5369,9 @@ class messageDialog {
5358
5369
  this._messageSubject = this.data.subject;
5359
5370
  this._messageDetails = this.data.details;
5360
5371
  this._messageReference = this.data.reference; // Added: optional backend correlation code for this failure
5372
+ this._confirmLabel = this.data.confirmLabel; // Added: optional caller-supplied action label for the affirmative button
5373
+ this._cancelLabel = this.data.cancelLabel; // Added: optional caller-supplied label for the dismissive button
5374
+ this._okLabel = this.data.okLabel; // Added: optional caller-supplied label for the acknowledge button
5361
5375
  // Added: resolve the style once, at open time. The panel class carries surface-level styling (radius,
5362
5376
  // shadow) that component CSS cannot reach, because the Material surface is an ancestor of this view.
5363
5377
  this.modern = !!this.dataService?.modernDialogs;
@@ -5399,6 +5413,12 @@ class messageDialog {
5399
5413
  return ref.length ? ref : null;
5400
5414
  }
5401
5415
  get isConfirm() { return this.messageType === 'confirm'; }
5416
+ label(value, fallback) {
5417
+ return (value && String(value).trim()) ? String(value).trim() : fallback;
5418
+ }
5419
+ get confirmLabel() { return this.label(this._confirmLabel, 'Yes'); }
5420
+ get cancelLabel() { return this.label(this._cancelLabel, 'No'); }
5421
+ get okLabel() { return this.label(this._okLabel, 'OK'); }
5402
5422
  // Added: heading for the CLASSIC branch. The original template hardcoded one word per type and only rendered
5403
5423
  // the caller's subject for 'info'; here the subject still wins when supplied, so the "No connection" fix
5404
5424
  // survives in classic mode too — this restores the old LOOK, not the old bug. The per-type words are the
@@ -5415,11 +5435,11 @@ class messageDialog {
5415
5435
  this.dialogRef.close(resp);
5416
5436
  }
5417
5437
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: messageDialog, deps: [{ token: i4.MatDialogRef }, { token: MAT_DIALOG_DATA }, { token: DataServiceLib, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
5418
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: messageDialog, isStandalone: false, selector: "lib-app-message", ngImport: i0, template: "<!-- Changed: two presentations behind one component. `modern` (appConfig.dialogStyle === 'modern') renders the\n structured layout; anything else renders the long-standing look, so an app that never opts in still gets a\n familiar dialog. Changed: classic is a SUPPORTED escape hatch, not frozen legacy \u2014 it is expected to render\n the same information as modern, and its three rendering bugs were repaired on 2026-08-06. Button ids\n (btnYes / btnNo / btnOK) are identical in BOTH branches \u2014 existing E2E specs select on them and must not\n care which style is active. -->\n\n<ng-container *ngIf=\"modern; else classic\">\n\n <!-- Rebuilt from a hardcoded per-type heading plus one undifferentiated paragraph into a structured layout \u2014\n icon chip, real title, lead paragraph, supporting detail. The type drives colour via a single class on\n the root, so adding a type is one entry in the component rather than three template branches. -->\n <div class=\"tin-msg\" [ngClass]=\"'tin-msg--' + messageType\">\n\n <div class=\"tin-msg__head\">\n <!-- The icon carries the type at a glance. It sits in a soft tinted disc rather than on a saturated\n banner: the intent is to inform, not to alarm someone who has usually done nothing wrong. -->\n <div class=\"tin-msg__icon\">\n <mat-icon>{{ iconName }}</mat-icon>\n </div>\n <h2 class=\"tin-msg__title\">{{ title }}</h2>\n </div>\n\n <mat-dialog-content class=\"tin-msg__body\">\n <!-- First paragraph is the lead (what happened); the rest is supporting detail (what to do next).\n Splitting on blank lines is what gives the eye somewhere to land \u2014 the old single run of text is\n why nobody read past the first line. -->\n <p *ngFor=\"let p of paragraphs; let first = first\" class=\"tin-msg__p\" [class.tin-msg__p--lead]=\"first\">{{ p }}</p>\n\n <!-- The backend's correlation code. Deliberately the quietest thing in the dialog \u2014 the user does not\n need to act on it, they only need to be able to read it out if they call support, at which point it\n turns a \"something broke this morning\" report into one log row. Hidden entirely when absent. -->\n <p class=\"tin-msg__ref\" *ngIf=\"reference\">If you contact your administrator, quote reference <span class=\"tin-msg__ref-code\">{{ reference }}</span>.</p>\n </mat-dialog-content>\n\n <mat-dialog-actions class=\"tin-msg__actions\">\n <!-- Changed: labels reverted to \"No\" / \"Yes\" (owner's call, 2026-08-06). The redesign had shipped them as\n \"Cancel\" / \"Yes, continue\"; these are the most-clicked buttons in the app and the original wording is\n what everybody here reads. Text only \u2014 the ids btnNo / btnYes are untouched and identical to the\n classic branch, so every E2E selector (#btnYes) and every caller is unaffected. -->\n <ng-container *ngIf=\"isConfirm; else modernAcknowledge\">\n <button id=\"btnNo\" mat-button class=\"tin-msg__btn-quiet\" (click)=\"response('no')\">No</button>\n <button id=\"btnYes\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('yes')\" cdkFocusInitial>Yes</button>\n </ng-container>\n <ng-template #modernAcknowledge>\n <button id=\"btnOK\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('ok')\" cdkFocusInitial>OK</button>\n </ng-template>\n </mat-dialog-actions>\n\n </div>\n\n</ng-container>\n\n\n<!-- Classic: the original LOOK, kept deliberately as the escape hatch (owner's call, 2026-08-06 \u2014 retire was\n recommended and declined). It is the original markup plus FIXES, never style changes, because reverting a\n bug alongside a look would be a regression. It renders the same INFORMATION as the modern branch, in its\n own visual language:\n 1. the caller's subject is used as the heading when one was supplied. The old template hardcoded the\n heading per type and rendered the subject for 'info' only, which is why a friendly title like\n \"No connection\" showed up as the bare word \"Error\".\n 2. 'warning' and 'success' are handled. They did not exist when this markup was written, so they would\n otherwise fall through every branch and render an empty dialog.\n 3. 'error' now has an icon; the reference line is no longer trapped in the error-only branch; and the\n body renders real paragraphs instead of one collapsed run of text.\n \u26A0\uFE0F Anything added to the modern branch above has to be considered here too \u2014 that omission is exactly how\n the three bugs in 3. got here. -->\n\n<ng-template #classic>\n <div class=\"tin-msg-classic\">\n\n <h2>\n <div class=\"tin-between\">\n <mat-label>{{ classicHeading }}</mat-label>\n <!-- Added: the missing 'error' icon. Every other type had one; error \u2014 the type that most needs a cue \u2014\n rendered its heading beside empty flex space. Colour matches the modern branch's error accent\n (#c62828) so the two presentations agree on what \"error\" looks like. -->\n <mat-icon *ngIf=\"messageType=='error'\" style=\"color: #c62828;\">error</mat-icon>\n <mat-icon *ngIf=\"messageType=='confirm'\">question_mark</mat-icon>\n <mat-icon *ngIf=\"messageType=='info'\" style=\"color: steelblue;\">info</mat-icon>\n <mat-icon *ngIf=\"messageType=='warning'\" style=\"color: #ef6c00;\">warning</mat-icon>\n <mat-icon *ngIf=\"messageType=='success'\" style=\"color: #2e7d32;\">check_circle</mat-icon>\n </div>\n </h2>\n\n <mat-dialog-content>\n\n <!-- Changed: was `{{ _messageDetails }}` rendered raw, so the blank-line-separated paragraphs the messages\n are actually written in (api-error.service.ts) collapsed into one undifferentiated run of text. That\n collapse is the original complaint that started WS-7 and it was still live here. Now renders the same\n `paragraphs` split the modern branch uses \u2014 same information, classic's own plain markup. -->\n <div *ngIf=\"messageType=='error'\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <div *ngIf=\"messageType!='error'\" class=\"alert alert-secondary\" role=\"alert\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <!-- Changed: lifted OUT of the error-only branch above. The reference is a correlation code, not an\n error-specific field \u2014 a warning or info that carried one used to drop it silently. -->\n <div class=\"tin-msg-classic__ref\" *ngIf=\"reference\">Reference: {{ reference }}</div>\n\n </mat-dialog-content>\n\n <mat-dialog-actions>\n\n <button id=\"btnYes\" mat-stroked-button style=\"color: green;\" *ngIf=\"isConfirm\" (click)=\"response('yes')\" cdkFocusInitial>Yes</button>\n\n <button id=\"btnNo\" mat-stroked-button style=\"color: red;\" *ngIf=\"isConfirm\" (click)=\"response('no')\">No</button>\n\n <button id=\"btnOK\" mat-stroked-button *ngIf=\"!isConfirm\" color=\"primary\" (click)=\"response('ok')\" cdkFocusInitial>OK</button>\n\n </mat-dialog-actions>\n\n </div>\n</ng-template>\n", styles: [":host{display:block}.tin-msg-classic h2{margin:0!important;padding:16px 16px 0!important;font-size:20px!important;font-weight:500!important;line-height:normal!important}.tin-msg-classic .tin-between{display:flex;justify-content:space-between;align-items:center;margin:0!important;padding:0!important}.tin-msg-classic__p{margin:0 0 10px}.tin-msg-classic__p:last-child{margin-bottom:0}.tin-msg-classic__ref{margin-top:10px;font-size:12px;color:#0000008c;letter-spacing:.04em}.tin-msg{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1);--tin-title: rgba(0, 0, 0, .87);--tin-body: rgba(0, 0, 0, .6);position:relative;padding:4px 4px 0}.tin-msg:before{content:\"\";position:absolute;top:0;left:0;right:0;height:3px;background:var(--tin-accent);border-radius:3px 3px 0 0}.tin-msg--error{--tin-accent: #c62828;--tin-accent-tint: rgba(198, 40, 40, .1)}.tin-msg--warning{--tin-accent: #ef6c00;--tin-accent-tint: rgba(239, 108, 0, .12)}.tin-msg--info{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1)}.tin-msg--success{--tin-accent: #2e7d32;--tin-accent-tint: rgba(46, 125, 50, .12)}.tin-msg--confirm{--tin-accent: #5e35b1;--tin-accent-tint: rgba(94, 53, 177, .1)}.tin-msg__head{display:flex;align-items:center;gap:14px;padding:20px 20px 8px}.tin-msg__icon{flex:0 0 auto;width:40px;height:40px;border-radius:50%;background:var(--tin-accent-tint);display:inline-flex;align-items:center;justify-content:center}.tin-msg__icon .mat-icon,.tin-msg__icon mat-icon{color:var(--tin-accent);font-size:22px;width:22px;height:22px;line-height:22px}.tin-msg__title{margin:0!important;padding:0!important;font-size:18px!important;font-weight:600!important;line-height:1.3!important;letter-spacing:-.1px;color:var(--tin-title)}.tin-msg__body{padding:0 20px 4px 74px!important;max-height:60vh}.tin-msg__p{margin:0 0 12px;font-size:13.5px;line-height:1.6;color:var(--tin-body)}.tin-msg__p--lead{font-size:14.5px;font-weight:500;color:var(--tin-title)}.tin-msg__p:last-child{margin-bottom:0}.tin-msg__ref{margin:14px 0 0;padding-top:10px;border-top:1px solid rgba(0,0,0,.08);font-size:12px;line-height:1.5;color:var(--tin-muted, rgba(0, 0, 0, .55))}.tin-msg__ref-code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;letter-spacing:.06em;color:var(--tin-title, rgba(0, 0, 0, .8));white-space:nowrap;-webkit-user-select:all;user-select:all}.tin-msg__actions{display:flex;justify-content:flex-end;gap:8px;padding:12px 20px 16px!important;margin:0!important;min-height:0!important}.tin-msg__btn-primary{background:var(--tin-accent)!important;color:#fff!important;min-width:88px;font-weight:500;letter-spacing:.2px}.tin-msg__btn-quiet{color:var(--tin-body)!important;font-weight:500}.tin-msg__icon{animation:tin-msg-pop .22s cubic-bezier(.2,.7,.3,1) both}@keyframes tin-msg-pop{0%{transform:scale(.82);opacity:0}to{transform:scale(1);opacity:1}}@media (prefers-reduced-motion: reduce){.tin-msg__icon{animation:none}}@media (max-width: 480px){.tin-msg__head{padding:16px 16px 8px;gap:12px}.tin-msg__body{padding:0 16px 4px!important}.tin-msg__actions{padding:12px 16px 14px!important}}\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: "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: "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: i3$1.MatLabel, selector: "mat-label" }] }); }
5438
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: messageDialog, isStandalone: false, selector: "lib-app-message", ngImport: i0, template: "<!-- Changed: two presentations behind one component. `modern` (appConfig.dialogStyle === 'modern') renders the\n structured layout; anything else renders the long-standing look, so an app that never opts in still gets a\n familiar dialog. Changed: classic is a SUPPORTED escape hatch, not frozen legacy \u2014 it is expected to render\n the same information as modern, and its three rendering bugs were repaired on 2026-08-06. Button ids\n (btnYes / btnNo / btnOK) are identical in BOTH branches \u2014 existing E2E specs select on them and must not\n care which style is active. -->\n\n<ng-container *ngIf=\"modern; else classic\">\n\n <!-- Rebuilt from a hardcoded per-type heading plus one undifferentiated paragraph into a structured layout \u2014\n icon chip, real title, lead paragraph, supporting detail. The type drives colour via a single class on\n the root, so adding a type is one entry in the component rather than three template branches. -->\n <div class=\"tin-msg\" [ngClass]=\"'tin-msg--' + messageType\">\n\n <div class=\"tin-msg__head\">\n <!-- The icon carries the type at a glance. It sits in a soft tinted disc rather than on a saturated\n banner: the intent is to inform, not to alarm someone who has usually done nothing wrong. -->\n <div class=\"tin-msg__icon\">\n <mat-icon>{{ iconName }}</mat-icon>\n </div>\n <h2 class=\"tin-msg__title\">{{ title }}</h2>\n </div>\n\n <mat-dialog-content class=\"tin-msg__body\">\n <!-- First paragraph is the lead (what happened); the rest is supporting detail (what to do next).\n Splitting on blank lines is what gives the eye somewhere to land \u2014 the old single run of text is\n why nobody read past the first line. -->\n <p *ngFor=\"let p of paragraphs; let first = first\" class=\"tin-msg__p\" [class.tin-msg__p--lead]=\"first\">{{ p }}</p>\n\n <!-- The backend's correlation code. Deliberately the quietest thing in the dialog \u2014 the user does not\n need to act on it, they only need to be able to read it out if they call support, at which point it\n turns a \"something broke this morning\" report into one log row. Hidden entirely when absent. -->\n <p class=\"tin-msg__ref\" *ngIf=\"reference\">If you contact your administrator, quote reference <span class=\"tin-msg__ref-code\">{{ reference }}</span>.</p>\n </mat-dialog-content>\n\n <mat-dialog-actions class=\"tin-msg__actions\">\n <!-- Changed: labels reverted to \"No\" / \"Yes\" (owner's call, 2026-08-06). The redesign had shipped them as\n \"Cancel\" / \"Yes, continue\"; these are the most-clicked buttons in the app and the original wording is\n what everybody here reads. Text only \u2014 the ids btnNo / btnYes are untouched and identical to the\n classic branch, so every E2E selector (#btnYes) and every caller is unaffected.\n Changed: the literal text is now {{ confirmLabel }} / {{ cancelLabel }} / {{ okLabel }}, which STILL\n render \"Yes\" / \"No\" / \"OK\" unless the caller passes a label. A caller that knows it is confirming a\n destructive action can name it (\"Delete\") so it no longer reads identically to a harmless one. -->\n <ng-container *ngIf=\"isConfirm; else modernAcknowledge\">\n <button id=\"btnNo\" mat-button class=\"tin-msg__btn-quiet\" (click)=\"response('no')\">{{ cancelLabel }}</button>\n <button id=\"btnYes\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('yes')\" cdkFocusInitial>{{ confirmLabel }}</button>\n </ng-container>\n <ng-template #modernAcknowledge>\n <button id=\"btnOK\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('ok')\" cdkFocusInitial>{{ okLabel }}</button>\n </ng-template>\n </mat-dialog-actions>\n\n </div>\n\n</ng-container>\n\n\n<!-- Classic: the original LOOK, kept deliberately as the escape hatch (owner's call, 2026-08-06 \u2014 retire was\n recommended and declined). It is the original markup plus FIXES, never style changes, because reverting a\n bug alongside a look would be a regression. It renders the same INFORMATION as the modern branch, in its\n own visual language:\n 1. the caller's subject is used as the heading when one was supplied. The old template hardcoded the\n heading per type and rendered the subject for 'info' only, which is why a friendly title like\n \"No connection\" showed up as the bare word \"Error\".\n 2. 'warning' and 'success' are handled. They did not exist when this markup was written, so they would\n otherwise fall through every branch and render an empty dialog.\n 3. 'error' now has an icon; the reference line is no longer trapped in the error-only branch; and the\n body renders real paragraphs instead of one collapsed run of text.\n \u26A0\uFE0F Anything added to the modern branch above has to be considered here too \u2014 that omission is exactly how\n the three bugs in 3. got here. -->\n\n<ng-template #classic>\n <div class=\"tin-msg-classic\">\n\n <h2>\n <div class=\"tin-between\">\n <mat-label>{{ classicHeading }}</mat-label>\n <!-- Added: the missing 'error' icon. Every other type had one; error \u2014 the type that most needs a cue \u2014\n rendered its heading beside empty flex space. Colour matches the modern branch's error accent\n (#c62828) so the two presentations agree on what \"error\" looks like. -->\n <mat-icon *ngIf=\"messageType=='error'\" style=\"color: #c62828;\">error</mat-icon>\n <mat-icon *ngIf=\"messageType=='confirm'\">question_mark</mat-icon>\n <mat-icon *ngIf=\"messageType=='info'\" style=\"color: steelblue;\">info</mat-icon>\n <mat-icon *ngIf=\"messageType=='warning'\" style=\"color: #ef6c00;\">warning</mat-icon>\n <mat-icon *ngIf=\"messageType=='success'\" style=\"color: #2e7d32;\">check_circle</mat-icon>\n </div>\n </h2>\n\n <mat-dialog-content>\n\n <!-- Changed: was `{{ _messageDetails }}` rendered raw, so the blank-line-separated paragraphs the messages\n are actually written in (api-error.service.ts) collapsed into one undifferentiated run of text. That\n collapse is the original complaint that started WS-7 and it was still live here. Now renders the same\n `paragraphs` split the modern branch uses \u2014 same information, classic's own plain markup. -->\n <div *ngIf=\"messageType=='error'\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <div *ngIf=\"messageType!='error'\" class=\"alert alert-secondary\" role=\"alert\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <!-- Changed: lifted OUT of the error-only branch above. The reference is a correlation code, not an\n error-specific field \u2014 a warning or info that carried one used to drop it silently. -->\n <div class=\"tin-msg-classic__ref\" *ngIf=\"reference\">Reference: {{ reference }}</div>\n\n </mat-dialog-content>\n\n <mat-dialog-actions>\n\n <!-- Changed: same caller-supplied labels as the modern branch, defaulting identically to \"Yes\" / \"No\" /\n \"OK\". Classic is expected to render the same INFORMATION as modern in its own visual language, so a\n caller that names its action must be honoured here too \u2014 otherwise the next person to pass a label\n would silently get \"Yes\" on every untracked consumer still running classic. Colours and ids untouched. -->\n <button id=\"btnYes\" mat-stroked-button style=\"color: green;\" *ngIf=\"isConfirm\" (click)=\"response('yes')\" cdkFocusInitial>{{ confirmLabel }}</button>\n\n <button id=\"btnNo\" mat-stroked-button style=\"color: red;\" *ngIf=\"isConfirm\" (click)=\"response('no')\">{{ cancelLabel }}</button>\n\n <button id=\"btnOK\" mat-stroked-button *ngIf=\"!isConfirm\" color=\"primary\" (click)=\"response('ok')\" cdkFocusInitial>{{ okLabel }}</button>\n\n </mat-dialog-actions>\n\n </div>\n</ng-template>\n", styles: [":host{display:block}.tin-msg-classic h2{margin:0!important;padding:16px 16px 0!important;font-size:20px!important;font-weight:500!important;line-height:normal!important}.tin-msg-classic .tin-between{display:flex;justify-content:space-between;align-items:center;margin:0!important;padding:0!important}.tin-msg-classic__p{margin:0 0 10px}.tin-msg-classic__p:last-child{margin-bottom:0}.tin-msg-classic__ref{margin-top:10px;font-size:12px;color:#0000008c;letter-spacing:.04em}.tin-msg{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1);--tin-title: rgba(0, 0, 0, .87);--tin-body: rgba(0, 0, 0, .6);position:relative;padding:4px 4px 0}.tin-msg:before{content:\"\";position:absolute;top:0;left:0;right:0;height:3px;background:var(--tin-accent);border-radius:3px 3px 0 0}.tin-msg--error{--tin-accent: #c62828;--tin-accent-tint: rgba(198, 40, 40, .1)}.tin-msg--warning{--tin-accent: #ef6c00;--tin-accent-tint: rgba(239, 108, 0, .12)}.tin-msg--info{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1)}.tin-msg--success{--tin-accent: #2e7d32;--tin-accent-tint: rgba(46, 125, 50, .12)}.tin-msg--confirm{--tin-accent: #5e35b1;--tin-accent-tint: rgba(94, 53, 177, .1)}.tin-msg__head{display:flex;align-items:center;gap:14px;padding:20px 20px 8px}.tin-msg__icon{flex:0 0 auto;width:40px;height:40px;border-radius:50%;background:var(--tin-accent-tint);display:inline-flex;align-items:center;justify-content:center}.tin-msg__icon .mat-icon,.tin-msg__icon mat-icon{color:var(--tin-accent);font-size:22px;width:22px;height:22px;line-height:22px}.tin-msg__title{margin:0!important;padding:0!important;font-size:18px!important;font-weight:600!important;line-height:1.3!important;letter-spacing:-.1px;color:var(--tin-title)}.tin-msg__body{padding:0 20px 4px 74px!important;max-height:60vh}.tin-msg__p{margin:0 0 12px;font-size:13.5px;line-height:1.6;color:var(--tin-body)}.tin-msg__p--lead{font-size:14.5px;font-weight:500;color:var(--tin-title)}.tin-msg__p:last-child{margin-bottom:0}.tin-msg__ref{margin:14px 0 0;padding-top:10px;border-top:1px solid rgba(0,0,0,.08);font-size:12px;line-height:1.5;color:var(--tin-muted, rgba(0, 0, 0, .55))}.tin-msg__ref-code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;letter-spacing:.06em;color:var(--tin-title, rgba(0, 0, 0, .8));white-space:nowrap;-webkit-user-select:all;user-select:all}.tin-msg__actions{display:flex;justify-content:flex-end;gap:8px;padding:12px 20px 16px!important;margin:0!important;min-height:0!important}.tin-msg__btn-primary{background:var(--tin-accent)!important;color:#fff!important;min-width:88px;font-weight:500;letter-spacing:.2px}.tin-msg__btn-quiet{color:var(--tin-body)!important;font-weight:500}.tin-msg__icon{animation:tin-msg-pop .22s cubic-bezier(.2,.7,.3,1) both}@keyframes tin-msg-pop{0%{transform:scale(.82);opacity:0}to{transform:scale(1);opacity:1}}@media (prefers-reduced-motion: reduce){.tin-msg__icon{animation:none}}@media (max-width: 480px){.tin-msg__head{padding:16px 16px 8px;gap:12px}.tin-msg__body{padding:0 16px 4px!important}.tin-msg__actions{padding:12px 16px 14px!important}}\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: "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: "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: i3$1.MatLabel, selector: "mat-label" }] }); }
5419
5439
  }
5420
5440
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: messageDialog, decorators: [{
5421
5441
  type: Component,
5422
- args: [{ selector: 'lib-app-message', standalone: false, template: "<!-- Changed: two presentations behind one component. `modern` (appConfig.dialogStyle === 'modern') renders the\n structured layout; anything else renders the long-standing look, so an app that never opts in still gets a\n familiar dialog. Changed: classic is a SUPPORTED escape hatch, not frozen legacy \u2014 it is expected to render\n the same information as modern, and its three rendering bugs were repaired on 2026-08-06. Button ids\n (btnYes / btnNo / btnOK) are identical in BOTH branches \u2014 existing E2E specs select on them and must not\n care which style is active. -->\n\n<ng-container *ngIf=\"modern; else classic\">\n\n <!-- Rebuilt from a hardcoded per-type heading plus one undifferentiated paragraph into a structured layout \u2014\n icon chip, real title, lead paragraph, supporting detail. The type drives colour via a single class on\n the root, so adding a type is one entry in the component rather than three template branches. -->\n <div class=\"tin-msg\" [ngClass]=\"'tin-msg--' + messageType\">\n\n <div class=\"tin-msg__head\">\n <!-- The icon carries the type at a glance. It sits in a soft tinted disc rather than on a saturated\n banner: the intent is to inform, not to alarm someone who has usually done nothing wrong. -->\n <div class=\"tin-msg__icon\">\n <mat-icon>{{ iconName }}</mat-icon>\n </div>\n <h2 class=\"tin-msg__title\">{{ title }}</h2>\n </div>\n\n <mat-dialog-content class=\"tin-msg__body\">\n <!-- First paragraph is the lead (what happened); the rest is supporting detail (what to do next).\n Splitting on blank lines is what gives the eye somewhere to land \u2014 the old single run of text is\n why nobody read past the first line. -->\n <p *ngFor=\"let p of paragraphs; let first = first\" class=\"tin-msg__p\" [class.tin-msg__p--lead]=\"first\">{{ p }}</p>\n\n <!-- The backend's correlation code. Deliberately the quietest thing in the dialog \u2014 the user does not\n need to act on it, they only need to be able to read it out if they call support, at which point it\n turns a \"something broke this morning\" report into one log row. Hidden entirely when absent. -->\n <p class=\"tin-msg__ref\" *ngIf=\"reference\">If you contact your administrator, quote reference <span class=\"tin-msg__ref-code\">{{ reference }}</span>.</p>\n </mat-dialog-content>\n\n <mat-dialog-actions class=\"tin-msg__actions\">\n <!-- Changed: labels reverted to \"No\" / \"Yes\" (owner's call, 2026-08-06). The redesign had shipped them as\n \"Cancel\" / \"Yes, continue\"; these are the most-clicked buttons in the app and the original wording is\n what everybody here reads. Text only \u2014 the ids btnNo / btnYes are untouched and identical to the\n classic branch, so every E2E selector (#btnYes) and every caller is unaffected. -->\n <ng-container *ngIf=\"isConfirm; else modernAcknowledge\">\n <button id=\"btnNo\" mat-button class=\"tin-msg__btn-quiet\" (click)=\"response('no')\">No</button>\n <button id=\"btnYes\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('yes')\" cdkFocusInitial>Yes</button>\n </ng-container>\n <ng-template #modernAcknowledge>\n <button id=\"btnOK\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('ok')\" cdkFocusInitial>OK</button>\n </ng-template>\n </mat-dialog-actions>\n\n </div>\n\n</ng-container>\n\n\n<!-- Classic: the original LOOK, kept deliberately as the escape hatch (owner's call, 2026-08-06 \u2014 retire was\n recommended and declined). It is the original markup plus FIXES, never style changes, because reverting a\n bug alongside a look would be a regression. It renders the same INFORMATION as the modern branch, in its\n own visual language:\n 1. the caller's subject is used as the heading when one was supplied. The old template hardcoded the\n heading per type and rendered the subject for 'info' only, which is why a friendly title like\n \"No connection\" showed up as the bare word \"Error\".\n 2. 'warning' and 'success' are handled. They did not exist when this markup was written, so they would\n otherwise fall through every branch and render an empty dialog.\n 3. 'error' now has an icon; the reference line is no longer trapped in the error-only branch; and the\n body renders real paragraphs instead of one collapsed run of text.\n \u26A0\uFE0F Anything added to the modern branch above has to be considered here too \u2014 that omission is exactly how\n the three bugs in 3. got here. -->\n\n<ng-template #classic>\n <div class=\"tin-msg-classic\">\n\n <h2>\n <div class=\"tin-between\">\n <mat-label>{{ classicHeading }}</mat-label>\n <!-- Added: the missing 'error' icon. Every other type had one; error \u2014 the type that most needs a cue \u2014\n rendered its heading beside empty flex space. Colour matches the modern branch's error accent\n (#c62828) so the two presentations agree on what \"error\" looks like. -->\n <mat-icon *ngIf=\"messageType=='error'\" style=\"color: #c62828;\">error</mat-icon>\n <mat-icon *ngIf=\"messageType=='confirm'\">question_mark</mat-icon>\n <mat-icon *ngIf=\"messageType=='info'\" style=\"color: steelblue;\">info</mat-icon>\n <mat-icon *ngIf=\"messageType=='warning'\" style=\"color: #ef6c00;\">warning</mat-icon>\n <mat-icon *ngIf=\"messageType=='success'\" style=\"color: #2e7d32;\">check_circle</mat-icon>\n </div>\n </h2>\n\n <mat-dialog-content>\n\n <!-- Changed: was `{{ _messageDetails }}` rendered raw, so the blank-line-separated paragraphs the messages\n are actually written in (api-error.service.ts) collapsed into one undifferentiated run of text. That\n collapse is the original complaint that started WS-7 and it was still live here. Now renders the same\n `paragraphs` split the modern branch uses \u2014 same information, classic's own plain markup. -->\n <div *ngIf=\"messageType=='error'\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <div *ngIf=\"messageType!='error'\" class=\"alert alert-secondary\" role=\"alert\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <!-- Changed: lifted OUT of the error-only branch above. The reference is a correlation code, not an\n error-specific field \u2014 a warning or info that carried one used to drop it silently. -->\n <div class=\"tin-msg-classic__ref\" *ngIf=\"reference\">Reference: {{ reference }}</div>\n\n </mat-dialog-content>\n\n <mat-dialog-actions>\n\n <button id=\"btnYes\" mat-stroked-button style=\"color: green;\" *ngIf=\"isConfirm\" (click)=\"response('yes')\" cdkFocusInitial>Yes</button>\n\n <button id=\"btnNo\" mat-stroked-button style=\"color: red;\" *ngIf=\"isConfirm\" (click)=\"response('no')\">No</button>\n\n <button id=\"btnOK\" mat-stroked-button *ngIf=\"!isConfirm\" color=\"primary\" (click)=\"response('ok')\" cdkFocusInitial>OK</button>\n\n </mat-dialog-actions>\n\n </div>\n</ng-template>\n", styles: [":host{display:block}.tin-msg-classic h2{margin:0!important;padding:16px 16px 0!important;font-size:20px!important;font-weight:500!important;line-height:normal!important}.tin-msg-classic .tin-between{display:flex;justify-content:space-between;align-items:center;margin:0!important;padding:0!important}.tin-msg-classic__p{margin:0 0 10px}.tin-msg-classic__p:last-child{margin-bottom:0}.tin-msg-classic__ref{margin-top:10px;font-size:12px;color:#0000008c;letter-spacing:.04em}.tin-msg{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1);--tin-title: rgba(0, 0, 0, .87);--tin-body: rgba(0, 0, 0, .6);position:relative;padding:4px 4px 0}.tin-msg:before{content:\"\";position:absolute;top:0;left:0;right:0;height:3px;background:var(--tin-accent);border-radius:3px 3px 0 0}.tin-msg--error{--tin-accent: #c62828;--tin-accent-tint: rgba(198, 40, 40, .1)}.tin-msg--warning{--tin-accent: #ef6c00;--tin-accent-tint: rgba(239, 108, 0, .12)}.tin-msg--info{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1)}.tin-msg--success{--tin-accent: #2e7d32;--tin-accent-tint: rgba(46, 125, 50, .12)}.tin-msg--confirm{--tin-accent: #5e35b1;--tin-accent-tint: rgba(94, 53, 177, .1)}.tin-msg__head{display:flex;align-items:center;gap:14px;padding:20px 20px 8px}.tin-msg__icon{flex:0 0 auto;width:40px;height:40px;border-radius:50%;background:var(--tin-accent-tint);display:inline-flex;align-items:center;justify-content:center}.tin-msg__icon .mat-icon,.tin-msg__icon mat-icon{color:var(--tin-accent);font-size:22px;width:22px;height:22px;line-height:22px}.tin-msg__title{margin:0!important;padding:0!important;font-size:18px!important;font-weight:600!important;line-height:1.3!important;letter-spacing:-.1px;color:var(--tin-title)}.tin-msg__body{padding:0 20px 4px 74px!important;max-height:60vh}.tin-msg__p{margin:0 0 12px;font-size:13.5px;line-height:1.6;color:var(--tin-body)}.tin-msg__p--lead{font-size:14.5px;font-weight:500;color:var(--tin-title)}.tin-msg__p:last-child{margin-bottom:0}.tin-msg__ref{margin:14px 0 0;padding-top:10px;border-top:1px solid rgba(0,0,0,.08);font-size:12px;line-height:1.5;color:var(--tin-muted, rgba(0, 0, 0, .55))}.tin-msg__ref-code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;letter-spacing:.06em;color:var(--tin-title, rgba(0, 0, 0, .8));white-space:nowrap;-webkit-user-select:all;user-select:all}.tin-msg__actions{display:flex;justify-content:flex-end;gap:8px;padding:12px 20px 16px!important;margin:0!important;min-height:0!important}.tin-msg__btn-primary{background:var(--tin-accent)!important;color:#fff!important;min-width:88px;font-weight:500;letter-spacing:.2px}.tin-msg__btn-quiet{color:var(--tin-body)!important;font-weight:500}.tin-msg__icon{animation:tin-msg-pop .22s cubic-bezier(.2,.7,.3,1) both}@keyframes tin-msg-pop{0%{transform:scale(.82);opacity:0}to{transform:scale(1);opacity:1}}@media (prefers-reduced-motion: reduce){.tin-msg__icon{animation:none}}@media (max-width: 480px){.tin-msg__head{padding:16px 16px 8px;gap:12px}.tin-msg__body{padding:0 16px 4px!important}.tin-msg__actions{padding:12px 16px 14px!important}}\n"] }]
5442
+ args: [{ selector: 'lib-app-message', standalone: false, template: "<!-- Changed: two presentations behind one component. `modern` (appConfig.dialogStyle === 'modern') renders the\n structured layout; anything else renders the long-standing look, so an app that never opts in still gets a\n familiar dialog. Changed: classic is a SUPPORTED escape hatch, not frozen legacy \u2014 it is expected to render\n the same information as modern, and its three rendering bugs were repaired on 2026-08-06. Button ids\n (btnYes / btnNo / btnOK) are identical in BOTH branches \u2014 existing E2E specs select on them and must not\n care which style is active. -->\n\n<ng-container *ngIf=\"modern; else classic\">\n\n <!-- Rebuilt from a hardcoded per-type heading plus one undifferentiated paragraph into a structured layout \u2014\n icon chip, real title, lead paragraph, supporting detail. The type drives colour via a single class on\n the root, so adding a type is one entry in the component rather than three template branches. -->\n <div class=\"tin-msg\" [ngClass]=\"'tin-msg--' + messageType\">\n\n <div class=\"tin-msg__head\">\n <!-- The icon carries the type at a glance. It sits in a soft tinted disc rather than on a saturated\n banner: the intent is to inform, not to alarm someone who has usually done nothing wrong. -->\n <div class=\"tin-msg__icon\">\n <mat-icon>{{ iconName }}</mat-icon>\n </div>\n <h2 class=\"tin-msg__title\">{{ title }}</h2>\n </div>\n\n <mat-dialog-content class=\"tin-msg__body\">\n <!-- First paragraph is the lead (what happened); the rest is supporting detail (what to do next).\n Splitting on blank lines is what gives the eye somewhere to land \u2014 the old single run of text is\n why nobody read past the first line. -->\n <p *ngFor=\"let p of paragraphs; let first = first\" class=\"tin-msg__p\" [class.tin-msg__p--lead]=\"first\">{{ p }}</p>\n\n <!-- The backend's correlation code. Deliberately the quietest thing in the dialog \u2014 the user does not\n need to act on it, they only need to be able to read it out if they call support, at which point it\n turns a \"something broke this morning\" report into one log row. Hidden entirely when absent. -->\n <p class=\"tin-msg__ref\" *ngIf=\"reference\">If you contact your administrator, quote reference <span class=\"tin-msg__ref-code\">{{ reference }}</span>.</p>\n </mat-dialog-content>\n\n <mat-dialog-actions class=\"tin-msg__actions\">\n <!-- Changed: labels reverted to \"No\" / \"Yes\" (owner's call, 2026-08-06). The redesign had shipped them as\n \"Cancel\" / \"Yes, continue\"; these are the most-clicked buttons in the app and the original wording is\n what everybody here reads. Text only \u2014 the ids btnNo / btnYes are untouched and identical to the\n classic branch, so every E2E selector (#btnYes) and every caller is unaffected.\n Changed: the literal text is now {{ confirmLabel }} / {{ cancelLabel }} / {{ okLabel }}, which STILL\n render \"Yes\" / \"No\" / \"OK\" unless the caller passes a label. A caller that knows it is confirming a\n destructive action can name it (\"Delete\") so it no longer reads identically to a harmless one. -->\n <ng-container *ngIf=\"isConfirm; else modernAcknowledge\">\n <button id=\"btnNo\" mat-button class=\"tin-msg__btn-quiet\" (click)=\"response('no')\">{{ cancelLabel }}</button>\n <button id=\"btnYes\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('yes')\" cdkFocusInitial>{{ confirmLabel }}</button>\n </ng-container>\n <ng-template #modernAcknowledge>\n <button id=\"btnOK\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('ok')\" cdkFocusInitial>{{ okLabel }}</button>\n </ng-template>\n </mat-dialog-actions>\n\n </div>\n\n</ng-container>\n\n\n<!-- Classic: the original LOOK, kept deliberately as the escape hatch (owner's call, 2026-08-06 \u2014 retire was\n recommended and declined). It is the original markup plus FIXES, never style changes, because reverting a\n bug alongside a look would be a regression. It renders the same INFORMATION as the modern branch, in its\n own visual language:\n 1. the caller's subject is used as the heading when one was supplied. The old template hardcoded the\n heading per type and rendered the subject for 'info' only, which is why a friendly title like\n \"No connection\" showed up as the bare word \"Error\".\n 2. 'warning' and 'success' are handled. They did not exist when this markup was written, so they would\n otherwise fall through every branch and render an empty dialog.\n 3. 'error' now has an icon; the reference line is no longer trapped in the error-only branch; and the\n body renders real paragraphs instead of one collapsed run of text.\n \u26A0\uFE0F Anything added to the modern branch above has to be considered here too \u2014 that omission is exactly how\n the three bugs in 3. got here. -->\n\n<ng-template #classic>\n <div class=\"tin-msg-classic\">\n\n <h2>\n <div class=\"tin-between\">\n <mat-label>{{ classicHeading }}</mat-label>\n <!-- Added: the missing 'error' icon. Every other type had one; error \u2014 the type that most needs a cue \u2014\n rendered its heading beside empty flex space. Colour matches the modern branch's error accent\n (#c62828) so the two presentations agree on what \"error\" looks like. -->\n <mat-icon *ngIf=\"messageType=='error'\" style=\"color: #c62828;\">error</mat-icon>\n <mat-icon *ngIf=\"messageType=='confirm'\">question_mark</mat-icon>\n <mat-icon *ngIf=\"messageType=='info'\" style=\"color: steelblue;\">info</mat-icon>\n <mat-icon *ngIf=\"messageType=='warning'\" style=\"color: #ef6c00;\">warning</mat-icon>\n <mat-icon *ngIf=\"messageType=='success'\" style=\"color: #2e7d32;\">check_circle</mat-icon>\n </div>\n </h2>\n\n <mat-dialog-content>\n\n <!-- Changed: was `{{ _messageDetails }}` rendered raw, so the blank-line-separated paragraphs the messages\n are actually written in (api-error.service.ts) collapsed into one undifferentiated run of text. That\n collapse is the original complaint that started WS-7 and it was still live here. Now renders the same\n `paragraphs` split the modern branch uses \u2014 same information, classic's own plain markup. -->\n <div *ngIf=\"messageType=='error'\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <div *ngIf=\"messageType!='error'\" class=\"alert alert-secondary\" role=\"alert\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <!-- Changed: lifted OUT of the error-only branch above. The reference is a correlation code, not an\n error-specific field \u2014 a warning or info that carried one used to drop it silently. -->\n <div class=\"tin-msg-classic__ref\" *ngIf=\"reference\">Reference: {{ reference }}</div>\n\n </mat-dialog-content>\n\n <mat-dialog-actions>\n\n <!-- Changed: same caller-supplied labels as the modern branch, defaulting identically to \"Yes\" / \"No\" /\n \"OK\". Classic is expected to render the same INFORMATION as modern in its own visual language, so a\n caller that names its action must be honoured here too \u2014 otherwise the next person to pass a label\n would silently get \"Yes\" on every untracked consumer still running classic. Colours and ids untouched. -->\n <button id=\"btnYes\" mat-stroked-button style=\"color: green;\" *ngIf=\"isConfirm\" (click)=\"response('yes')\" cdkFocusInitial>{{ confirmLabel }}</button>\n\n <button id=\"btnNo\" mat-stroked-button style=\"color: red;\" *ngIf=\"isConfirm\" (click)=\"response('no')\">{{ cancelLabel }}</button>\n\n <button id=\"btnOK\" mat-stroked-button *ngIf=\"!isConfirm\" color=\"primary\" (click)=\"response('ok')\" cdkFocusInitial>{{ okLabel }}</button>\n\n </mat-dialog-actions>\n\n </div>\n</ng-template>\n", styles: [":host{display:block}.tin-msg-classic h2{margin:0!important;padding:16px 16px 0!important;font-size:20px!important;font-weight:500!important;line-height:normal!important}.tin-msg-classic .tin-between{display:flex;justify-content:space-between;align-items:center;margin:0!important;padding:0!important}.tin-msg-classic__p{margin:0 0 10px}.tin-msg-classic__p:last-child{margin-bottom:0}.tin-msg-classic__ref{margin-top:10px;font-size:12px;color:#0000008c;letter-spacing:.04em}.tin-msg{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1);--tin-title: rgba(0, 0, 0, .87);--tin-body: rgba(0, 0, 0, .6);position:relative;padding:4px 4px 0}.tin-msg:before{content:\"\";position:absolute;top:0;left:0;right:0;height:3px;background:var(--tin-accent);border-radius:3px 3px 0 0}.tin-msg--error{--tin-accent: #c62828;--tin-accent-tint: rgba(198, 40, 40, .1)}.tin-msg--warning{--tin-accent: #ef6c00;--tin-accent-tint: rgba(239, 108, 0, .12)}.tin-msg--info{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1)}.tin-msg--success{--tin-accent: #2e7d32;--tin-accent-tint: rgba(46, 125, 50, .12)}.tin-msg--confirm{--tin-accent: #5e35b1;--tin-accent-tint: rgba(94, 53, 177, .1)}.tin-msg__head{display:flex;align-items:center;gap:14px;padding:20px 20px 8px}.tin-msg__icon{flex:0 0 auto;width:40px;height:40px;border-radius:50%;background:var(--tin-accent-tint);display:inline-flex;align-items:center;justify-content:center}.tin-msg__icon .mat-icon,.tin-msg__icon mat-icon{color:var(--tin-accent);font-size:22px;width:22px;height:22px;line-height:22px}.tin-msg__title{margin:0!important;padding:0!important;font-size:18px!important;font-weight:600!important;line-height:1.3!important;letter-spacing:-.1px;color:var(--tin-title)}.tin-msg__body{padding:0 20px 4px 74px!important;max-height:60vh}.tin-msg__p{margin:0 0 12px;font-size:13.5px;line-height:1.6;color:var(--tin-body)}.tin-msg__p--lead{font-size:14.5px;font-weight:500;color:var(--tin-title)}.tin-msg__p:last-child{margin-bottom:0}.tin-msg__ref{margin:14px 0 0;padding-top:10px;border-top:1px solid rgba(0,0,0,.08);font-size:12px;line-height:1.5;color:var(--tin-muted, rgba(0, 0, 0, .55))}.tin-msg__ref-code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;letter-spacing:.06em;color:var(--tin-title, rgba(0, 0, 0, .8));white-space:nowrap;-webkit-user-select:all;user-select:all}.tin-msg__actions{display:flex;justify-content:flex-end;gap:8px;padding:12px 20px 16px!important;margin:0!important;min-height:0!important}.tin-msg__btn-primary{background:var(--tin-accent)!important;color:#fff!important;min-width:88px;font-weight:500;letter-spacing:.2px}.tin-msg__btn-quiet{color:var(--tin-body)!important;font-weight:500}.tin-msg__icon{animation:tin-msg-pop .22s cubic-bezier(.2,.7,.3,1) both}@keyframes tin-msg-pop{0%{transform:scale(.82);opacity:0}to{transform:scale(1);opacity:1}}@media (prefers-reduced-motion: reduce){.tin-msg__icon{animation:none}}@media (max-width: 480px){.tin-msg__head{padding:16px 16px 8px;gap:12px}.tin-msg__body{padding:0 16px 4px!important}.tin-msg__actions{padding:12px 16px 14px!important}}\n"] }]
5423
5443
  }], ctorParameters: () => [{ type: i4.MatDialogRef }, { type: undefined, decorators: [{
5424
5444
  type: Inject,
5425
5445
  args: [MAT_DIALOG_DATA]
@@ -5492,13 +5512,20 @@ class MessageService {
5492
5512
  data: { type, subject, details },
5493
5513
  });
5494
5514
  }
5495
- confirm(msg) {
5515
+ // Changed: optional `confirmLabel` / `cancelLabel`, appended last so all 23 existing call sites are unaffected
5516
+ // and keep rendering "Yes" / "No". Supply confirmLabel to name the action being confirmed — confirm('Delete this
5517
+ // shelter?', 'Delete') — so a destructive prompt stops reading identically to a harmless one. Only the caller
5518
+ // knows the intent; the dialog is handed a sentence.
5519
+ // ⚠️ Do NOT change the defaults to make every confirm read better: piglet-spa (38) and shift-spa (13) select
5520
+ // these buttons by their literal text via getByRole('button', { name: 'Yes' }), and the owner reverted a
5521
+ // hardcoded relabel on 2026-08-06. Opt in per call site.
5522
+ confirm(msg, confirmLabel, cancelLabel) {
5496
5523
  let type = "confirm";
5497
5524
  let subject = "";
5498
5525
  let details = msg;
5499
5526
  const dialogRef = this.dialog.open(messageDialog, {
5500
5527
  width: "400px",
5501
- data: { type, subject, details },
5528
+ data: { type, subject, details, confirmLabel, cancelLabel },
5502
5529
  });
5503
5530
  return dialogRef.afterClosed().pipe(mergeMap((result) => {
5504
5531
  return of(result);
@@ -6365,7 +6392,7 @@ class InventoryService {
6365
6392
  minColumns: ['requisitionNumber', 'requesterName', 'requestDate'],
6366
6393
  columns: [
6367
6394
  { name: 'requisitionNumber', type: 'text', alias: 'Requisition #' },
6368
- { name: 'requesterName', type: 'text', alias: 'Requested By' },
6395
+ { name: 'requesterName', type: 'monogram', alias: 'Requested By' }, // Changed: monogram — the row still carries the full name, so filter/sort are untouched
6369
6396
  { name: 'departmentName', type: 'text', alias: 'Department' },
6370
6397
  { name: 'requestDate', type: 'date', alias: 'Request Date' },
6371
6398
  { name: 'issuedDate', type: 'date', alias: 'Issued Date' },
@@ -6931,31 +6958,39 @@ class AccountingService {
6931
6958
  formConfig: this.invoiceItemsFormConfig // Changed: Reference extracted form config
6932
6959
  };
6933
6960
  // Invoice action buttons
6934
- this.invoiceRecordPaymentButton = { name: 'record-payment', display: 'Record Payment', dialog: true, icon: { name: 'payment', color: 'blue' },
6961
+ // Changed: the hand-rolled cap53 gate is now declared, not written. Every custom action is floored at Edit on
6962
+ // the controller's own capability (BaseController.CustomActionAccess), and InvoicesController raises
6963
+ // return/discard/write-off/provision to Full (InvoicesController.cs:540-546). capAccountingInvoices IS cap53,
6964
+ // which is BOTH InvoicesController.CapabilityName and InvoicePaymentsController's — so these read the same
6965
+ // column the server reads. `visible` is back to being about the ROW; the capability is about the ROLE.
6966
+ // Changed: `record-payment` opens a create dialog posting to invoicepayments?action=create, and BaseController
6967
+ // demands RoleAccess.Create for "create" (BaseController.cs:751) — NOT the Edit floor. It was wholly ungated:
6968
+ // a cap53 Edit holder was offered it and refused by the server.
6969
+ this.invoiceRecordPaymentButton = { name: 'record-payment', display: 'Record Payment', dialog: true, icon: { name: 'payment', color: 'blue' }, capability: this.dataService.capAccountingInvoices, requiredAccess: RoleAccess.Create,
6935
6970
  detailsConfig: this.invoicePaymentsCreateDetailsDialogConfig,
6936
6971
  visible: x => (x.status == InvoiceStatus.Submitted || x.status == InvoiceStatus.Paying) && x.outstandingAmount > 0 // Changed: Allow on both Submitted and Paying
6937
6972
  }; // Changed: Use detailsConfig to open payment form dialog
6938
- this.invoiceDiscardButton = { name: 'discard', inDialog: true, display: 'Discard', icon: { name: 'close', color: 'red' },
6973
+ this.invoiceDiscardButton = { name: 'discard', inDialog: true, display: 'Discard', icon: { name: 'close', color: 'red' }, capability: this.dataService.capAccountingInvoices, requiredAccess: RoleAccess.Full,
6939
6974
  action: { url: 'invoices?action=discard', method: 'post', successMessage: 'Discarded' },
6940
6975
  confirm: { message: 'Invoice will be marked as cancelled?' },
6941
- visible: x => x.status == InvoiceStatus.Draft // Changed: Only allow on Draft (use Return for Submitted)
6976
+ visible: x => x.status == InvoiceStatus.Draft // Changed: `discard` is one of the four actions InvoicesController raises to Full — now declared above
6942
6977
  };
6943
6978
  // Changed: Return button — reverts submitted invoice back to draft, reverses accounting transactions
6944
- this.invoiceReturnButton = { name: 'return', inDialog: true, display: 'Return', icon: { name: 'undo', color: 'orange' },
6979
+ this.invoiceReturnButton = { name: 'return', inDialog: true, display: 'Return', icon: { name: 'undo', color: 'orange' }, capability: this.dataService.capAccountingInvoices, requiredAccess: RoleAccess.Full,
6945
6980
  action: { url: 'invoices?action=return', method: 'post', successMessage: 'Returned to Draft' },
6946
6981
  confirm: { message: 'Return invoice to draft? This will reverse the receivable and VAT transactions.' },
6947
- visible: x => x.status == InvoiceStatus.Submitted
6982
+ visible: x => x.status == InvoiceStatus.Submitted // Changed: `return` requires Full at the server — now declared above
6948
6983
  };
6949
6984
  // Changed: Write-off button — expenses outstanding balance as bad debt and closes the invoice (terminal WrittenOff status)
6950
- this.invoiceWriteOffButton = { name: 'write-off', inDialog: true, display: 'Write Off', icon: { name: 'money_off', color: 'red' },
6985
+ this.invoiceWriteOffButton = { name: 'write-off', inDialog: true, display: 'Write Off', icon: { name: 'money_off', color: 'red' }, capability: this.dataService.capAccountingInvoices, requiredAccess: RoleAccess.Full,
6951
6986
  action: { url: 'invoices?action=write-off', method: 'post' },
6952
6987
  confirm: { message: 'Outstanding balance will be expensed as bad debt and the invoice closed. This posts a GL journal.' },
6953
- visible: x => x.status == InvoiceStatus.Submitted || x.status == InvoiceStatus.Paying
6988
+ visible: x => (x.status == InvoiceStatus.Submitted || x.status == InvoiceStatus.Paying) // Changed: `write-off` requires Full at the server — now declared above
6954
6989
  };
6955
- this.invoiceSubmitButton = { name: 'submit', inDialog: true, display: 'Submit', icon: { name: 'send', },
6990
+ this.invoiceSubmitButton = { name: 'submit', inDialog: true, display: 'Submit', icon: { name: 'send', }, capability: this.dataService.capAccountingInvoices, requiredAccess: RoleAccess.Edit,
6956
6991
  action: { url: 'invoices?action=submit', method: 'post', successMessage: 'Submitted' },
6957
6992
  confirm: { message: 'Submit invoice? This will record revenue and create an accounts receivable entry.' },
6958
- visible: x => x.status == InvoiceStatus.Draft,
6993
+ visible: x => x.status == InvoiceStatus.Draft, // Changed: `submit` keeps the Edit floor — the comment on InvoicesController.CustomActionAccess says so explicitly
6959
6994
  disabled: x => x.totalAmount == 0
6960
6995
  };
6961
6996
  this.invoiceEditButton = { name: 'edit', display: 'Save', dialog: true, action: { url: 'invoices?action=edit', method: 'post', },
@@ -10754,10 +10789,28 @@ class ButtonService {
10754
10789
  getVisibleButtons(buttons, row, tableConfig) {
10755
10790
  return buttons.filter(button => this.testVisible(button, row, tableConfig));
10756
10791
  }
10792
+ // Added: the opt-in capability gate, on its own. Mirrors the server's real rule — a NAMED capability at a
10793
+ // NAMED level — because that is what BaseController checks (CustomActionAccess: Edit on the controller's own
10794
+ // CapabilityName). Reads the same role object the configs already read by hand: dataService.myRole is fed
10795
+ // from authService.myRoleObserv (datalib.service.ts:569), which is currentRoleSource.asObservable() — one
10796
+ // source, not a second way to read a role. A button that names no capability returns true immediately, so
10797
+ // every existing button is untouched.
10798
+ hasCapability(button) {
10799
+ if (!button?.capability)
10800
+ return true;
10801
+ const capName = typeof button.capability === 'string' ? button.capability : button.capability.name; // resolved HERE, not at config-build time — datalib assigns CapItem.name after the configs are constructed
10802
+ if (!capName)
10803
+ return true; // an unresolved capability must not gate anything — a silent deny is worse than the status quo
10804
+ const level = this.authService.currentRoleSource.value?.[capName] ?? RoleAccess.None;
10805
+ return level >= (button.requiredAccess ?? RoleAccess.Edit); // Edit is BaseController.CustomActionAccess's floor
10806
+ }
10757
10807
  testVisible(button, row, tableConfig) {
10758
10808
  if (!Core.isItemVisible(button, row)) { // Changed: unified visible/hidden (boolean | condition), hidden wins
10759
10809
  return false;
10760
10810
  }
10811
+ if (!this.hasCapability(button)) { // Added: opt-in capability gate — no-op unless the config names one
10812
+ return false;
10813
+ }
10761
10814
  const currentRole = this.authService.currentRoleSource.value;
10762
10815
  // Check if button has detailsConfig with form restrictions
10763
10816
  if (button.detailsConfig?.formConfig?.security) {
@@ -11773,28 +11826,60 @@ class ConfigService {
11773
11826
  get value() {
11774
11827
  return this.config.value;
11775
11828
  }
11829
+ static { this.maxFailedAttempts = 3; }
11776
11830
  constructor(dataService) {
11777
11831
  this.dataService = dataService;
11778
11832
  this.config = new BehaviorSubject({});
11779
11833
  this.config$ = this.config.asObservable();
11780
11834
  this.loaded = false;
11835
+ // Added: one attempt at a time. Two callers racing on login used to be harmless only because the latch below
11836
+ // swallowed the second; with the latch corrected they would both hit the wire, so dedupe explicitly instead.
11837
+ this.inFlight = false;
11838
+ // Added: the bound. Retrying is CALLER-DRIVEN — there is no timer and no resubscribe here, so a retry only
11839
+ // ever happens because something asked again. This cap stops a chatty caller (a component that loads on every
11840
+ // navigation) from turning a genuinely down API into one request per navigation, forever.
11841
+ this.failedAttempts = 0;
11781
11842
  }
11782
11843
  // Called on login/bootstrap — the backend creates the row on first read, so this never comes back empty
11783
11844
  load(force = false) {
11845
+ // Changed: `loaded` was set HERE, before the request, and every failure was swallowed — so the FIRST attempt
11846
+ // won the session outright. shift-spa fired one on the login screen (AuthService seeds currentRoleSource with
11847
+ // a truthy `new Role`, and a BehaviorSubject replays its seed synchronously), it 401'd with an empty bearer
11848
+ // token, and every later authenticated caller returned early forever. The app then ran on an empty config —
11849
+ // every flag false, ~70 call sites — while the database said otherwise. Only a page reload masked it, which
11850
+ // is why it survived: silent-restore replays the 401'd request, a fresh interactive login does not.
11851
+ if (force)
11852
+ this.failedAttempts = 0; // an explicit refresh is a deliberate act (the App Configuration page just saved), so it is allowed past the cap
11784
11853
  if (this.loaded && !force)
11785
11854
  return;
11786
- this.loaded = true;
11855
+ if (this.inFlight && !force)
11856
+ return;
11857
+ if (this.failedAttempts >= ConfigService.maxFailedAttempts && !force)
11858
+ return;
11859
+ this.inFlight = true;
11787
11860
  this.dataService.CallApi({ url: 'configuration/get', skipCache: true }).subscribe({
11788
11861
  next: (apiResponse) => {
11862
+ this.inFlight = false;
11789
11863
  // DELIBERATELY SILENT (WS-6 Phase 4 triage). This runs on login/bootstrap, before the user has asked
11790
- // for anything, and everything downstream of it FAILS OPEN: an empty config hides no nav item, gates
11791
- // no module and suffixes no unit. So a failure here costs cosmetics, not capability and greeting
11792
- // someone with an error dialog the instant they sign in, for an app that then works, is a worse lie
11793
- // than saying nothing. The error callback below already absorbs the transport case for the same reason.
11794
- if (apiResponse.success)
11864
+ // for anything, and greeting someone with an error dialog the instant they sign in, for an app that then
11865
+ // works, is a worse lie than saying nothing. The error callback below absorbs the transport case likewise.
11866
+ // Changed: the old comment claimed everything downstream FAILS OPEN. It does not. Config flags are read as
11867
+ // `!!config.enableX` and the database defaults are TRUE, so an empty config reads FALSE and HIDES things —
11868
+ // fail-CLOSED. Staying silent is still right; latching the failure was not. Retry stays possible instead.
11869
+ if (apiResponse.success) {
11870
+ this.loaded = true; // Changed: only a response that actually carried a config counts as loaded
11871
+ this.failedAttempts = 0;
11795
11872
  this.config.next(apiResponse.data || {});
11873
+ }
11874
+ else
11875
+ this.failedAttempts++; // Added: a refusal (401/403 shaped as success:false) is a failure, not a loaded config
11796
11876
  },
11797
- error: () => { } // an app without a ConfigurationController simply keeps the empty object
11877
+ error: () => {
11878
+ // an app without a ConfigurationController simply keeps the empty object — and after maxFailedAttempts
11879
+ // stops asking, so the 404 costs three requests a session rather than one, and never more
11880
+ this.inFlight = false;
11881
+ this.failedAttempts++;
11882
+ }
11798
11883
  });
11799
11884
  }
11800
11885
  refresh() {
@@ -12143,7 +12228,9 @@ class SetupGuideComponent {
12143
12228
  const act = this.getAction(step);
12144
12229
  if (act.button) {
12145
12230
  this.dialogService.openDefaultDetailsDialog({ ...act.button }, {}).subscribe(result => {
12146
- if (result?.action === 'inputChange')
12231
+ if (!result)
12232
+ return; // Added: Escape/backdrop dismissal closes with undefined. This one never threw (it already read through `?.`) but it fell on through to refresh() on every dismissal — a dismissal changes nothing, so it must not recount either.
12233
+ if (result.action === 'inputChange')
12147
12234
  return; // Changed: an input change is not a close — nothing to recount or follow up
12148
12235
  this.setupService.refresh(); // Recount after the dialog closes so progress advances immediately
12149
12236
  this.openSuccessButton(act.button, result); // Added: complete CLAUDE.md's Two-Step Create Pattern from this page too
@@ -12428,11 +12515,11 @@ class SetupGuideComponent {
12428
12515
  });
12429
12516
  }
12430
12517
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SetupGuideComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
12431
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SetupGuideComponent, isStandalone: false, selector: "spa-setup-guide", ngImport: i0, template: "<div class=\"setup-page\" *ngIf=\"status\">\n\n <!-- Hero: overall readiness + celebration state at 100% -->\n <mat-card class=\"setup-hero\" [class.celebrate]=\"status.percent === 100\">\n <div class=\"hero-content\" *ngIf=\"status.percent < 100\">\n <div class=\"hero-text\">\n <h1>{{ title }}</h1>\n <p>Complete these steps to get your system ready for day-to-day operation.</p>\n <span class=\"hero-counter\">{{ status.completed }} of {{ status.total }} steps completed</span>\n </div>\n <div class=\"hero-progress\">\n <span class=\"hero-percent\">{{ status.percent }}%</span>\n <mat-progress-bar mode=\"determinate\" [value]=\"status.percent\"></mat-progress-bar>\n </div>\n </div>\n <div class=\"hero-content celebration\" *ngIf=\"status.percent === 100\">\n <mat-icon class=\"celebrate-icon\">celebration</mat-icon>\n <div class=\"hero-text\">\n <h1>You're all set!</h1>\n <p>All setup steps are complete \u2014 your system is ready to operate.</p>\n </div>\n </div>\n </mat-card>\n\n <!-- Module picker (v2): choose what the business uses; optional modules toggle steps + menus -->\n <mat-card class=\"setup-modules\" *ngIf=\"modules.length > 0\">\n <div class=\"group-header\">\n <h2>Your modules</h2>\n <span class=\"group-counter\">Tap a module to turn it on or off \u2014 you can change this anytime.</span>\n </div>\n <!-- Changed (v3): grouped \u2014 the picker now carries the whole library catalog, so cards sit under their\n group heading (the app's own modules first). A backend without groups yields one unnamed group,\n which renders as the original flat grid. -->\n <div class=\"module-section\" *ngFor=\"let mgroup of moduleGroups\">\n <div class=\"module-section-head\">\n <span class=\"module-section-name\">{{ mgroup.name }}</span>\n <span class=\"module-section-count\">{{ mgroup.enabled }} of {{ mgroup.total }} on</span>\n </div>\n <div class=\"module-grid\">\n <div class=\"module-card\" *ngFor=\"let mod of mgroup.modules\"\n [class.enabled]=\"mod.enabled\" [class.core]=\"mod.core\" [class.busy]=\"togglingKey === mod.key\"\n (click)=\"toggleModule(mod)\">\n <div class=\"module-head\">\n <mat-icon class=\"module-icon\">{{ mod.icon || 'extension' }}</mat-icon>\n <mat-icon class=\"module-state\" [class.on]=\"mod.enabled\">{{ mod.enabled ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n </div>\n <div class=\"module-title\">{{ mod.title }}</div>\n <div class=\"module-description\">{{ mod.description }}</div>\n <span class=\"module-chip\" *ngIf=\"mod.core\">Always on</span>\n </div>\n </div>\n </div>\n <div class=\"module-actions\" *ngIf=\"!modulesConfirmed\">\n <button mat-flat-button color=\"primary\" [disabled]=\"togglingKey !== ''\" (click)=\"confirmModules()\">Confirm selection</button>\n <span class=\"module-hint\">Happy with this selection? Confirm it to complete the step below.</span>\n </div>\n </mat-card>\n\n <!-- Category groups, top-down \u2014 Changed: flat spa-checklist replaces the raised mat-accordion -->\n <mat-card class=\"setup-group\" *ngFor=\"let group of groups\">\n <div class=\"group-header\">\n <h2>{{ group.name }}</h2>\n <span class=\"group-counter\">{{ group.completed }} of {{ group.total }}</span>\n </div>\n <spa-checklist [config]=\"group.config\" [itemTemplate]=\"stepExtrasTpl\"></spa-checklist>\n </mat-card>\n\n <!-- Demo data (opt-in via setupConfig.demoData) \u2014 moved here off the per-app configuration pages -->\n <mat-card class=\"setup-group setup-demo\" *ngIf=\"showDemoData\">\n <div class=\"group-header\">\n <h2>Demo data</h2>\n <span class=\"group-counter\">Explore with example records, then clear them out</span>\n </div>\n <p class=\"demo-description\">Seeding fills the system with example records so you can try it out before capturing anything real. Removing deletes only those example records.</p>\n <div class=\"demo-actions\">\n <button mat-flat-button color=\"primary\" [disabled]=\"demoBusy\" (click)=\"seedDemoData()\"><mat-icon>add_circle</mat-icon> Seed demo data</button>\n <button mat-stroked-button color=\"warn\" [disabled]=\"demoBusy\" (click)=\"removeDemoData()\"><mat-icon>delete</mat-icon> Remove demo data</button>\n </div>\n </mat-card>\n\n</div>\n\n<!-- Projected into the expanded checklist body: predefined roles picker on the roles step (v2) -->\n<ng-template #stepExtrasTpl let-item>\n <div class=\"role-templates\" *ngIf=\"hasRoleTemplates(item.data) && roleTemplates.length > 0\">\n <div class=\"role-template\" *ngFor=\"let tpl of roleTemplates\" [class.created]=\"tpl.exists\" [class.selected]=\"!tpl.exists && selectedTemplates[tpl.key]\" (click)=\"toggleTemplate(tpl)\">\n <div class=\"role-head\">\n <mat-icon class=\"role-state\" [class.on]=\"tpl.exists || selectedTemplates[tpl.key]\">{{ (tpl.exists || selectedTemplates[tpl.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"role-name\">{{ tpl.name }}</span>\n <span class=\"role-created\" *ngIf=\"tpl.exists\"><mat-icon>check</mat-icon>Created</span>\n </div>\n <div class=\"role-description\">{{ tpl.description }}</div>\n </div>\n </div>\n\n <!-- Added (presets): the preset picker. Same row as the role picker above \u2014 same state icon, same 8px stack,\n same colours \u2014 plus ONE sentence and ONE control.\n Changed (presets P4): rendered through ONE shared template for both the notification step and the sign-off\n step, parameterised by the picker object. The two sit one above the other on the same page, so a second\n near-identical block would drift and every difference would read as a mistake. -->\n <ng-container *ngIf=\"pickerFor(item.data) as picker\">\n <ng-container *ngTemplateOutlet=\"presetPickerTpl; context: { $implicit: picker }\"></ng-container>\n </ng-container>\n</ng-template>\n\n<!-- The one picker body. `picker` carries the domain: its rows, its roles, its wording, its apply call. -->\n<ng-template #presetPickerTpl let-picker>\n <div class=\"preset-picker\" *ngIf=\"picker.loaded && picker.presets.length > 0\">\n\n <!-- No selectable role \u2192 we ask for one instead of offering ticks that would build an inert rule. The wording\n is the picker's own, because \"no roles\" and \"roles nobody is in\" are different situations. -->\n <div class=\"preset-empty\" *ngIf=\"picker.roles.length === 0\">{{ picker.emptyText }}</div>\n\n <ng-container *ngIf=\"picker.roles.length > 0\">\n <div class=\"preset-section\" *ngFor=\"let pgroup of picker.groups\">\n <div class=\"preset-section-head\">\n <span class=\"preset-section-name\">{{ pgroup.name }}</span>\n <span class=\"preset-section-count\">{{ pgroup.on }} of {{ pgroup.total }}</span>\n </div>\n <div class=\"preset-row\" *ngFor=\"let row of pgroup.rows\" role=\"checkbox\" tabindex=\"0\"\n [class.configured]=\"row.preset.exists\" [class.selected]=\"!row.preset.exists && picker.selected[row.preset.key]\"\n [attr.aria-checked]=\"row.preset.exists || !!picker.selected[row.preset.key]\" [attr.aria-disabled]=\"row.preset.exists\"\n (click)=\"togglePreset(picker, row.preset)\"\n (keydown.enter)=\"togglePreset(picker, row.preset)\"\n (keydown.space)=\"togglePreset(picker, row.preset); $event.preventDefault()\">\n <div class=\"preset-head\">\n <mat-icon class=\"preset-state\" [class.on]=\"row.preset.exists || picker.selected[row.preset.key]\">{{ (row.preset.exists || picker.selected[row.preset.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"preset-name\">{{ row.preset.name }}</span>\n <span class=\"preset-configured\" *ngIf=\"row.preset.exists\"><mat-icon>check</mat-icon>Configured</span>\n </div>\n <div class=\"preset-description\">{{ row.preset.description }}</div>\n <!-- The effect sentence comes from the server whole; `before` and `after` are its two halves either side\n of the {role} placeholder, so reading the line IS reading the configuration. The sign-off sentences\n carry their own deferred-notification caveat, which is why there is no extra note here. -->\n <div class=\"preset-effect\" *ngIf=\"!row.preset.exists\">{{ row.before }}<select class=\"preset-role\" [attr.aria-label]=\"picker.roleAria + row.preset.name\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n (change)=\"setPresetRole(picker, row.preset, $any($event.target).value)\">\n <option *ngFor=\"let role of picker.roles\" [value]=\"role.value\" [selected]=\"role.value === picker.role[row.preset.key]\">{{ role.name }}</option>\n </select>{{ row.after }}</div>\n <div class=\"preset-effect preset-done\" *ngIf=\"row.preset.exists\">{{ picker.doneText }}</div>\n </div>\n </div>\n\n <!-- One page-level channel choice, notifications only. In-app is the only channel that always works; SMS is a\n stub and is never offered; an approval request is always in-app, so the sign-off picker has no such line. -->\n <div class=\"preset-email\" *ngIf=\"picker.showEmail\" role=\"checkbox\" tabindex=\"0\" [attr.aria-checked]=\"includeEmail\"\n (click)=\"toggleIncludeEmail()\"\n (keydown.enter)=\"toggleIncludeEmail()\"\n (keydown.space)=\"toggleIncludeEmail(); $event.preventDefault()\">\n <mat-icon class=\"preset-state\" [class.on]=\"includeEmail\">{{ includeEmail ? 'check_box' : 'check_box_outline_blank' }}</mat-icon>\n <span class=\"preset-email-label\">Also send these by email</span>\n <span class=\"preset-email-hint\">In-app always. Email needs your mail settings.</span>\n </div>\n </ng-container>\n </div>\n</ng-template>\n\n<!-- Graceful empty state (feature disabled or status unavailable) -->\n<div class=\"setup-empty\" *ngIf=\"!status\">\n <mat-icon>rocket_launch</mat-icon>\n <p>Setup status is not available yet.</p>\n</div>\n", styles: [".setup-page{max-width:860px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.setup-hero{padding:24px}.hero-content{display:flex;align-items:center;justify-content:space-between;gap:24px;flex-wrap:wrap}.hero-text h1{margin:0 0 4px;font-size:24px}.hero-text p{margin:0 0 8px;color:#0009}.hero-counter{font-size:13px;color:#0009}.hero-progress{flex:1;min-width:220px;max-width:340px}.hero-percent{display:block;font-size:28px;font-weight:600;color:#2e7d32;margin-bottom:6px;text-align:right}.hero-progress mat-progress-bar{height:10px;border-radius:5px}.setup-hero.celebrate{background:linear-gradient(135deg,#e8f5e9,#f1f8e9)}.celebration{justify-content:flex-start}.celebrate-icon{font-size:48px;width:48px;height:48px;color:#2e7d32;animation:celebrate-pop .6s ease-out}@keyframes celebrate-pop{0%{transform:scale(.3);opacity:0}70%{transform:scale(1.15)}to{transform:scale(1);opacity:1}}.setup-group{padding:16px}.group-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.group-header h2{margin:0;font-size:17px}.group-counter{font-size:12px;color:#0000008c}.setup-modules{padding:16px}.module-section+.module-section{margin-top:18px}.module-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.module-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.module-section-count{font-size:11px;color:#00000073;white-space:nowrap}.module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;margin-top:8px}.module-card{border:1px solid rgba(0,0,0,.12);border-radius:10px;padding:12px;cursor:pointer;transition:border-color .15s,background .15s,opacity .15s;opacity:.72;position:relative}.module-card:hover{border-color:#90a4ae}.module-card.enabled{border-color:#4caf50;background:#f6fbf6;opacity:1}.module-card.core{cursor:default}.module-card.busy{pointer-events:none;opacity:.5}.module-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.module-icon{color:#546e7a}.module-card.enabled .module-icon{color:#2e7d32}.module-state{color:#b0bec5}.module-state.on{color:#4caf50}.module-title{font-weight:600;font-size:14px;margin-bottom:4px}.module-description{font-size:12px;color:#0009;min-height:30px}.module-chip{display:inline-block;margin-top:8px;background:#eceff1;color:#546e7a;border-radius:12px;padding:2px 10px;font-size:11px}.module-actions{display:flex;align-items:center;gap:8px 12px;margin-top:14px;flex-wrap:wrap}.module-actions button{flex-shrink:0}.module-hint{font-size:12px;color:#0000008c}.role-templates{display:flex;flex-direction:column;gap:8px;margin:0 0 12px}.role-template{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.role-template:hover{border-color:#90a4ae}.role-template.selected{border-color:#4caf50;background:#f6fbf6}.role-template.created{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.role-head{display:flex;align-items:center;gap:8px}.role-state{color:#b0bec5;flex-shrink:0}.role-state.on{color:#4caf50}.role-name{font-weight:500;font-size:13px}.role-created{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32}.role-created mat-icon{font-size:16px;width:16px;height:16px}.role-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-picker{display:flex;flex-direction:column;gap:14px;margin:0 0 12px}.preset-empty{font-size:12px;line-height:18px;color:#0009;max-width:62ch}.preset-section{display:flex;flex-direction:column;gap:8px}.preset-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.preset-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.preset-section-count{font-size:11px;color:#00000073;white-space:nowrap}.preset-row{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.preset-row:hover{border-color:#90a4ae}.preset-row:focus-visible{outline:2px solid #4caf50;outline-offset:2px}.preset-row.selected{border-color:#4caf50;background:#f6fbf6}.preset-row.configured{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.preset-head{display:flex;align-items:center;gap:8px}.preset-state{color:#b0bec5;flex-shrink:0}.preset-state.on{color:#4caf50}.preset-name{font-weight:500;font-size:13px}.preset-configured{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32;white-space:nowrap}.preset-configured mat-icon{font-size:16px;width:16px;height:16px}.preset-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-effect{font-size:12px;line-height:22px;color:#000000c7;margin:4px 0 0 32px}.preset-done{color:#2e7d32}.preset-role{font-family:inherit;font-size:12px;font-weight:500;color:#2e7d32;background:#fff;border:1px solid #c8e6c9;border-radius:6px;padding:2px 4px;margin:0 2px;max-width:100%;cursor:pointer}.preset-role:hover{border-color:#4caf50}.preset-role:focus-visible{outline:2px solid #4caf50;outline-offset:1px}.preset-email{display:flex;align-items:center;gap:8px;flex-wrap:wrap;cursor:pointer;padding:2px 0}.preset-email:focus-visible{outline:2px solid #4caf50;outline-offset:2px;border-radius:6px}.preset-email-label{font-size:13px;font-weight:500}.preset-email-hint{font-size:12px;color:#0000008c}.setup-empty{text-align:center;padding:48px 16px;color:#00000080}.setup-empty mat-icon{font-size:40px;width:40px;height:40px}.demo-description{margin:8px 0 14px;font-size:13px;color:#0009}.demo-actions{display:flex;gap:12px;flex-wrap:wrap}@media (max-width: 700px){.module-hint{flex:0 0 100%}.preset-role{display:block;width:100%;margin:4px 0 0;padding:6px 8px}.preset-email-hint{flex:0 0 100%}}\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: i2$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { 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: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: i6$1.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: ChecklistComponent, selector: "spa-checklist", inputs: ["config", "itemTemplate"] }] }); }
12518
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SetupGuideComponent, isStandalone: false, selector: "spa-setup-guide", ngImport: i0, template: "<div class=\"setup-page\" *ngIf=\"status\">\n\n <!-- Hero: overall readiness + celebration state at 100% -->\n <mat-card class=\"setup-hero\" [class.celebrate]=\"status.percent === 100\">\n <div class=\"hero-content\" *ngIf=\"status.percent < 100\">\n <div class=\"hero-text\">\n <h1>{{ title }}</h1>\n <p>Complete these steps to get your system ready for day-to-day operation.</p>\n <span class=\"hero-counter\">{{ status.completed }} of {{ status.total }} steps completed</span>\n </div>\n <div class=\"hero-progress\">\n <span class=\"hero-percent\">{{ status.percent }}%</span>\n <mat-progress-bar mode=\"determinate\" [value]=\"status.percent\"></mat-progress-bar>\n </div>\n </div>\n <div class=\"hero-content celebration\" *ngIf=\"status.percent === 100\">\n <mat-icon class=\"celebrate-icon\">celebration</mat-icon>\n <div class=\"hero-text\">\n <h1>You're all set!</h1>\n <p>All setup steps are complete \u2014 your system is ready to operate.</p>\n </div>\n </div>\n </mat-card>\n\n <!-- Module picker (v2): choose what the business uses; optional modules toggle steps + menus -->\n <mat-card class=\"setup-modules\" *ngIf=\"modules.length > 0\">\n <div class=\"group-header\">\n <h2>Your modules</h2>\n <span class=\"group-counter\">Tap a module to turn it on or off \u2014 you can change this anytime.</span>\n </div>\n <!-- Changed (v3): grouped \u2014 the picker now carries the whole library catalog, so cards sit under their\n group heading (the app's own modules first). A backend without groups yields one unnamed group,\n which renders as the original flat grid. -->\n <div class=\"module-section\" *ngFor=\"let mgroup of moduleGroups\">\n <div class=\"module-section-head\">\n <span class=\"module-section-name\">{{ mgroup.name }}</span>\n <span class=\"module-section-count\">{{ mgroup.enabled }} of {{ mgroup.total }} on</span>\n </div>\n <div class=\"module-grid\">\n <div class=\"module-card\" *ngFor=\"let mod of mgroup.modules\"\n [class.enabled]=\"mod.enabled\" [class.core]=\"mod.core\" [class.busy]=\"togglingKey === mod.key\"\n (click)=\"toggleModule(mod)\">\n <div class=\"module-head\">\n <mat-icon class=\"module-icon\">{{ mod.icon || 'extension' }}</mat-icon>\n <mat-icon class=\"module-state\" [class.on]=\"mod.enabled\">{{ mod.enabled ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n </div>\n <div class=\"module-title\">{{ mod.title }}</div>\n <div class=\"module-description\">{{ mod.description }}</div>\n <span class=\"module-chip\" *ngIf=\"mod.core\">Always on</span>\n </div>\n </div>\n </div>\n <div class=\"module-actions\" *ngIf=\"!modulesConfirmed\">\n <button mat-flat-button color=\"primary\" [disabled]=\"togglingKey !== ''\" (click)=\"confirmModules()\">Confirm selection</button>\n <span class=\"module-hint\">Happy with this selection? Confirm it to complete the step below.</span>\n </div>\n </mat-card>\n\n <!-- Category groups, top-down \u2014 Changed: flat spa-checklist replaces the raised mat-accordion -->\n <mat-card class=\"setup-group\" *ngFor=\"let group of groups\">\n <div class=\"group-header\">\n <h2>{{ group.name }}</h2>\n <span class=\"group-counter\">{{ group.completed }} of {{ group.total }}</span>\n </div>\n <spa-checklist [config]=\"group.config\" [itemTemplate]=\"stepExtrasTpl\"></spa-checklist>\n </mat-card>\n\n <!-- Demo data (opt-in via setupConfig.demoData) \u2014 moved here off the per-app configuration pages -->\n <mat-card class=\"setup-group setup-demo\" *ngIf=\"showDemoData\">\n <div class=\"group-header\">\n <h2>Demo data</h2>\n <span class=\"group-counter\">Explore with example records, then clear them out</span>\n </div>\n <p class=\"demo-description\">Seeding fills the system with example records so you can try it out before capturing anything real. Removing deletes only those example records.</p>\n <div class=\"demo-actions\">\n <button mat-flat-button color=\"primary\" [disabled]=\"demoBusy\" (click)=\"seedDemoData()\"><mat-icon>add_circle</mat-icon> Seed demo data</button>\n <button mat-stroked-button color=\"warn\" [disabled]=\"demoBusy\" (click)=\"removeDemoData()\"><mat-icon>delete</mat-icon> Remove demo data</button>\n </div>\n </mat-card>\n\n</div>\n\n<!-- Projected into the expanded checklist body: predefined roles picker on the roles step (v2) -->\n<ng-template #stepExtrasTpl let-item>\n <div class=\"role-templates\" *ngIf=\"hasRoleTemplates(item.data) && roleTemplates.length > 0\">\n <div class=\"role-template\" *ngFor=\"let tpl of roleTemplates\" [class.created]=\"tpl.exists\" [class.selected]=\"!tpl.exists && selectedTemplates[tpl.key]\" (click)=\"toggleTemplate(tpl)\">\n <div class=\"role-head\">\n <mat-icon class=\"role-state\" [class.on]=\"tpl.exists || selectedTemplates[tpl.key]\">{{ (tpl.exists || selectedTemplates[tpl.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"role-name\">{{ tpl.name }}</span>\n <span class=\"role-created\" *ngIf=\"tpl.exists\"><mat-icon>check</mat-icon>Created</span>\n </div>\n <div class=\"role-description\">{{ tpl.description }}</div>\n </div>\n </div>\n\n <!-- Added (presets): the preset picker. Same row as the role picker above \u2014 same state icon, same 8px stack,\n same colours \u2014 plus ONE sentence and ONE control.\n Changed (presets P4): rendered through ONE shared template for both the notification step and the sign-off\n step, parameterised by the picker object. The two sit one above the other on the same page, so a second\n near-identical block would drift and every difference would read as a mistake. -->\n <ng-container *ngIf=\"pickerFor(item.data) as picker\">\n <ng-container *ngTemplateOutlet=\"presetPickerTpl; context: { $implicit: picker }\"></ng-container>\n </ng-container>\n</ng-template>\n\n<!-- The one picker body. `picker` carries the domain: its rows, its roles, its wording, its apply call. -->\n<ng-template #presetPickerTpl let-picker>\n <div class=\"preset-picker\" *ngIf=\"picker.loaded && picker.presets.length > 0\">\n\n <!-- No selectable role \u2192 we ask for one instead of offering ticks that would build an inert rule. The wording\n is the picker's own, because \"no roles\" and \"roles nobody is in\" are different situations. -->\n <div class=\"preset-empty\" *ngIf=\"picker.roles.length === 0\">{{ picker.emptyText }}</div>\n\n <ng-container *ngIf=\"picker.roles.length > 0\">\n <div class=\"preset-section\" *ngFor=\"let pgroup of picker.groups\">\n <div class=\"preset-section-head\">\n <span class=\"preset-section-name\">{{ pgroup.name }}</span>\n <span class=\"preset-section-count\">{{ pgroup.on }} of {{ pgroup.total }}</span>\n </div>\n <div class=\"preset-row\" *ngFor=\"let row of pgroup.rows\" role=\"checkbox\" tabindex=\"0\"\n [class.configured]=\"row.preset.exists\" [class.selected]=\"!row.preset.exists && picker.selected[row.preset.key]\"\n [attr.aria-checked]=\"row.preset.exists || !!picker.selected[row.preset.key]\" [attr.aria-disabled]=\"row.preset.exists\"\n (click)=\"togglePreset(picker, row.preset)\"\n (keydown.enter)=\"togglePreset(picker, row.preset)\"\n (keydown.space)=\"togglePreset(picker, row.preset); $event.preventDefault()\">\n <div class=\"preset-head\">\n <mat-icon class=\"preset-state\" [class.on]=\"row.preset.exists || picker.selected[row.preset.key]\">{{ (row.preset.exists || picker.selected[row.preset.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"preset-name\">{{ row.preset.name }}</span>\n <span class=\"preset-configured\" *ngIf=\"row.preset.exists\"><mat-icon>check</mat-icon>Configured</span>\n </div>\n <div class=\"preset-description\">{{ row.preset.description }}</div>\n <!-- The effect sentence comes from the server whole; `before` and `after` are its two halves either side\n of the {role} placeholder, so reading the line IS reading the configuration. The sign-off sentences\n carry their own deferred-notification caveat, which is why there is no extra note here. -->\n <div class=\"preset-effect\" *ngIf=\"!row.preset.exists\">{{ row.before }}<select class=\"preset-role\" [attr.aria-label]=\"picker.roleAria + row.preset.name\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n (change)=\"setPresetRole(picker, row.preset, $any($event.target).value)\">\n <option *ngFor=\"let role of picker.roles\" [value]=\"role.value\" [selected]=\"role.value === picker.role[row.preset.key]\">{{ role.name }}</option>\n </select>{{ row.after }}</div>\n <div class=\"preset-effect preset-done\" *ngIf=\"row.preset.exists\">{{ picker.doneText }}</div>\n </div>\n </div>\n\n <!-- One page-level channel choice, notifications only. In-app is the only channel that always works; SMS is a\n stub and is never offered; an approval request is always in-app, so the sign-off picker has no such line. -->\n <div class=\"preset-email\" *ngIf=\"picker.showEmail\" role=\"checkbox\" tabindex=\"0\" [attr.aria-checked]=\"includeEmail\"\n (click)=\"toggleIncludeEmail()\"\n (keydown.enter)=\"toggleIncludeEmail()\"\n (keydown.space)=\"toggleIncludeEmail(); $event.preventDefault()\">\n <mat-icon class=\"preset-state\" [class.on]=\"includeEmail\">{{ includeEmail ? 'check_box' : 'check_box_outline_blank' }}</mat-icon>\n <span class=\"preset-email-label\">Also send these by email</span>\n <span class=\"preset-email-hint\">In-app always. Email needs your mail settings.</span>\n </div>\n </ng-container>\n </div>\n</ng-template>\n\n<!-- Graceful empty state (feature disabled or status unavailable) -->\n<div class=\"setup-empty\" *ngIf=\"!status\">\n <mat-icon>rocket_launch</mat-icon>\n <p>Setup status is not available yet.</p>\n</div>\n", styles: [".setup-page{max-width:860px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.setup-hero{padding:24px}.hero-content{display:flex;align-items:center;justify-content:space-between;gap:24px;flex-wrap:wrap}.hero-text h1{margin:0 0 4px;font-size:24px}.hero-text p{margin:0 0 8px;color:#0009}.hero-counter{font-size:13px;color:#0009}.hero-progress{flex:1;min-width:220px;max-width:340px}.hero-percent{display:block;font-size:28px;font-weight:600;color:#2e7d32;margin-bottom:6px;text-align:right}.hero-progress mat-progress-bar{height:10px;border-radius:5px}.setup-hero.celebrate{background:linear-gradient(135deg,#e8f5e9,#f1f8e9)}.celebration{justify-content:flex-start}.celebrate-icon{font-size:48px;width:48px;height:48px;color:#2e7d32;animation:celebrate-pop .6s ease-out}@keyframes celebrate-pop{0%{transform:scale(.3);opacity:0}70%{transform:scale(1.15)}to{transform:scale(1);opacity:1}}.setup-group{padding:16px}.group-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.group-header h2{margin:0;font-size:17px}.group-counter{font-size:12px;color:#0000008c}.setup-modules{padding:16px}.module-section+.module-section{margin-top:18px}.module-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.module-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.module-section-count{font-size:11px;color:#00000073;white-space:nowrap}.module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;margin-top:8px}.module-card{border:1px solid rgba(0,0,0,.12);border-radius:10px;padding:12px;cursor:pointer;transition:border-color .15s,background .15s,opacity .15s;opacity:.72;position:relative}.module-card:hover{border-color:#90a4ae}.module-card.enabled{border-color:#4caf50;background:#f6fbf6;opacity:1}.module-card.core{cursor:default}.module-card.busy{pointer-events:none;opacity:.5}.module-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.module-icon{color:#546e7a}.module-card.enabled .module-icon{color:#2e7d32}.module-state{color:#b0bec5}.module-state.on{color:#4caf50}.module-title{font-weight:600;font-size:14px;margin-bottom:4px}.module-description{font-size:12px;color:#0009;min-height:30px}.module-chip{display:inline-block;margin-top:8px;background:#eceff1;color:#546e7a;border-radius:12px;padding:2px 10px;font-size:11px}.module-actions{display:flex;align-items:center;gap:8px 12px;margin-top:14px;flex-wrap:wrap}.module-actions button{flex-shrink:0}.module-hint{font-size:12px;color:#0000008c}.role-templates{display:flex;flex-direction:column;gap:8px;margin:0 0 12px}.role-template{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.role-template:hover{border-color:#90a4ae}.role-template.selected{border-color:#4caf50;background:#f6fbf6}.role-template.created{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.role-head{display:flex;align-items:center;gap:8px}.role-state{color:#b0bec5;flex-shrink:0}.role-state.on{color:#4caf50}.role-name{font-weight:500;font-size:13px}.role-created{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32}.role-created mat-icon{font-size:16px;width:16px;height:16px}.role-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-picker{display:flex;flex-direction:column;gap:14px;margin:0 0 12px}.preset-empty{font-size:12px;line-height:18px;color:#0009;max-width:62ch}.preset-section{display:flex;flex-direction:column;gap:8px}.preset-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.preset-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.preset-section-count{font-size:11px;color:#00000073;white-space:nowrap}.preset-row{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.preset-row:hover{border-color:#90a4ae}.preset-row:focus-visible{outline:2px solid #4caf50;outline-offset:2px}.preset-row.selected{border-color:#4caf50;background:#f6fbf6}.preset-row.configured{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.preset-head{display:flex;align-items:center;gap:8px}.preset-state{color:#b0bec5;flex-shrink:0}.preset-state.on{color:#4caf50}.preset-name{font-weight:500;font-size:13px}.preset-configured{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32;white-space:nowrap}.preset-configured mat-icon{font-size:16px;width:16px;height:16px}.preset-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-effect{font-size:12px;line-height:22px;color:#000000c7;margin:4px 0 0 32px}.preset-done{color:#2e7d32}.preset-role{font-family:inherit;font-size:12px;font-weight:500;color:#2e7d32;background:#fff;border:1px solid #c8e6c9;border-radius:6px;padding:2px 4px;margin:0 2px;max-width:100%;cursor:pointer}.preset-role:hover{border-color:#4caf50}.preset-role:focus-visible{outline:2px solid #4caf50;outline-offset:1px}.preset-email{display:flex;align-items:center;gap:8px;flex-wrap:wrap;cursor:pointer;padding:2px 0}.preset-email:focus-visible{outline:2px solid #4caf50;outline-offset:2px;border-radius:6px}.preset-email-label{font-size:13px;font-weight:500}.preset-email-hint{font-size:12px;color:#0000008c}.setup-empty{text-align:center;padding:48px 16px;color:#00000080}.setup-empty mat-icon{font-size:40px;width:40px;height:40px}.demo-description{margin:8px 0 14px;font-size:13px;color:#0009}.demo-actions{display:flex;gap:12px;flex-wrap:wrap}@media (max-width: 700px){.setup-page{padding:10px 0;gap:12px}.setup-hero{padding:12px}.setup-modules,.setup-group{padding:10px}.hero-content{gap:10px}.hero-text h1{font-size:20px}.setup-hero:not(.celebrate) .hero-text p{display:none}.hero-counter{font-size:12px}.hero-progress{min-width:0;max-width:none}.hero-percent{font-size:22px;margin-bottom:4px}.hero-progress mat-progress-bar{height:8px;border-radius:4px}.celebrate-icon{font-size:36px;width:36px;height:36px}.group-header{margin-bottom:6px;gap:2px 8px}.group-header h2{font-size:15px}.group-counter{font-size:11px}.setup-modules>.group-header,.setup-demo>.group-header{flex-wrap:wrap}.setup-modules>.group-header .group-counter,.setup-demo>.group-header .group-counter{flex:1 0 100%}.module-section+.module-section{margin-top:12px}.module-section-head{padding-bottom:4px}.module-grid{gap:8px;margin-top:6px}.module-card{padding:10px}.module-head{margin-bottom:4px}.module-title{font-size:13.5px;margin-bottom:2px}.module-description{min-height:0}.module-chip{margin-top:6px}.module-actions{margin-top:10px}.role-templates,.preset-picker{gap:6px;margin-bottom:8px}.role-template,.preset-row{padding:8px 10px}.role-description,.preset-description,.preset-effect{margin-left:0}.preset-section{gap:6px}.preset-section-head{padding-bottom:4px}.demo-description{margin:6px 0 10px;font-size:12.5px}.demo-actions{gap:8px}.setup-empty{padding:28px 12px}.module-hint{flex:0 0 100%}.preset-role{display:block;width:100%;margin:4px 0 0;padding:6px 8px}.preset-email-hint{flex:0 0 100%}}\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: i2$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { 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: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: i6$1.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: ChecklistComponent, selector: "spa-checklist", inputs: ["config", "itemTemplate"] }] }); }
12432
12519
  }
12433
12520
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SetupGuideComponent, decorators: [{
12434
12521
  type: Component,
12435
- args: [{ selector: 'spa-setup-guide', standalone: false, template: "<div class=\"setup-page\" *ngIf=\"status\">\n\n <!-- Hero: overall readiness + celebration state at 100% -->\n <mat-card class=\"setup-hero\" [class.celebrate]=\"status.percent === 100\">\n <div class=\"hero-content\" *ngIf=\"status.percent < 100\">\n <div class=\"hero-text\">\n <h1>{{ title }}</h1>\n <p>Complete these steps to get your system ready for day-to-day operation.</p>\n <span class=\"hero-counter\">{{ status.completed }} of {{ status.total }} steps completed</span>\n </div>\n <div class=\"hero-progress\">\n <span class=\"hero-percent\">{{ status.percent }}%</span>\n <mat-progress-bar mode=\"determinate\" [value]=\"status.percent\"></mat-progress-bar>\n </div>\n </div>\n <div class=\"hero-content celebration\" *ngIf=\"status.percent === 100\">\n <mat-icon class=\"celebrate-icon\">celebration</mat-icon>\n <div class=\"hero-text\">\n <h1>You're all set!</h1>\n <p>All setup steps are complete \u2014 your system is ready to operate.</p>\n </div>\n </div>\n </mat-card>\n\n <!-- Module picker (v2): choose what the business uses; optional modules toggle steps + menus -->\n <mat-card class=\"setup-modules\" *ngIf=\"modules.length > 0\">\n <div class=\"group-header\">\n <h2>Your modules</h2>\n <span class=\"group-counter\">Tap a module to turn it on or off \u2014 you can change this anytime.</span>\n </div>\n <!-- Changed (v3): grouped \u2014 the picker now carries the whole library catalog, so cards sit under their\n group heading (the app's own modules first). A backend without groups yields one unnamed group,\n which renders as the original flat grid. -->\n <div class=\"module-section\" *ngFor=\"let mgroup of moduleGroups\">\n <div class=\"module-section-head\">\n <span class=\"module-section-name\">{{ mgroup.name }}</span>\n <span class=\"module-section-count\">{{ mgroup.enabled }} of {{ mgroup.total }} on</span>\n </div>\n <div class=\"module-grid\">\n <div class=\"module-card\" *ngFor=\"let mod of mgroup.modules\"\n [class.enabled]=\"mod.enabled\" [class.core]=\"mod.core\" [class.busy]=\"togglingKey === mod.key\"\n (click)=\"toggleModule(mod)\">\n <div class=\"module-head\">\n <mat-icon class=\"module-icon\">{{ mod.icon || 'extension' }}</mat-icon>\n <mat-icon class=\"module-state\" [class.on]=\"mod.enabled\">{{ mod.enabled ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n </div>\n <div class=\"module-title\">{{ mod.title }}</div>\n <div class=\"module-description\">{{ mod.description }}</div>\n <span class=\"module-chip\" *ngIf=\"mod.core\">Always on</span>\n </div>\n </div>\n </div>\n <div class=\"module-actions\" *ngIf=\"!modulesConfirmed\">\n <button mat-flat-button color=\"primary\" [disabled]=\"togglingKey !== ''\" (click)=\"confirmModules()\">Confirm selection</button>\n <span class=\"module-hint\">Happy with this selection? Confirm it to complete the step below.</span>\n </div>\n </mat-card>\n\n <!-- Category groups, top-down \u2014 Changed: flat spa-checklist replaces the raised mat-accordion -->\n <mat-card class=\"setup-group\" *ngFor=\"let group of groups\">\n <div class=\"group-header\">\n <h2>{{ group.name }}</h2>\n <span class=\"group-counter\">{{ group.completed }} of {{ group.total }}</span>\n </div>\n <spa-checklist [config]=\"group.config\" [itemTemplate]=\"stepExtrasTpl\"></spa-checklist>\n </mat-card>\n\n <!-- Demo data (opt-in via setupConfig.demoData) \u2014 moved here off the per-app configuration pages -->\n <mat-card class=\"setup-group setup-demo\" *ngIf=\"showDemoData\">\n <div class=\"group-header\">\n <h2>Demo data</h2>\n <span class=\"group-counter\">Explore with example records, then clear them out</span>\n </div>\n <p class=\"demo-description\">Seeding fills the system with example records so you can try it out before capturing anything real. Removing deletes only those example records.</p>\n <div class=\"demo-actions\">\n <button mat-flat-button color=\"primary\" [disabled]=\"demoBusy\" (click)=\"seedDemoData()\"><mat-icon>add_circle</mat-icon> Seed demo data</button>\n <button mat-stroked-button color=\"warn\" [disabled]=\"demoBusy\" (click)=\"removeDemoData()\"><mat-icon>delete</mat-icon> Remove demo data</button>\n </div>\n </mat-card>\n\n</div>\n\n<!-- Projected into the expanded checklist body: predefined roles picker on the roles step (v2) -->\n<ng-template #stepExtrasTpl let-item>\n <div class=\"role-templates\" *ngIf=\"hasRoleTemplates(item.data) && roleTemplates.length > 0\">\n <div class=\"role-template\" *ngFor=\"let tpl of roleTemplates\" [class.created]=\"tpl.exists\" [class.selected]=\"!tpl.exists && selectedTemplates[tpl.key]\" (click)=\"toggleTemplate(tpl)\">\n <div class=\"role-head\">\n <mat-icon class=\"role-state\" [class.on]=\"tpl.exists || selectedTemplates[tpl.key]\">{{ (tpl.exists || selectedTemplates[tpl.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"role-name\">{{ tpl.name }}</span>\n <span class=\"role-created\" *ngIf=\"tpl.exists\"><mat-icon>check</mat-icon>Created</span>\n </div>\n <div class=\"role-description\">{{ tpl.description }}</div>\n </div>\n </div>\n\n <!-- Added (presets): the preset picker. Same row as the role picker above \u2014 same state icon, same 8px stack,\n same colours \u2014 plus ONE sentence and ONE control.\n Changed (presets P4): rendered through ONE shared template for both the notification step and the sign-off\n step, parameterised by the picker object. The two sit one above the other on the same page, so a second\n near-identical block would drift and every difference would read as a mistake. -->\n <ng-container *ngIf=\"pickerFor(item.data) as picker\">\n <ng-container *ngTemplateOutlet=\"presetPickerTpl; context: { $implicit: picker }\"></ng-container>\n </ng-container>\n</ng-template>\n\n<!-- The one picker body. `picker` carries the domain: its rows, its roles, its wording, its apply call. -->\n<ng-template #presetPickerTpl let-picker>\n <div class=\"preset-picker\" *ngIf=\"picker.loaded && picker.presets.length > 0\">\n\n <!-- No selectable role \u2192 we ask for one instead of offering ticks that would build an inert rule. The wording\n is the picker's own, because \"no roles\" and \"roles nobody is in\" are different situations. -->\n <div class=\"preset-empty\" *ngIf=\"picker.roles.length === 0\">{{ picker.emptyText }}</div>\n\n <ng-container *ngIf=\"picker.roles.length > 0\">\n <div class=\"preset-section\" *ngFor=\"let pgroup of picker.groups\">\n <div class=\"preset-section-head\">\n <span class=\"preset-section-name\">{{ pgroup.name }}</span>\n <span class=\"preset-section-count\">{{ pgroup.on }} of {{ pgroup.total }}</span>\n </div>\n <div class=\"preset-row\" *ngFor=\"let row of pgroup.rows\" role=\"checkbox\" tabindex=\"0\"\n [class.configured]=\"row.preset.exists\" [class.selected]=\"!row.preset.exists && picker.selected[row.preset.key]\"\n [attr.aria-checked]=\"row.preset.exists || !!picker.selected[row.preset.key]\" [attr.aria-disabled]=\"row.preset.exists\"\n (click)=\"togglePreset(picker, row.preset)\"\n (keydown.enter)=\"togglePreset(picker, row.preset)\"\n (keydown.space)=\"togglePreset(picker, row.preset); $event.preventDefault()\">\n <div class=\"preset-head\">\n <mat-icon class=\"preset-state\" [class.on]=\"row.preset.exists || picker.selected[row.preset.key]\">{{ (row.preset.exists || picker.selected[row.preset.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"preset-name\">{{ row.preset.name }}</span>\n <span class=\"preset-configured\" *ngIf=\"row.preset.exists\"><mat-icon>check</mat-icon>Configured</span>\n </div>\n <div class=\"preset-description\">{{ row.preset.description }}</div>\n <!-- The effect sentence comes from the server whole; `before` and `after` are its two halves either side\n of the {role} placeholder, so reading the line IS reading the configuration. The sign-off sentences\n carry their own deferred-notification caveat, which is why there is no extra note here. -->\n <div class=\"preset-effect\" *ngIf=\"!row.preset.exists\">{{ row.before }}<select class=\"preset-role\" [attr.aria-label]=\"picker.roleAria + row.preset.name\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n (change)=\"setPresetRole(picker, row.preset, $any($event.target).value)\">\n <option *ngFor=\"let role of picker.roles\" [value]=\"role.value\" [selected]=\"role.value === picker.role[row.preset.key]\">{{ role.name }}</option>\n </select>{{ row.after }}</div>\n <div class=\"preset-effect preset-done\" *ngIf=\"row.preset.exists\">{{ picker.doneText }}</div>\n </div>\n </div>\n\n <!-- One page-level channel choice, notifications only. In-app is the only channel that always works; SMS is a\n stub and is never offered; an approval request is always in-app, so the sign-off picker has no such line. -->\n <div class=\"preset-email\" *ngIf=\"picker.showEmail\" role=\"checkbox\" tabindex=\"0\" [attr.aria-checked]=\"includeEmail\"\n (click)=\"toggleIncludeEmail()\"\n (keydown.enter)=\"toggleIncludeEmail()\"\n (keydown.space)=\"toggleIncludeEmail(); $event.preventDefault()\">\n <mat-icon class=\"preset-state\" [class.on]=\"includeEmail\">{{ includeEmail ? 'check_box' : 'check_box_outline_blank' }}</mat-icon>\n <span class=\"preset-email-label\">Also send these by email</span>\n <span class=\"preset-email-hint\">In-app always. Email needs your mail settings.</span>\n </div>\n </ng-container>\n </div>\n</ng-template>\n\n<!-- Graceful empty state (feature disabled or status unavailable) -->\n<div class=\"setup-empty\" *ngIf=\"!status\">\n <mat-icon>rocket_launch</mat-icon>\n <p>Setup status is not available yet.</p>\n</div>\n", styles: [".setup-page{max-width:860px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.setup-hero{padding:24px}.hero-content{display:flex;align-items:center;justify-content:space-between;gap:24px;flex-wrap:wrap}.hero-text h1{margin:0 0 4px;font-size:24px}.hero-text p{margin:0 0 8px;color:#0009}.hero-counter{font-size:13px;color:#0009}.hero-progress{flex:1;min-width:220px;max-width:340px}.hero-percent{display:block;font-size:28px;font-weight:600;color:#2e7d32;margin-bottom:6px;text-align:right}.hero-progress mat-progress-bar{height:10px;border-radius:5px}.setup-hero.celebrate{background:linear-gradient(135deg,#e8f5e9,#f1f8e9)}.celebration{justify-content:flex-start}.celebrate-icon{font-size:48px;width:48px;height:48px;color:#2e7d32;animation:celebrate-pop .6s ease-out}@keyframes celebrate-pop{0%{transform:scale(.3);opacity:0}70%{transform:scale(1.15)}to{transform:scale(1);opacity:1}}.setup-group{padding:16px}.group-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.group-header h2{margin:0;font-size:17px}.group-counter{font-size:12px;color:#0000008c}.setup-modules{padding:16px}.module-section+.module-section{margin-top:18px}.module-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.module-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.module-section-count{font-size:11px;color:#00000073;white-space:nowrap}.module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;margin-top:8px}.module-card{border:1px solid rgba(0,0,0,.12);border-radius:10px;padding:12px;cursor:pointer;transition:border-color .15s,background .15s,opacity .15s;opacity:.72;position:relative}.module-card:hover{border-color:#90a4ae}.module-card.enabled{border-color:#4caf50;background:#f6fbf6;opacity:1}.module-card.core{cursor:default}.module-card.busy{pointer-events:none;opacity:.5}.module-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.module-icon{color:#546e7a}.module-card.enabled .module-icon{color:#2e7d32}.module-state{color:#b0bec5}.module-state.on{color:#4caf50}.module-title{font-weight:600;font-size:14px;margin-bottom:4px}.module-description{font-size:12px;color:#0009;min-height:30px}.module-chip{display:inline-block;margin-top:8px;background:#eceff1;color:#546e7a;border-radius:12px;padding:2px 10px;font-size:11px}.module-actions{display:flex;align-items:center;gap:8px 12px;margin-top:14px;flex-wrap:wrap}.module-actions button{flex-shrink:0}.module-hint{font-size:12px;color:#0000008c}.role-templates{display:flex;flex-direction:column;gap:8px;margin:0 0 12px}.role-template{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.role-template:hover{border-color:#90a4ae}.role-template.selected{border-color:#4caf50;background:#f6fbf6}.role-template.created{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.role-head{display:flex;align-items:center;gap:8px}.role-state{color:#b0bec5;flex-shrink:0}.role-state.on{color:#4caf50}.role-name{font-weight:500;font-size:13px}.role-created{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32}.role-created mat-icon{font-size:16px;width:16px;height:16px}.role-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-picker{display:flex;flex-direction:column;gap:14px;margin:0 0 12px}.preset-empty{font-size:12px;line-height:18px;color:#0009;max-width:62ch}.preset-section{display:flex;flex-direction:column;gap:8px}.preset-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.preset-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.preset-section-count{font-size:11px;color:#00000073;white-space:nowrap}.preset-row{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.preset-row:hover{border-color:#90a4ae}.preset-row:focus-visible{outline:2px solid #4caf50;outline-offset:2px}.preset-row.selected{border-color:#4caf50;background:#f6fbf6}.preset-row.configured{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.preset-head{display:flex;align-items:center;gap:8px}.preset-state{color:#b0bec5;flex-shrink:0}.preset-state.on{color:#4caf50}.preset-name{font-weight:500;font-size:13px}.preset-configured{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32;white-space:nowrap}.preset-configured mat-icon{font-size:16px;width:16px;height:16px}.preset-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-effect{font-size:12px;line-height:22px;color:#000000c7;margin:4px 0 0 32px}.preset-done{color:#2e7d32}.preset-role{font-family:inherit;font-size:12px;font-weight:500;color:#2e7d32;background:#fff;border:1px solid #c8e6c9;border-radius:6px;padding:2px 4px;margin:0 2px;max-width:100%;cursor:pointer}.preset-role:hover{border-color:#4caf50}.preset-role:focus-visible{outline:2px solid #4caf50;outline-offset:1px}.preset-email{display:flex;align-items:center;gap:8px;flex-wrap:wrap;cursor:pointer;padding:2px 0}.preset-email:focus-visible{outline:2px solid #4caf50;outline-offset:2px;border-radius:6px}.preset-email-label{font-size:13px;font-weight:500}.preset-email-hint{font-size:12px;color:#0000008c}.setup-empty{text-align:center;padding:48px 16px;color:#00000080}.setup-empty mat-icon{font-size:40px;width:40px;height:40px}.demo-description{margin:8px 0 14px;font-size:13px;color:#0009}.demo-actions{display:flex;gap:12px;flex-wrap:wrap}@media (max-width: 700px){.module-hint{flex:0 0 100%}.preset-role{display:block;width:100%;margin:4px 0 0;padding:6px 8px}.preset-email-hint{flex:0 0 100%}}\n"] }]
12522
+ args: [{ selector: 'spa-setup-guide', standalone: false, template: "<div class=\"setup-page\" *ngIf=\"status\">\n\n <!-- Hero: overall readiness + celebration state at 100% -->\n <mat-card class=\"setup-hero\" [class.celebrate]=\"status.percent === 100\">\n <div class=\"hero-content\" *ngIf=\"status.percent < 100\">\n <div class=\"hero-text\">\n <h1>{{ title }}</h1>\n <p>Complete these steps to get your system ready for day-to-day operation.</p>\n <span class=\"hero-counter\">{{ status.completed }} of {{ status.total }} steps completed</span>\n </div>\n <div class=\"hero-progress\">\n <span class=\"hero-percent\">{{ status.percent }}%</span>\n <mat-progress-bar mode=\"determinate\" [value]=\"status.percent\"></mat-progress-bar>\n </div>\n </div>\n <div class=\"hero-content celebration\" *ngIf=\"status.percent === 100\">\n <mat-icon class=\"celebrate-icon\">celebration</mat-icon>\n <div class=\"hero-text\">\n <h1>You're all set!</h1>\n <p>All setup steps are complete \u2014 your system is ready to operate.</p>\n </div>\n </div>\n </mat-card>\n\n <!-- Module picker (v2): choose what the business uses; optional modules toggle steps + menus -->\n <mat-card class=\"setup-modules\" *ngIf=\"modules.length > 0\">\n <div class=\"group-header\">\n <h2>Your modules</h2>\n <span class=\"group-counter\">Tap a module to turn it on or off \u2014 you can change this anytime.</span>\n </div>\n <!-- Changed (v3): grouped \u2014 the picker now carries the whole library catalog, so cards sit under their\n group heading (the app's own modules first). A backend without groups yields one unnamed group,\n which renders as the original flat grid. -->\n <div class=\"module-section\" *ngFor=\"let mgroup of moduleGroups\">\n <div class=\"module-section-head\">\n <span class=\"module-section-name\">{{ mgroup.name }}</span>\n <span class=\"module-section-count\">{{ mgroup.enabled }} of {{ mgroup.total }} on</span>\n </div>\n <div class=\"module-grid\">\n <div class=\"module-card\" *ngFor=\"let mod of mgroup.modules\"\n [class.enabled]=\"mod.enabled\" [class.core]=\"mod.core\" [class.busy]=\"togglingKey === mod.key\"\n (click)=\"toggleModule(mod)\">\n <div class=\"module-head\">\n <mat-icon class=\"module-icon\">{{ mod.icon || 'extension' }}</mat-icon>\n <mat-icon class=\"module-state\" [class.on]=\"mod.enabled\">{{ mod.enabled ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n </div>\n <div class=\"module-title\">{{ mod.title }}</div>\n <div class=\"module-description\">{{ mod.description }}</div>\n <span class=\"module-chip\" *ngIf=\"mod.core\">Always on</span>\n </div>\n </div>\n </div>\n <div class=\"module-actions\" *ngIf=\"!modulesConfirmed\">\n <button mat-flat-button color=\"primary\" [disabled]=\"togglingKey !== ''\" (click)=\"confirmModules()\">Confirm selection</button>\n <span class=\"module-hint\">Happy with this selection? Confirm it to complete the step below.</span>\n </div>\n </mat-card>\n\n <!-- Category groups, top-down \u2014 Changed: flat spa-checklist replaces the raised mat-accordion -->\n <mat-card class=\"setup-group\" *ngFor=\"let group of groups\">\n <div class=\"group-header\">\n <h2>{{ group.name }}</h2>\n <span class=\"group-counter\">{{ group.completed }} of {{ group.total }}</span>\n </div>\n <spa-checklist [config]=\"group.config\" [itemTemplate]=\"stepExtrasTpl\"></spa-checklist>\n </mat-card>\n\n <!-- Demo data (opt-in via setupConfig.demoData) \u2014 moved here off the per-app configuration pages -->\n <mat-card class=\"setup-group setup-demo\" *ngIf=\"showDemoData\">\n <div class=\"group-header\">\n <h2>Demo data</h2>\n <span class=\"group-counter\">Explore with example records, then clear them out</span>\n </div>\n <p class=\"demo-description\">Seeding fills the system with example records so you can try it out before capturing anything real. Removing deletes only those example records.</p>\n <div class=\"demo-actions\">\n <button mat-flat-button color=\"primary\" [disabled]=\"demoBusy\" (click)=\"seedDemoData()\"><mat-icon>add_circle</mat-icon> Seed demo data</button>\n <button mat-stroked-button color=\"warn\" [disabled]=\"demoBusy\" (click)=\"removeDemoData()\"><mat-icon>delete</mat-icon> Remove demo data</button>\n </div>\n </mat-card>\n\n</div>\n\n<!-- Projected into the expanded checklist body: predefined roles picker on the roles step (v2) -->\n<ng-template #stepExtrasTpl let-item>\n <div class=\"role-templates\" *ngIf=\"hasRoleTemplates(item.data) && roleTemplates.length > 0\">\n <div class=\"role-template\" *ngFor=\"let tpl of roleTemplates\" [class.created]=\"tpl.exists\" [class.selected]=\"!tpl.exists && selectedTemplates[tpl.key]\" (click)=\"toggleTemplate(tpl)\">\n <div class=\"role-head\">\n <mat-icon class=\"role-state\" [class.on]=\"tpl.exists || selectedTemplates[tpl.key]\">{{ (tpl.exists || selectedTemplates[tpl.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"role-name\">{{ tpl.name }}</span>\n <span class=\"role-created\" *ngIf=\"tpl.exists\"><mat-icon>check</mat-icon>Created</span>\n </div>\n <div class=\"role-description\">{{ tpl.description }}</div>\n </div>\n </div>\n\n <!-- Added (presets): the preset picker. Same row as the role picker above \u2014 same state icon, same 8px stack,\n same colours \u2014 plus ONE sentence and ONE control.\n Changed (presets P4): rendered through ONE shared template for both the notification step and the sign-off\n step, parameterised by the picker object. The two sit one above the other on the same page, so a second\n near-identical block would drift and every difference would read as a mistake. -->\n <ng-container *ngIf=\"pickerFor(item.data) as picker\">\n <ng-container *ngTemplateOutlet=\"presetPickerTpl; context: { $implicit: picker }\"></ng-container>\n </ng-container>\n</ng-template>\n\n<!-- The one picker body. `picker` carries the domain: its rows, its roles, its wording, its apply call. -->\n<ng-template #presetPickerTpl let-picker>\n <div class=\"preset-picker\" *ngIf=\"picker.loaded && picker.presets.length > 0\">\n\n <!-- No selectable role \u2192 we ask for one instead of offering ticks that would build an inert rule. The wording\n is the picker's own, because \"no roles\" and \"roles nobody is in\" are different situations. -->\n <div class=\"preset-empty\" *ngIf=\"picker.roles.length === 0\">{{ picker.emptyText }}</div>\n\n <ng-container *ngIf=\"picker.roles.length > 0\">\n <div class=\"preset-section\" *ngFor=\"let pgroup of picker.groups\">\n <div class=\"preset-section-head\">\n <span class=\"preset-section-name\">{{ pgroup.name }}</span>\n <span class=\"preset-section-count\">{{ pgroup.on }} of {{ pgroup.total }}</span>\n </div>\n <div class=\"preset-row\" *ngFor=\"let row of pgroup.rows\" role=\"checkbox\" tabindex=\"0\"\n [class.configured]=\"row.preset.exists\" [class.selected]=\"!row.preset.exists && picker.selected[row.preset.key]\"\n [attr.aria-checked]=\"row.preset.exists || !!picker.selected[row.preset.key]\" [attr.aria-disabled]=\"row.preset.exists\"\n (click)=\"togglePreset(picker, row.preset)\"\n (keydown.enter)=\"togglePreset(picker, row.preset)\"\n (keydown.space)=\"togglePreset(picker, row.preset); $event.preventDefault()\">\n <div class=\"preset-head\">\n <mat-icon class=\"preset-state\" [class.on]=\"row.preset.exists || picker.selected[row.preset.key]\">{{ (row.preset.exists || picker.selected[row.preset.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"preset-name\">{{ row.preset.name }}</span>\n <span class=\"preset-configured\" *ngIf=\"row.preset.exists\"><mat-icon>check</mat-icon>Configured</span>\n </div>\n <div class=\"preset-description\">{{ row.preset.description }}</div>\n <!-- The effect sentence comes from the server whole; `before` and `after` are its two halves either side\n of the {role} placeholder, so reading the line IS reading the configuration. The sign-off sentences\n carry their own deferred-notification caveat, which is why there is no extra note here. -->\n <div class=\"preset-effect\" *ngIf=\"!row.preset.exists\">{{ row.before }}<select class=\"preset-role\" [attr.aria-label]=\"picker.roleAria + row.preset.name\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n (change)=\"setPresetRole(picker, row.preset, $any($event.target).value)\">\n <option *ngFor=\"let role of picker.roles\" [value]=\"role.value\" [selected]=\"role.value === picker.role[row.preset.key]\">{{ role.name }}</option>\n </select>{{ row.after }}</div>\n <div class=\"preset-effect preset-done\" *ngIf=\"row.preset.exists\">{{ picker.doneText }}</div>\n </div>\n </div>\n\n <!-- One page-level channel choice, notifications only. In-app is the only channel that always works; SMS is a\n stub and is never offered; an approval request is always in-app, so the sign-off picker has no such line. -->\n <div class=\"preset-email\" *ngIf=\"picker.showEmail\" role=\"checkbox\" tabindex=\"0\" [attr.aria-checked]=\"includeEmail\"\n (click)=\"toggleIncludeEmail()\"\n (keydown.enter)=\"toggleIncludeEmail()\"\n (keydown.space)=\"toggleIncludeEmail(); $event.preventDefault()\">\n <mat-icon class=\"preset-state\" [class.on]=\"includeEmail\">{{ includeEmail ? 'check_box' : 'check_box_outline_blank' }}</mat-icon>\n <span class=\"preset-email-label\">Also send these by email</span>\n <span class=\"preset-email-hint\">In-app always. Email needs your mail settings.</span>\n </div>\n </ng-container>\n </div>\n</ng-template>\n\n<!-- Graceful empty state (feature disabled or status unavailable) -->\n<div class=\"setup-empty\" *ngIf=\"!status\">\n <mat-icon>rocket_launch</mat-icon>\n <p>Setup status is not available yet.</p>\n</div>\n", styles: [".setup-page{max-width:860px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.setup-hero{padding:24px}.hero-content{display:flex;align-items:center;justify-content:space-between;gap:24px;flex-wrap:wrap}.hero-text h1{margin:0 0 4px;font-size:24px}.hero-text p{margin:0 0 8px;color:#0009}.hero-counter{font-size:13px;color:#0009}.hero-progress{flex:1;min-width:220px;max-width:340px}.hero-percent{display:block;font-size:28px;font-weight:600;color:#2e7d32;margin-bottom:6px;text-align:right}.hero-progress mat-progress-bar{height:10px;border-radius:5px}.setup-hero.celebrate{background:linear-gradient(135deg,#e8f5e9,#f1f8e9)}.celebration{justify-content:flex-start}.celebrate-icon{font-size:48px;width:48px;height:48px;color:#2e7d32;animation:celebrate-pop .6s ease-out}@keyframes celebrate-pop{0%{transform:scale(.3);opacity:0}70%{transform:scale(1.15)}to{transform:scale(1);opacity:1}}.setup-group{padding:16px}.group-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.group-header h2{margin:0;font-size:17px}.group-counter{font-size:12px;color:#0000008c}.setup-modules{padding:16px}.module-section+.module-section{margin-top:18px}.module-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.module-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.module-section-count{font-size:11px;color:#00000073;white-space:nowrap}.module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;margin-top:8px}.module-card{border:1px solid rgba(0,0,0,.12);border-radius:10px;padding:12px;cursor:pointer;transition:border-color .15s,background .15s,opacity .15s;opacity:.72;position:relative}.module-card:hover{border-color:#90a4ae}.module-card.enabled{border-color:#4caf50;background:#f6fbf6;opacity:1}.module-card.core{cursor:default}.module-card.busy{pointer-events:none;opacity:.5}.module-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.module-icon{color:#546e7a}.module-card.enabled .module-icon{color:#2e7d32}.module-state{color:#b0bec5}.module-state.on{color:#4caf50}.module-title{font-weight:600;font-size:14px;margin-bottom:4px}.module-description{font-size:12px;color:#0009;min-height:30px}.module-chip{display:inline-block;margin-top:8px;background:#eceff1;color:#546e7a;border-radius:12px;padding:2px 10px;font-size:11px}.module-actions{display:flex;align-items:center;gap:8px 12px;margin-top:14px;flex-wrap:wrap}.module-actions button{flex-shrink:0}.module-hint{font-size:12px;color:#0000008c}.role-templates{display:flex;flex-direction:column;gap:8px;margin:0 0 12px}.role-template{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.role-template:hover{border-color:#90a4ae}.role-template.selected{border-color:#4caf50;background:#f6fbf6}.role-template.created{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.role-head{display:flex;align-items:center;gap:8px}.role-state{color:#b0bec5;flex-shrink:0}.role-state.on{color:#4caf50}.role-name{font-weight:500;font-size:13px}.role-created{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32}.role-created mat-icon{font-size:16px;width:16px;height:16px}.role-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-picker{display:flex;flex-direction:column;gap:14px;margin:0 0 12px}.preset-empty{font-size:12px;line-height:18px;color:#0009;max-width:62ch}.preset-section{display:flex;flex-direction:column;gap:8px}.preset-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.preset-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.preset-section-count{font-size:11px;color:#00000073;white-space:nowrap}.preset-row{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.preset-row:hover{border-color:#90a4ae}.preset-row:focus-visible{outline:2px solid #4caf50;outline-offset:2px}.preset-row.selected{border-color:#4caf50;background:#f6fbf6}.preset-row.configured{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.preset-head{display:flex;align-items:center;gap:8px}.preset-state{color:#b0bec5;flex-shrink:0}.preset-state.on{color:#4caf50}.preset-name{font-weight:500;font-size:13px}.preset-configured{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32;white-space:nowrap}.preset-configured mat-icon{font-size:16px;width:16px;height:16px}.preset-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-effect{font-size:12px;line-height:22px;color:#000000c7;margin:4px 0 0 32px}.preset-done{color:#2e7d32}.preset-role{font-family:inherit;font-size:12px;font-weight:500;color:#2e7d32;background:#fff;border:1px solid #c8e6c9;border-radius:6px;padding:2px 4px;margin:0 2px;max-width:100%;cursor:pointer}.preset-role:hover{border-color:#4caf50}.preset-role:focus-visible{outline:2px solid #4caf50;outline-offset:1px}.preset-email{display:flex;align-items:center;gap:8px;flex-wrap:wrap;cursor:pointer;padding:2px 0}.preset-email:focus-visible{outline:2px solid #4caf50;outline-offset:2px;border-radius:6px}.preset-email-label{font-size:13px;font-weight:500}.preset-email-hint{font-size:12px;color:#0000008c}.setup-empty{text-align:center;padding:48px 16px;color:#00000080}.setup-empty mat-icon{font-size:40px;width:40px;height:40px}.demo-description{margin:8px 0 14px;font-size:13px;color:#0009}.demo-actions{display:flex;gap:12px;flex-wrap:wrap}@media (max-width: 700px){.setup-page{padding:10px 0;gap:12px}.setup-hero{padding:12px}.setup-modules,.setup-group{padding:10px}.hero-content{gap:10px}.hero-text h1{font-size:20px}.setup-hero:not(.celebrate) .hero-text p{display:none}.hero-counter{font-size:12px}.hero-progress{min-width:0;max-width:none}.hero-percent{font-size:22px;margin-bottom:4px}.hero-progress mat-progress-bar{height:8px;border-radius:4px}.celebrate-icon{font-size:36px;width:36px;height:36px}.group-header{margin-bottom:6px;gap:2px 8px}.group-header h2{font-size:15px}.group-counter{font-size:11px}.setup-modules>.group-header,.setup-demo>.group-header{flex-wrap:wrap}.setup-modules>.group-header .group-counter,.setup-demo>.group-header .group-counter{flex:1 0 100%}.module-section+.module-section{margin-top:12px}.module-section-head{padding-bottom:4px}.module-grid{gap:8px;margin-top:6px}.module-card{padding:10px}.module-head{margin-bottom:4px}.module-title{font-size:13.5px;margin-bottom:2px}.module-description{min-height:0}.module-chip{margin-top:6px}.module-actions{margin-top:10px}.role-templates,.preset-picker{gap:6px;margin-bottom:8px}.role-template,.preset-row{padding:8px 10px}.role-description,.preset-description,.preset-effect{margin-left:0}.preset-section{gap:6px}.preset-section-head{padding-bottom:4px}.demo-description{margin:6px 0 10px;font-size:12.5px}.demo-actions{gap:8px}.setup-empty{padding:28px 12px}.module-hint{flex:0 0 100%}.preset-role{display:block;width:100%;margin:4px 0 0;padding:6px 8px}.preset-email-hint{flex:0 0 100%}}\n"] }]
12436
12523
  }] });
12437
12524
 
12438
12525
  // Quiet Loading — perceived-progress engine (FSD D1).
@@ -13342,7 +13429,7 @@ class TextSingleComponent {
13342
13429
  let button = this.detailsConfig.buttons.find(b => b.name === mode);
13343
13430
  button.detailsConfig = this.detailsConfig;
13344
13431
  this.dialogService.openDefaultDetailsDialog(button, dynamicData).subscribe(result => {
13345
- if (result.message === 'success') {
13432
+ if (result?.message === 'success') { // Changed: a dismissed quick-add dialog closes with undefined — read through it rather than throwing
13346
13433
  // Added (dropdown quick-add): auto-select the record the user just created; refresh's initFilter
13347
13434
  // then resolves the display text once the new option is in the list
13348
13435
  if (mode === 'create')
@@ -13542,6 +13629,9 @@ class SelectCommonComponent {
13542
13629
  this.loadIndicator = new FieldLoadIndicator();
13543
13630
  // Added (dropdown quick-add): sentinel value for the always-first "add new" option — never a real option value
13544
13631
  this.ADD_NEW_OPTION = "__spa_add_new__";
13632
+ // Added: the deferred default must not fire into a torn-down field (a dialog closed while its options were
13633
+ // still settling would otherwise write onto data nobody is showing any more)
13634
+ this.destroyed = false;
13545
13635
  this.width = "100%";
13546
13636
  this.readonly = false;
13547
13637
  this.required = true;
@@ -13579,6 +13669,24 @@ class SelectCommonComponent {
13579
13669
  // Added (dropdown quick-add): keep the restore point in sync with parent-driven value changes
13580
13670
  if (this.value !== this.ADD_NEW_OPTION)
13581
13671
  this.previousValue = this.value;
13672
+ // Added: defaultFirstValue was inert on every select that names a masterField. setDefaultValue() has only
13673
+ // two entry points — ngOnInit (options are still empty then) and the field.optionsSubject subscription —
13674
+ // and form.component.ts only creates an optionsSubject for fields WITHOUT a masterField, so a child field
13675
+ // has no second entry point at all. Its options arrive later, as a plain [options] input mutation from
13676
+ // updateChildOptions(), which lands here in ngOnChanges and nowhere else. Reproduced on shift-spa's driver
13677
+ // create dialog: Department self-selected, Position stayed empty with exactly one option in its list.
13678
+ // Deliberately scoped to masterField selects: every other select already gets its default through one of
13679
+ // the two existing entry points, so nothing else's timing moves. setDefaultValue() still declines when a
13680
+ // value is already set, so this can only fill a blank, never overwrite a choice.
13681
+ // Deferred by one microtask on purpose: setDefaultValue() emits valueChange, and emitting from inside
13682
+ // ngOnChanges writes the parent's data[field.name] after the parent's bindings were already checked, which
13683
+ // is NG0100 — the same shape spa-select-multi's selectAll had. The microtask runs once the pass has
13684
+ // finished, so the new value is picked up by the next one instead of invalidating this one.
13685
+ if (this.masterField && this.options !== this.defaultedOptions) {
13686
+ this.defaultedOptions = this.options;
13687
+ Promise.resolve().then(() => { if (!this.destroyed)
13688
+ this.setDefaultValue(); });
13689
+ }
13582
13690
  if (this.readonlyMode != "" && this.options.length > 0) {
13583
13691
  this.displayValue = this.options.filter((m) => m[`${this.optionValue}`] == this.value)[0][`${this.optionDisplay}`];
13584
13692
  }
@@ -13749,6 +13857,7 @@ class SelectCommonComponent {
13749
13857
  });
13750
13858
  }
13751
13859
  ngOnDestroy() {
13860
+ this.destroyed = true; // Added: cancels a master-driven default still queued on the microtask queue
13752
13861
  if (this.subscription) {
13753
13862
  this.subscription.unsubscribe();
13754
13863
  }
@@ -13858,8 +13967,21 @@ class TextMultiComponent {
13858
13967
  this.loadIndicator = new FieldLoadIndicator();
13859
13968
  this.hoverChange = new EventEmitter();
13860
13969
  } // Changed: injected ApiErrorService so a failed option load is no longer silent
13970
+ // Added: identical normalisation to SelectMultiComponent.toValueList, deliberately kept the same shape and
13971
+ // name so the two multi-value components cannot drift. `value` is documented as a ';'-delimited string, but
13972
+ // the truthiness guard below used to hand anything truthy straight to .split() — an array (any form whose
13973
+ // backend DTO takes a List<int> rewrites the bound property in place before posting) or a bare scalar both
13974
+ // threw "split is not a function". select-multi hit exactly that in piglet; nothing feeds text-multi an array
13975
+ // today, so this is hardening ahead of the first caller that does, not a repair.
13976
+ toValueList(raw) {
13977
+ if (Array.isArray(raw))
13978
+ return raw.filter(v => v !== null && v !== undefined && String(v).trim() !== '').map(v => String(v).trim()); // Added: the List<int> shape
13979
+ if (!raw)
13980
+ return []; // Added: preserves the previous falsy-means-nothing-entered rule for null/undefined/''
13981
+ return String(raw).split(';').filter(v => v.trim() !== '').map(v => v.trim()); // Changed: String() first, so a bare scalar no longer throws either
13982
+ }
13861
13983
  ngOnInit() {
13862
- this.values = this.value ? this.value.split(';').filter(val => val.trim() !== '') : [];
13984
+ this.values = this.toValueList(this.value); // Changed: was this.value.split(';') directly on an @Input of unknown shape
13863
13985
  this.setupAutoComplete();
13864
13986
  this.getData(this.loadAction);
13865
13987
  }
@@ -14057,6 +14179,7 @@ class SelectMultiComponent {
14057
14179
  this.selectAll = false; // Changed: auto-select all options when no value is set
14058
14180
  this.control = new FormControl([]);
14059
14181
  this.selectedValues = [];
14182
+ this.destroyed = false; // Added: a field torn down before the microtask runs must not emit into a dead parent
14060
14183
  this.isHovered = false;
14061
14184
  // Added: drives the suffix refresh icon — held visible and spinning while the loadAction is in flight
14062
14185
  this.loadIndicator = new FieldLoadIndicator();
@@ -14086,13 +14209,23 @@ class SelectMultiComponent {
14086
14209
  }
14087
14210
  this.initializeValues();
14088
14211
  }
14212
+ // Added: `value` is documented as a ';'-delimited string, but real callers legitimately hold an array —
14213
+ // any form whose backend DTO takes a List<int> rewrites the bound property in place before posting
14214
+ // (piglet has four: wean, sell, move, treat). That array flowed straight back down [(value)] and
14215
+ // this.value.split(';') threw "split is not a function" on every subsequent change-detection pass.
14216
+ // Normalise on the way in rather than type-guarding at one call site, and keep emitting the ';' string
14217
+ // on the way out so no existing consumer's parsing changes.
14218
+ toValueList(raw) {
14219
+ if (Array.isArray(raw))
14220
+ return raw.filter(v => v !== null && v !== undefined && String(v).trim() !== '').map(v => String(v).trim()); // Added: the List<int> shape
14221
+ if (!raw)
14222
+ return []; // Added: preserves the previous falsy-means-nothing-selected rule for null/undefined/''
14223
+ return String(raw).split(';').filter(v => v.trim() !== '').map(v => v.trim()); // Changed: String() first, so a bare scalar (a single id) no longer throws either
14224
+ }
14089
14225
  initializeValues() {
14090
- if (this.value) {
14091
- const values = this.value.split(';')
14092
- .filter(v => v.trim())
14093
- .map(v => this.options.find(opt => opt[this.optionValue]?.toString() === v)
14094
- ? v : null)
14095
- .filter(v => v !== null);
14226
+ const requested = this.toValueList(this.value); // Changed: was this.value.split(';') directly on an @Input of unknown shape
14227
+ if (requested.length > 0) { // Changed: gate on the parsed request, not on truthiness of a raw value that may be an array
14228
+ const values = requested.filter(v => this.options?.some(opt => opt[this.optionValue]?.toString() === v)); // Changed: same "drop values not in options" rule, expressed without the map-to-null round trip
14096
14229
  this.selectedValues = values;
14097
14230
  this.control.setValue(values);
14098
14231
  }
@@ -14101,13 +14234,24 @@ class SelectMultiComponent {
14101
14234
  const allValues = this.options.map(opt => opt[this.optionValue]?.toString());
14102
14235
  this.selectedValues = allValues;
14103
14236
  this.control.setValue(allValues);
14104
- this.updateValue();
14237
+ this.publishAutoSelection(); // Changed: was this.updateValue() — see publishAutoSelection for why the emit cannot happen here
14105
14238
  }
14106
14239
  else {
14107
14240
  this.selectedValues = [];
14108
14241
  this.control.setValue([]);
14109
14242
  }
14110
14243
  }
14244
+ // Added: initializeValues runs from ngOnChanges/ngOnInit — inside the parent's change-detection pass.
14245
+ // Emitting valueChange there writes the parent's bound property after its bindings were already checked,
14246
+ // which is exactly what NG0100 ExpressionChangedAfterItHasBeenCheckedError reports. It fired on every
14247
+ // selectAll open, independently of the array crash above. Deferring by one microtask moves the parent
14248
+ // write into the next turn. Same idiom as day-book.component.ts's showLoading.
14249
+ // Only the AUTOMATIC selection defers — selectionChange() still emits synchronously, because a user
14250
+ // click is not inside a checked pass and callers rely on the value being current when they read it.
14251
+ publishAutoSelection() {
14252
+ Promise.resolve().then(() => { if (!this.destroyed)
14253
+ this.updateValue(); });
14254
+ }
14111
14255
  selectionChange(event) {
14112
14256
  this.selectedValues = event.value || [];
14113
14257
  this.updateValue();
@@ -14169,6 +14313,7 @@ class SelectMultiComponent {
14169
14313
  });
14170
14314
  }
14171
14315
  ngOnDestroy() {
14316
+ this.destroyed = true; // Added: suppresses a deferred selectAll emit that lands after teardown
14172
14317
  this.loadIndicator.destroy(); // Added: clear any pending delay/hold timer on a field torn down mid-read
14173
14318
  }
14174
14319
  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 }); }
@@ -15171,7 +15316,7 @@ class TableHeaderComponent {
15171
15316
  this.customClick.emit("upload");
15172
15317
  this.dialogService.openDefaultDialog(this.uploadDetailsDialog).afterClosed.subscribe(result => {
15173
15318
  // console.log(result)
15174
- if (result.message == 'success') {
15319
+ if (result?.message == 'success') { // Changed: a dismissed upload dialog closes with undefined — read through it rather than throwing
15175
15320
  this.refreshClick.emit();
15176
15321
  this.messageService.toast("Upload successful");
15177
15322
  }
@@ -15314,11 +15459,11 @@ class CheckComponent {
15314
15459
  this.infoClick.emit();
15315
15460
  }
15316
15461
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CheckComponent, deps: [{ token: MessageService }], target: i0.ɵɵFactoryTarget.Component }); }
15317
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: CheckComponent, isStandalone: false, selector: "spa-check", inputs: { readonly: "readonly", display: "display", value: "value", infoMessage: "infoMessage" }, outputs: { valueChange: "valueChange", click: "click", check: "check", uncheck: "uncheck", infoClick: "infoClick" }, ngImport: i0, template: "\n\n<mat-checkbox color=\"primary\" [(ngModel)]=\"value\" (change)=\"changed()\" (click)=\"clicked()\" [disabled]=\"readonly\">{{display}}</mat-checkbox>\n\n<!-- <div class=\"suffix-icons\">\n\n <mat-checkbox color=\"primary\" [(ngModel)]=\"value\" (change)=\"changed()\" (click)=\"clicked()\" [disabled]=\"readonly\">{{display}}</mat-checkbox>\n\n <mat-icon *ngIf=\"infoMessage\" (click)=\"onInfoClick($event)\" matTooltip=\"Info\" matTooltipPosition=\"above\" style=\"color: steelblue;font-size: 15px;margin-left: 5px;margin-top: 8px;\">info</mat-icon>\n\n</div> -->\n\n", styles: [".suffix-icons{display:flex;align-items:center}\n"], dependencies: [{ 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$3.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }] }); }
15462
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: CheckComponent, isStandalone: false, selector: "spa-check", inputs: { readonly: "readonly", display: "display", value: "value", infoMessage: "infoMessage", hint: "hint" }, outputs: { valueChange: "valueChange", click: "click", check: "check", uncheck: "uncheck", infoClick: "infoClick" }, ngImport: i0, template: "\n\n<!-- Changed: checkbox + hint are wrapped so the host presents ONE flex item, not two.\n tin-styles.css's FORM-1/FORM-3 rule makes `spa-form spa-check` a flex row to centre it on the field\n control band; with the hint as a second child it became a sibling COLUMN beside the label instead of a\n line beneath it. Wrapping restores the single-item assumption that rule was written against, so the\n band alignment is preserved untouched and the hint stacks where check.component.css already aims it. -->\n<div class=\"check-body\">\n <mat-checkbox color=\"primary\" [(ngModel)]=\"value\" (change)=\"changed()\" (click)=\"clicked()\" [disabled]=\"readonly\">{{display}}</mat-checkbox>\n\n <!-- Added: the hint line. Sits under the label rather than beside it so a full sentence can be read without\n stretching the grid column, and is indented to the label's own left edge, not the box's. -->\n <div class=\"check-hint\" *ngIf=\"hint\">{{hint}}</div>\n</div>\n\n<!-- <div class=\"suffix-icons\">\n\n <mat-checkbox color=\"primary\" [(ngModel)]=\"value\" (change)=\"changed()\" (click)=\"clicked()\" [disabled]=\"readonly\">{{display}}</mat-checkbox>\n\n <mat-icon *ngIf=\"infoMessage\" (click)=\"onInfoClick($event)\" matTooltip=\"Info\" matTooltipPosition=\"above\" style=\"color: steelblue;font-size: 15px;margin-left: 5px;margin-top: 8px;\">info</mat-icon>\n\n</div> -->\n\n", styles: [".suffix-icons{display:flex;align-items:center}.check-body{display:inline-flex;flex-direction:column;align-items:flex-start}.check-hint{font-size:12px;line-height:1.35;color:#0000008c;margin:1px 0 0 calc(var(--mat-checkbox-state-layer-size, var(--mdc-checkbox-state-layer-size, 40px)) + 4px)}\n"], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { 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$3.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }] }); }
15318
15463
  }
15319
15464
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CheckComponent, decorators: [{
15320
15465
  type: Component,
15321
- args: [{ selector: 'spa-check', standalone: false, template: "\n\n<mat-checkbox color=\"primary\" [(ngModel)]=\"value\" (change)=\"changed()\" (click)=\"clicked()\" [disabled]=\"readonly\">{{display}}</mat-checkbox>\n\n<!-- <div class=\"suffix-icons\">\n\n <mat-checkbox color=\"primary\" [(ngModel)]=\"value\" (change)=\"changed()\" (click)=\"clicked()\" [disabled]=\"readonly\">{{display}}</mat-checkbox>\n\n <mat-icon *ngIf=\"infoMessage\" (click)=\"onInfoClick($event)\" matTooltip=\"Info\" matTooltipPosition=\"above\" style=\"color: steelblue;font-size: 15px;margin-left: 5px;margin-top: 8px;\">info</mat-icon>\n\n</div> -->\n\n", styles: [".suffix-icons{display:flex;align-items:center}\n"] }]
15466
+ args: [{ selector: 'spa-check', standalone: false, template: "\n\n<!-- Changed: checkbox + hint are wrapped so the host presents ONE flex item, not two.\n tin-styles.css's FORM-1/FORM-3 rule makes `spa-form spa-check` a flex row to centre it on the field\n control band; with the hint as a second child it became a sibling COLUMN beside the label instead of a\n line beneath it. Wrapping restores the single-item assumption that rule was written against, so the\n band alignment is preserved untouched and the hint stacks where check.component.css already aims it. -->\n<div class=\"check-body\">\n <mat-checkbox color=\"primary\" [(ngModel)]=\"value\" (change)=\"changed()\" (click)=\"clicked()\" [disabled]=\"readonly\">{{display}}</mat-checkbox>\n\n <!-- Added: the hint line. Sits under the label rather than beside it so a full sentence can be read without\n stretching the grid column, and is indented to the label's own left edge, not the box's. -->\n <div class=\"check-hint\" *ngIf=\"hint\">{{hint}}</div>\n</div>\n\n<!-- <div class=\"suffix-icons\">\n\n <mat-checkbox color=\"primary\" [(ngModel)]=\"value\" (change)=\"changed()\" (click)=\"clicked()\" [disabled]=\"readonly\">{{display}}</mat-checkbox>\n\n <mat-icon *ngIf=\"infoMessage\" (click)=\"onInfoClick($event)\" matTooltip=\"Info\" matTooltipPosition=\"above\" style=\"color: steelblue;font-size: 15px;margin-left: 5px;margin-top: 8px;\">info</mat-icon>\n\n</div> -->\n\n", styles: [".suffix-icons{display:flex;align-items:center}.check-body{display:inline-flex;flex-direction:column;align-items:flex-start}.check-hint{font-size:12px;line-height:1.35;color:#0000008c;margin:1px 0 0 calc(var(--mat-checkbox-state-layer-size, var(--mdc-checkbox-state-layer-size, 40px)) + 4px)}\n"] }]
15322
15467
  }], ctorParameters: () => [{ type: MessageService }], propDecorators: { readonly: [{
15323
15468
  type: Input
15324
15469
  }], display: [{
@@ -15337,6 +15482,94 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
15337
15482
  type: Input
15338
15483
  }], infoClick: [{
15339
15484
  type: Output
15485
+ }], hint: [{
15486
+ type: Input
15487
+ }] } });
15488
+
15489
+ // TinCore Constants.SystemUser = "System"; its Users row is FirstName "System" / LastName "User",
15490
+ // so a system-stamped row reaches the client as either of these. Kept tight and evidence-based —
15491
+ // speculative tokens ('job', 'seed') are NOT matched because nothing in this codebase produces them.
15492
+ const SYSTEM_TOKENS = ['system', 'system user'];
15493
+ // Ten hues carry people. Slate is NOT in the rotation — it is reserved for "not a person", so a
15494
+ // neutral circle can never be mistaken for someone whose colour merely happens to be grey.
15495
+ const MONOGRAM_TONES = ['indigo', 'blue', 'cyan', 'teal', 'green', 'moss', 'ochre', 'clay', 'rose', 'plum'];
15496
+ // Initials from a display name. Rules, in order:
15497
+ // - an email uses the local part, split on . _ + - john.smith@x.com -> JS, admin@x.com -> AD
15498
+ // - two or more words take FIRST + LAST, so middle names are ignored Mary Jane Watson -> MW
15499
+ // - one word takes its first two characters admin -> AD, Prince -> PR
15500
+ // - a single character stands alone X -> X
15501
+ function monogramInitials(display) {
15502
+ const s = (display ?? '').replace(/\s+/g, ' ').trim();
15503
+ if (!s)
15504
+ return '';
15505
+ if (s.includes('@')) {
15506
+ const local = s.split('@')[0];
15507
+ const parts = local.split(/[._+-]+/).filter(p => p.length);
15508
+ if (parts.length > 1)
15509
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
15510
+ return local.slice(0, 2).toUpperCase();
15511
+ }
15512
+ const words = s.split(' ').filter(w => w.length);
15513
+ if (words.length > 1)
15514
+ return (words[0][0] + words[words.length - 1][0]).toUpperCase();
15515
+ return words[0].slice(0, 2).toUpperCase();
15516
+ }
15517
+ // Classifies the actor. BaseBasic.CreatedByName is $"{CreatedByUser?.FirstName} {CreatedByUser?.LastName}",
15518
+ // which yields a bare " " when the user is missing — that trims to empty and lands here as 'unknown',
15519
+ // which is exactly why the trim above is load-bearing rather than cosmetic.
15520
+ function monogramKind(display, key) {
15521
+ const d = (display ?? '').replace(/\s+/g, ' ').trim().toLowerCase();
15522
+ const k = (key ?? '').trim().toLowerCase();
15523
+ if (SYSTEM_TOKENS.includes(k) || SYSTEM_TOKENS.includes(d))
15524
+ return 'system';
15525
+ if (!d && !k)
15526
+ return 'unknown';
15527
+ return 'person';
15528
+ }
15529
+ // FNV-1a over the stable key. Deterministic and well spread for short strings; Math.imul keeps the
15530
+ // multiply in 32-bit so the result is identical across engines.
15531
+ function monogramPaletteIndex(key, buckets) {
15532
+ const s = (key ?? '').trim().toLowerCase();
15533
+ let h = 0x811c9dc5;
15534
+ for (let i = 0; i < s.length; i++) {
15535
+ h ^= s.charCodeAt(i);
15536
+ h = Math.imul(h, 0x01000193);
15537
+ }
15538
+ return (h >>> 0) % buckets;
15539
+ }
15540
+ class MonogramComponent {
15541
+ constructor() {
15542
+ this.tapped = new EventEmitter(); // touch path — there is no hover on a phone
15543
+ this.initials = '';
15544
+ this.kind = 'unknown';
15545
+ this.tone = 'slate';
15546
+ this.label = 'Unknown';
15547
+ }
15548
+ ngOnChanges() {
15549
+ const display = (this.value ?? '').replace(/\s+/g, ' ').trim();
15550
+ this.kind = monogramKind(this.value, this.key);
15551
+ // Colour seeds off the stable key when there is one; the normalized display name is the fallback,
15552
+ // which is stable in practice because every audit name is formatted by one base class.
15553
+ const seed = (this.key ?? '').trim() || display;
15554
+ this.initials = monogramInitials(display || this.key);
15555
+ this.tone = this.kind === 'person' ? MONOGRAM_TONES[monogramPaletteIndex(seed, MONOGRAM_TONES.length)] : 'slate';
15556
+ this.label = this.kind === 'system' ? 'System' : (display || 'Unknown');
15557
+ }
15558
+ onTap() {
15559
+ this.tapped.emit(this.label); // tap/Enter surfaces the full name where hover cannot
15560
+ }
15561
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: MonogramComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
15562
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: MonogramComponent, isStandalone: false, selector: "spa-monogram", inputs: { value: "value", key: "key" }, outputs: { tapped: "tapped" }, usesOnChanges: true, ngImport: i0, template: "<span class=\"tin-mono\" [ngClass]=\"'tin-mono--' + tone\" role=\"img\" [attr.aria-label]=\"label\" [matTooltip]=\"label\" matTooltipPosition=\"above\" tabindex=\"0\" (click)=\"onTap()\" (keydown.enter)=\"onTap()\" (keydown.space)=\"onTap()\">\n <mat-icon *ngIf=\"kind === 'system'\" class=\"tin-mono__icon\">settings</mat-icon>\n <ng-container *ngIf=\"kind !== 'system'\">{{ initials || '\u2013' }}</ng-container>\n</span>\n", styles: [".tin-mono{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;font-size:10.5px;font-weight:600;letter-spacing:.3px;line-height:1;font-variant-numeric:tabular-nums;-webkit-user-select:none;user-select:none;cursor:pointer;flex-shrink:0;vertical-align:middle;background:var(--tin-mono-bg);color:var(--tin-mono-fg);box-shadow:inset 0 0 0 1px #0000000b;transition:box-shadow .12s ease,transform .12s ease}.tin-mono:hover{box-shadow:inset 0 0 0 1px #00000017,0 1px 3px #00000024}.tin-mono:active{transform:scale(.94)}.tin-mono:focus-visible{outline:2px solid var(--tin-mono-fg);outline-offset:2px}.tin-mono__icon{font-size:15px;width:15px;height:15px;opacity:.8}.tin-mono--slate{--tin-mono-bg: #E4E7EA;--tin-mono-fg: #3F4B57}.tin-mono--indigo{--tin-mono-bg: #E4E6F7;--tin-mono-fg: #343F94}.tin-mono--blue{--tin-mono-bg: #DBE8F8;--tin-mono-fg: #1B4F8A}.tin-mono--cyan{--tin-mono-bg: #D6EAF0;--tin-mono-fg: #17545F}.tin-mono--teal{--tin-mono-bg: #D5EBE3;--tin-mono-fg: #1A5847}.tin-mono--green{--tin-mono-bg: #DDEBD6;--tin-mono-fg: #33602A}.tin-mono--moss{--tin-mono-bg: #E6E8CF;--tin-mono-fg: #55591B}.tin-mono--ochre{--tin-mono-bg: #F3E6CD;--tin-mono-fg: #6B5115}.tin-mono--clay{--tin-mono-bg: #F6E2D6;--tin-mono-fg: #7A4522}.tin-mono--rose{--tin-mono-bg: #F7E0E4;--tin-mono-fg: #8A3A4C}.tin-mono--plum{--tin-mono-bg: #EEE0F2;--tin-mono-fg: #6B3A82}:host-context([data-theme=\"dark\"]) .tin-mono--slate,:host-context(.tin-dark) .tin-mono--slate{--tin-mono-bg: #333B44;--tin-mono-fg: #C5CDD6}:host-context([data-theme=\"dark\"]) .tin-mono--indigo,:host-context(.tin-dark) .tin-mono--indigo{--tin-mono-bg: #2E3468;--tin-mono-fg: #C5CBF0}:host-context([data-theme=\"dark\"]) .tin-mono--blue,:host-context(.tin-dark) .tin-mono--blue{--tin-mono-bg: #22415F;--tin-mono-fg: #B9D5F2}:host-context([data-theme=\"dark\"]) .tin-mono--cyan,:host-context(.tin-dark) .tin-mono--cyan{--tin-mono-bg: #1C424A;--tin-mono-fg: #AED8E2}:host-context([data-theme=\"dark\"]) .tin-mono--teal,:host-context(.tin-dark) .tin-mono--teal{--tin-mono-bg: #1D453A;--tin-mono-fg: #ABDCC9}:host-context([data-theme=\"dark\"]) .tin-mono--green,:host-context(.tin-dark) .tin-mono--green{--tin-mono-bg: #2A4423;--tin-mono-fg: #C0DCB4}:host-context([data-theme=\"dark\"]) .tin-mono--moss,:host-context(.tin-dark) .tin-mono--moss{--tin-mono-bg: #3F4218;--tin-mono-fg: #D2D6A6}:host-context([data-theme=\"dark\"]) .tin-mono--ochre,:host-context(.tin-dark) .tin-mono--ochre{--tin-mono-bg: #4C3C13;--tin-mono-fg: #E4CD9C}:host-context([data-theme=\"dark\"]) .tin-mono--clay,:host-context(.tin-dark) .tin-mono--clay{--tin-mono-bg: #55351E;--tin-mono-fg: #EEC3A6}:host-context([data-theme=\"dark\"]) .tin-mono--rose,:host-context(.tin-dark) .tin-mono--rose{--tin-mono-bg: #5C2E38;--tin-mono-fg: #F0BFC8}:host-context([data-theme=\"dark\"]) .tin-mono--plum,:host-context(.tin-dark) .tin-mono--plum{--tin-mono-bg: #4A2E58;--tin-mono-fg: #DCBFE8}:host-context([data-theme=\"dark\"]) .tin-mono,:host-context(.tin-dark) .tin-mono{box-shadow:inset 0 0 0 1px #ffffff0f}@media (prefers-reduced-motion: reduce){.tin-mono{transition:none}.tin-mono:active{transform:none}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
15563
+ }
15564
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: MonogramComponent, decorators: [{
15565
+ type: Component,
15566
+ args: [{ selector: 'spa-monogram', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<span class=\"tin-mono\" [ngClass]=\"'tin-mono--' + tone\" role=\"img\" [attr.aria-label]=\"label\" [matTooltip]=\"label\" matTooltipPosition=\"above\" tabindex=\"0\" (click)=\"onTap()\" (keydown.enter)=\"onTap()\" (keydown.space)=\"onTap()\">\n <mat-icon *ngIf=\"kind === 'system'\" class=\"tin-mono__icon\">settings</mat-icon>\n <ng-container *ngIf=\"kind !== 'system'\">{{ initials || '\u2013' }}</ng-container>\n</span>\n", styles: [".tin-mono{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;font-size:10.5px;font-weight:600;letter-spacing:.3px;line-height:1;font-variant-numeric:tabular-nums;-webkit-user-select:none;user-select:none;cursor:pointer;flex-shrink:0;vertical-align:middle;background:var(--tin-mono-bg);color:var(--tin-mono-fg);box-shadow:inset 0 0 0 1px #0000000b;transition:box-shadow .12s ease,transform .12s ease}.tin-mono:hover{box-shadow:inset 0 0 0 1px #00000017,0 1px 3px #00000024}.tin-mono:active{transform:scale(.94)}.tin-mono:focus-visible{outline:2px solid var(--tin-mono-fg);outline-offset:2px}.tin-mono__icon{font-size:15px;width:15px;height:15px;opacity:.8}.tin-mono--slate{--tin-mono-bg: #E4E7EA;--tin-mono-fg: #3F4B57}.tin-mono--indigo{--tin-mono-bg: #E4E6F7;--tin-mono-fg: #343F94}.tin-mono--blue{--tin-mono-bg: #DBE8F8;--tin-mono-fg: #1B4F8A}.tin-mono--cyan{--tin-mono-bg: #D6EAF0;--tin-mono-fg: #17545F}.tin-mono--teal{--tin-mono-bg: #D5EBE3;--tin-mono-fg: #1A5847}.tin-mono--green{--tin-mono-bg: #DDEBD6;--tin-mono-fg: #33602A}.tin-mono--moss{--tin-mono-bg: #E6E8CF;--tin-mono-fg: #55591B}.tin-mono--ochre{--tin-mono-bg: #F3E6CD;--tin-mono-fg: #6B5115}.tin-mono--clay{--tin-mono-bg: #F6E2D6;--tin-mono-fg: #7A4522}.tin-mono--rose{--tin-mono-bg: #F7E0E4;--tin-mono-fg: #8A3A4C}.tin-mono--plum{--tin-mono-bg: #EEE0F2;--tin-mono-fg: #6B3A82}:host-context([data-theme=\"dark\"]) .tin-mono--slate,:host-context(.tin-dark) .tin-mono--slate{--tin-mono-bg: #333B44;--tin-mono-fg: #C5CDD6}:host-context([data-theme=\"dark\"]) .tin-mono--indigo,:host-context(.tin-dark) .tin-mono--indigo{--tin-mono-bg: #2E3468;--tin-mono-fg: #C5CBF0}:host-context([data-theme=\"dark\"]) .tin-mono--blue,:host-context(.tin-dark) .tin-mono--blue{--tin-mono-bg: #22415F;--tin-mono-fg: #B9D5F2}:host-context([data-theme=\"dark\"]) .tin-mono--cyan,:host-context(.tin-dark) .tin-mono--cyan{--tin-mono-bg: #1C424A;--tin-mono-fg: #AED8E2}:host-context([data-theme=\"dark\"]) .tin-mono--teal,:host-context(.tin-dark) .tin-mono--teal{--tin-mono-bg: #1D453A;--tin-mono-fg: #ABDCC9}:host-context([data-theme=\"dark\"]) .tin-mono--green,:host-context(.tin-dark) .tin-mono--green{--tin-mono-bg: #2A4423;--tin-mono-fg: #C0DCB4}:host-context([data-theme=\"dark\"]) .tin-mono--moss,:host-context(.tin-dark) .tin-mono--moss{--tin-mono-bg: #3F4218;--tin-mono-fg: #D2D6A6}:host-context([data-theme=\"dark\"]) .tin-mono--ochre,:host-context(.tin-dark) .tin-mono--ochre{--tin-mono-bg: #4C3C13;--tin-mono-fg: #E4CD9C}:host-context([data-theme=\"dark\"]) .tin-mono--clay,:host-context(.tin-dark) .tin-mono--clay{--tin-mono-bg: #55351E;--tin-mono-fg: #EEC3A6}:host-context([data-theme=\"dark\"]) .tin-mono--rose,:host-context(.tin-dark) .tin-mono--rose{--tin-mono-bg: #5C2E38;--tin-mono-fg: #F0BFC8}:host-context([data-theme=\"dark\"]) .tin-mono--plum,:host-context(.tin-dark) .tin-mono--plum{--tin-mono-bg: #4A2E58;--tin-mono-fg: #DCBFE8}:host-context([data-theme=\"dark\"]) .tin-mono,:host-context(.tin-dark) .tin-mono{box-shadow:inset 0 0 0 1px #ffffff0f}@media (prefers-reduced-motion: reduce){.tin-mono{transition:none}.tin-mono:active{transform:none}}\n"] }]
15567
+ }], propDecorators: { value: [{
15568
+ type: Input
15569
+ }], key: [{
15570
+ type: Input
15571
+ }], tapped: [{
15572
+ type: Output
15340
15573
  }] } });
15341
15574
 
15342
15575
  class TableRowComponent {
@@ -15427,11 +15660,11 @@ class TableRowComponent {
15427
15660
  return false;
15428
15661
  }
15429
15662
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableRowComponent, deps: [{ token: ButtonService }], target: i0.ɵɵFactoryTarget.Component }); }
15430
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TableRowComponent, isStandalone: false, selector: "app-table-row", inputs: { column: "column", row: "row", config: "config", smallScreen: "smallScreen" }, outputs: { actionClick: "actionClick", columnClick: "columnClick", showBannerEvent: "showBannerEvent" }, usesOnChanges: true, ngImport: i0, template: "<ng-container [ngSwitch]=\"column.type\">\n <ng-container *ngSwitchCase=\"'checkbox'\">\n <spa-check [value]=\"row[column.name]\" [readonly]=\"true\"></spa-check>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'select'\">\n <spa-select-lite [options]=\"column.options\" [optionDisplay]=\"column.optionDisplay\" [optionValue]=\"column.optionValue\" [(value)]=\"row[column.name]\" width=\"90%\"></spa-select-lite>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'chip'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'background-color': vm.color || '#eceff1', 'color': 'rgba(0, 0, 0, 0.87)', 'border': 'none'}\">{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'icon'\">\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date'\">\n {{row[column.name] | date : 'dd/MM/yyyy'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date-short'\">\n {{row[column.name] | date : 'd MMM'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetime'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetimesec'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm:ss'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'money'\">\n <label [ngStyle]=\"{'color': vm.color }\">{{row[column.name] | currency:'':''}}</label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'button'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'color': vm.color}\" >{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchDefault>\n <label [ngStyle]=\"{'color': vm.color }\">\n <ng-container *ngIf=\"column.type === 'number'\">\n {{row[column.name] | number:'1.0-2'}}\n </ng-container>\n <ng-container *ngIf=\"column.type !== 'number'\">\n {{vm.text}}\n </ng-container>\n </label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.truncated\" matTooltip='Show more' matTooltipPosition=\"above\" (click)=\"showBanner(row[column.name])\">more_horiz</mat-icon>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n</ng-container>\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;vertical-align:middle}.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}.mat-mdc-cell .mat-mdc-outlined-button,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button){height:auto!important;min-height:36px!important;white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important;line-height:1.4!important;text-align:left!important;padding:6px 16px!important}.mat-mdc-cell .mat-mdc-outlined-button .mdc-button__label,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button) .mdc-button__label{white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!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: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i1$2.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1$2.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1$2.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { 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: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: SelectLiteComponent, selector: "spa-select-lite" }, { kind: "pipe", type: i1$2.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$2.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
15663
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TableRowComponent, isStandalone: false, selector: "app-table-row", inputs: { column: "column", row: "row", config: "config", smallScreen: "smallScreen" }, outputs: { actionClick: "actionClick", columnClick: "columnClick", showBannerEvent: "showBannerEvent" }, usesOnChanges: true, ngImport: i0, template: "<ng-container [ngSwitch]=\"column.type\">\n <ng-container *ngSwitchCase=\"'checkbox'\">\n <spa-check [value]=\"row[column.name]\" [readonly]=\"true\"></spa-check>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'select'\">\n <spa-select-lite [options]=\"column.options\" [optionDisplay]=\"column.optionDisplay\" [optionValue]=\"column.optionValue\" [(value)]=\"row[column.name]\" width=\"90%\"></spa-select-lite>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'chip'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'background-color': vm.color || '#eceff1', 'color': 'rgba(0, 0, 0, 0.87)', 'border': 'none'}\">{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'icon'\">\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date'\">\n {{row[column.name] | date : 'dd/MM/yyyy'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date-short'\">\n {{row[column.name] | date : 'd MMM'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetime'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetimesec'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm:ss'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'money'\">\n <label [ngStyle]=\"{'color': vm.color }\">{{row[column.name] | currency:'':''}}</label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <!-- Added: monogram \u2014 the row still holds the full name, so filter/sort keep working on the underlying value -->\n <ng-container *ngSwitchCase=\"'monogram'\">\n <spa-monogram [value]=\"row[column.name]\" [key]=\"column.keyField ? row[column.keyField] : null\" (tapped)=\"showBanner($event)\"></spa-monogram>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'button'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'color': vm.color}\" >{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchDefault>\n <label [ngStyle]=\"{'color': vm.color }\">\n <ng-container *ngIf=\"column.type === 'number'\">\n {{row[column.name] | number:'1.0-2'}}\n </ng-container>\n <ng-container *ngIf=\"column.type !== 'number'\">\n {{vm.text}}\n </ng-container>\n </label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.truncated\" matTooltip='Show more' matTooltipPosition=\"above\" (click)=\"showBanner(row[column.name])\">more_horiz</mat-icon>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n</ng-container>\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;vertical-align:middle}.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}.mat-mdc-cell .mat-mdc-outlined-button,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button){height:auto!important;min-height:36px!important;white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important;line-height:1.4!important;text-align:left!important;padding:6px 16px!important}.mat-mdc-cell .mat-mdc-outlined-button .mdc-button__label,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button) .mdc-button__label{white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!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: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i1$2.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1$2.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1$2.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { 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: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage", "hint"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: MonogramComponent, selector: "spa-monogram", inputs: ["value", "key"], outputs: ["tapped"] }, { kind: "component", type: SelectLiteComponent, selector: "spa-select-lite" }, { kind: "pipe", type: i1$2.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$2.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
15431
15664
  }
15432
15665
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableRowComponent, decorators: [{
15433
15666
  type: Component,
15434
- args: [{ selector: 'app-table-row', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<ng-container [ngSwitch]=\"column.type\">\n <ng-container *ngSwitchCase=\"'checkbox'\">\n <spa-check [value]=\"row[column.name]\" [readonly]=\"true\"></spa-check>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'select'\">\n <spa-select-lite [options]=\"column.options\" [optionDisplay]=\"column.optionDisplay\" [optionValue]=\"column.optionValue\" [(value)]=\"row[column.name]\" width=\"90%\"></spa-select-lite>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'chip'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'background-color': vm.color || '#eceff1', 'color': 'rgba(0, 0, 0, 0.87)', 'border': 'none'}\">{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'icon'\">\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date'\">\n {{row[column.name] | date : 'dd/MM/yyyy'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date-short'\">\n {{row[column.name] | date : 'd MMM'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetime'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetimesec'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm:ss'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'money'\">\n <label [ngStyle]=\"{'color': vm.color }\">{{row[column.name] | currency:'':''}}</label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'button'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'color': vm.color}\" >{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchDefault>\n <label [ngStyle]=\"{'color': vm.color }\">\n <ng-container *ngIf=\"column.type === 'number'\">\n {{row[column.name] | number:'1.0-2'}}\n </ng-container>\n <ng-container *ngIf=\"column.type !== 'number'\">\n {{vm.text}}\n </ng-container>\n </label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.truncated\" matTooltip='Show more' matTooltipPosition=\"above\" (click)=\"showBanner(row[column.name])\">more_horiz</mat-icon>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n</ng-container>\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;vertical-align:middle}.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}.mat-mdc-cell .mat-mdc-outlined-button,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button){height:auto!important;min-height:36px!important;white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important;line-height:1.4!important;text-align:left!important;padding:6px 16px!important}.mat-mdc-cell .mat-mdc-outlined-button .mdc-button__label,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button) .mdc-button__label{white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important}\n"] }]
15667
+ args: [{ selector: 'app-table-row', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<ng-container [ngSwitch]=\"column.type\">\n <ng-container *ngSwitchCase=\"'checkbox'\">\n <spa-check [value]=\"row[column.name]\" [readonly]=\"true\"></spa-check>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'select'\">\n <spa-select-lite [options]=\"column.options\" [optionDisplay]=\"column.optionDisplay\" [optionValue]=\"column.optionValue\" [(value)]=\"row[column.name]\" width=\"90%\"></spa-select-lite>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'chip'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'background-color': vm.color || '#eceff1', 'color': 'rgba(0, 0, 0, 0.87)', 'border': 'none'}\">{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'icon'\">\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date'\">\n {{row[column.name] | date : 'dd/MM/yyyy'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date-short'\">\n {{row[column.name] | date : 'd MMM'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetime'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetimesec'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm:ss'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'money'\">\n <label [ngStyle]=\"{'color': vm.color }\">{{row[column.name] | currency:'':''}}</label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <!-- Added: monogram \u2014 the row still holds the full name, so filter/sort keep working on the underlying value -->\n <ng-container *ngSwitchCase=\"'monogram'\">\n <spa-monogram [value]=\"row[column.name]\" [key]=\"column.keyField ? row[column.keyField] : null\" (tapped)=\"showBanner($event)\"></spa-monogram>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'button'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'color': vm.color}\" >{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchDefault>\n <label [ngStyle]=\"{'color': vm.color }\">\n <ng-container *ngIf=\"column.type === 'number'\">\n {{row[column.name] | number:'1.0-2'}}\n </ng-container>\n <ng-container *ngIf=\"column.type !== 'number'\">\n {{vm.text}}\n </ng-container>\n </label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.truncated\" matTooltip='Show more' matTooltipPosition=\"above\" (click)=\"showBanner(row[column.name])\">more_horiz</mat-icon>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n</ng-container>\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;vertical-align:middle}.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}.mat-mdc-cell .mat-mdc-outlined-button,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button){height:auto!important;min-height:36px!important;white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important;line-height:1.4!important;text-align:left!important;padding:6px 16px!important}.mat-mdc-cell .mat-mdc-outlined-button .mdc-button__label,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button) .mdc-button__label{white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important}\n"] }]
15435
15668
  }], ctorParameters: () => [{ type: ButtonService }], propDecorators: { column: [{
15436
15669
  type: Input
15437
15670
  }], row: [{
@@ -15866,7 +16099,7 @@ class SelectComponent extends SelectCommonComponent {
15866
16099
  let button = this.detailsConfig.buttons.find(b => b.name === mode);
15867
16100
  button.detailsConfig = this.detailsConfig;
15868
16101
  this.dialogService.openDefaultDetailsDialog(button, dynamicData).subscribe(result => {
15869
- if (result.message === 'success') {
16102
+ if (result?.message === 'success') { // Changed: a dismissed quick-add dialog closes with undefined — read through it rather than throwing
15870
16103
  // Added (dropdown quick-add): auto-select the record the user just created — the point of adding
15871
16104
  // from a dropdown is to use the new record, so don't make them find it in the list afterwards
15872
16105
  if (mode === 'create')
@@ -16594,7 +16827,7 @@ class InlineCellComponent {
16594
16827
  <spa-text *ngSwitchDefault [display]="display" [(value)]="data[field.name]" (valueChange)="cellChanged()" [required]="field.required" [min]="field.min" [max]="field.max" [suffix]="field.suffix" [regex]="field.regex"></spa-text>
16595
16828
 
16596
16829
  </ng-container>
16597
- `, isInline: true, styles: [":host{display:block;min-width:90px;padding-top:6px}\n"], dependencies: [{ kind: "directive", type: i1$2.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1$2.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1$2.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "component", type: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextAreaComponent, selector: "spa-text-area", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "rows", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: DateComponent, selector: "spa-date", inputs: ["required", "min", "max", "readonly", "hint", "value", "display", "placeholder", "width", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: DatetimeComponent, selector: "spa-datetime", inputs: ["display", "value", "readonly", "width", "min", "max", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: MoneyComponent, selector: "spa-money", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "currency", "required", "min", "max", "infoMessage", "copyContent", "clearContent", "suffix"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: NumberComponent, selector: "spa-number", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "step", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: EmailComponent, selector: "spa-email", inputs: ["display", "value", "readonly", "required", "hint", "suffix", "infoMessage", "copyContent", "clearContent", "options", "optionValue"], outputs: ["valueChange"] }] }); }
16830
+ `, isInline: true, styles: [":host{display:block;min-width:90px;padding-top:6px}\n"], dependencies: [{ kind: "directive", type: i1$2.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1$2.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1$2.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "component", type: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextAreaComponent, selector: "spa-text-area", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "rows", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage", "hint"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: DateComponent, selector: "spa-date", inputs: ["required", "min", "max", "readonly", "hint", "value", "display", "placeholder", "width", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: DatetimeComponent, selector: "spa-datetime", inputs: ["display", "value", "readonly", "width", "min", "max", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: MoneyComponent, selector: "spa-money", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "currency", "required", "min", "max", "infoMessage", "copyContent", "clearContent", "suffix"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: NumberComponent, selector: "spa-number", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "step", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: EmailComponent, selector: "spa-email", inputs: ["display", "value", "readonly", "required", "hint", "suffix", "infoMessage", "copyContent", "clearContent", "options", "optionValue"], outputs: ["valueChange"] }] }); }
16598
16831
  }
16599
16832
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: InlineCellComponent, decorators: [{
16600
16833
  type: Component,
@@ -17055,7 +17288,7 @@ class GroupsComponent {
17055
17288
  getVisibleButtons(item) {
17056
17289
  let buttons = this.buttonCache.get(item);
17057
17290
  if (!buttons) {
17058
- buttons = this.displayedButtons ? this.displayedButtons.filter(button => Core.isItemVisible(button, item)) : []; // Changed: unified visible/hidden (boolean | condition)
17291
+ buttons = this.displayedButtons ? this.displayedButtons.filter(button => Core.isItemVisible(button, item) && this.buttonService.hasCapability(button)) : []; // Changed: unified visible/hidden (boolean | condition) + the opt-in capability, which this path bypassed by not going through testVisible
17059
17292
  this.buttonCache.set(item, buttons);
17060
17293
  }
17061
17294
  return buttons;
@@ -17201,6 +17434,15 @@ class TableComponent {
17201
17434
  this.filterActive = false;
17202
17435
  this.filterText = ''; // Added: current paged-mode filter text
17203
17436
  this.groupFilterText = ''; // Added: relayed to spa-groups so the standard header filter drives the grouped view
17437
+ // Added: the headline for the "filter matched nothing" empty state. Built ONCE per filter change rather than
17438
+ // by a template getter — a getter that concatenates a string allocates on every change-detection pass.
17439
+ this.filterEmptyTitle = 'No matches';
17440
+ this.filterEmptyTitlePartial = 'No matches in the rows loaded so far'; // Added: the partial-window variant of the headline above (see filterScopePartial)
17441
+ // Added: a load that FAILED is not a load that returned nothing, and until now the grid could not tell them
17442
+ // apart. The failure paths never assign dataSource, so it stayed undefined and the template's
17443
+ // `!dataSource && !loadingStage` branch rendered "Loading..." — permanently, under a dismissed error dialog.
17444
+ // Set by every failure handler, cleared whenever a read starts or succeeds.
17445
+ this.loadFailed = false;
17204
17446
  this.fetchSize = 20;
17205
17447
  this.pagedUrlBase = '';
17206
17448
  this.pageFetchInFlight = false;
@@ -17253,16 +17495,29 @@ class TableComponent {
17253
17495
  get sectionHidden() {
17254
17496
  return !!(this.config?.sectionConfig?.hideWhenEmpty && this.dataSource?.length === 0);
17255
17497
  }
17498
+ // Changed: the expand half is delegated to expandSection so the two entry points cannot drift apart
17256
17499
  toggleSection() {
17257
17500
  if (this.config?.sectionConfig?.collapsible === false)
17258
17501
  return;
17259
- this.sectionCollapsed = !this.sectionCollapsed;
17502
+ if (this.sectionCollapsed) {
17503
+ this.expandSection();
17504
+ return;
17505
+ }
17506
+ this.sectionCollapsed = true;
17507
+ }
17508
+ // Added: the expand-ONLY entry point, for a caller that wants this section open rather than flipped — a Day
17509
+ // Book chip or stage tile means "show me this list", and routing that through toggleSection() closes the very
17510
+ // thing the user asked to see whenever the section happened to be open already. Deliberately NOT gated on
17511
+ // `collapsible === false`: a section the operator may not collapse by hand is one that should be open anyway.
17512
+ expandSection() {
17260
17513
  if (!this.sectionCollapsed)
17261
- setTimeout(() => this.setPaginator()); // re-attach — the paginator ViewChild did not exist while collapsed
17514
+ return;
17515
+ this.sectionCollapsed = false;
17516
+ setTimeout(() => this.setPaginator()); // re-attach — the paginator ViewChild did not exist while collapsed
17262
17517
  }
17263
17518
  buildSectionHeader() {
17264
17519
  this.sectionChipList = (this.config?.sectionConfig?.chips || []).filter(c => Core.isItemVisible(c, this.dataSourceValue)); // predicates receive the rows
17265
- this.sectionButtonList = (this.config?.sectionConfig?.buttons || []).filter(b => Core.isItemVisible(b, this.dataSourceValue));
17520
+ this.sectionButtonList = (this.config?.sectionConfig?.buttons || []).filter(b => Core.isItemVisible(b, this.dataSourceValue) && this.buttonService.hasCapability(b)); // Changed: section-header buttons bypass testVisible, so the opt-in capability is applied here too. No-op for a button that names none
17266
17521
  }
17267
17522
  sectionChips() { return this.sectionChipList; } // Changed: returns the materialized field
17268
17523
  sectionButtons() { return this.sectionButtonList; } // Changed: returns the materialized field
@@ -17344,6 +17599,45 @@ class TableComponent {
17344
17599
  return 35;
17345
17600
  return [72, 58, 86, 64, 78, 52][index % 6];
17346
17601
  }
17602
+ // Added: "nothing here yet" and "nothing matches what you typed" are two different states and they get two
17603
+ // different sentences. The "No Data" line beside this one tests dataSource — the LOADED WINDOW — which a
17604
+ // filter never shrinks: filtering happens downstream of it (MatTableDataSource.filteredData when unpaged,
17605
+ // filteredRows when paged). So a filter matching nothing left dataSource full, the line suppressed, and the
17606
+ // user staring at a header, a "0 of 0" paginator and no explanation. This getter is the missing case only —
17607
+ // it is FALSE whenever dataSource is empty, so the genuinely-empty state keeps the line and the words it
17608
+ // always had. Reads lengths and flags only; it allocates nothing, so it is safe to bind in the template.
17609
+ get filteredToNothing() {
17610
+ if (this.loadingStage === 'initial')
17611
+ return false; // the loading stage owns the space until it completes
17612
+ if (!(this.dataSource?.length > 0))
17613
+ return false; // genuinely empty — "No Data" answers for that state
17614
+ // Only the table view filters what it renders. Cards and capsules are bound to dataSource directly and show
17615
+ // every row regardless of the filter, and the grouped view filters on its own predicate inside spa-groups —
17616
+ // claiming "no matches" over a screen still full of cards would be a worse lie than saying nothing.
17617
+ if (this.config?.viewType && this.config.viewType !== 'table')
17618
+ return false;
17619
+ if (this.pagedMode)
17620
+ return this.filterActive && this.filteredRows?.length === 0;
17621
+ return !!this.tableDataSource?.filter && this.tableDataSource.filteredData?.length === 0;
17622
+ }
17623
+ // Added: THE predicate for "the filter did not see everything". In paged mode the client filter runs over
17624
+ // loadedRows — the accumulated window — and filterChanged issues no fetch, so with a partial window a filter
17625
+ // searched the loaded rows ONLY. This getter is deliberately the single definition of that fact: the hint
17626
+ // banner above the grid and the empty-state message below it both bind to it, so the two physically cannot
17627
+ // disagree about how much was searched. They previously stated it independently and did — the banner said
17628
+ // "only the 20 loaded rows of 240" while the message underneath offered "all 240 rows", implying the filter
17629
+ // had covered the lot. Reads lengths and flags only; allocates nothing, so it is safe to bind in a template.
17630
+ get filterScopePartial() {
17631
+ return this.pagedMode && this.filterActive && this.loadedRows.length < this.serverTotal;
17632
+ }
17633
+ // Added: what the user gets back by clearing the filter. Paged mode's dataSource is only the loaded window, so
17634
+ // the server total (the same figure its own paginator shows) is the honest number there. Only ever rendered in
17635
+ // the COMPLETE-scope state — where the window is partial the count would misdescribe what was searched, which
17636
+ // is exactly why that state gets its own sentence rather than this one.
17637
+ get filteredTotal() {
17638
+ return this.pagedMode ? this.serverTotal + this.overlayDelta : (this.dataSource?.length ?? 0);
17639
+ }
17640
+ get filteredRowNoun() { return this.filteredTotal === 1 ? 'row' : 'rows'; } // Added: keeps "all 1 rows" out of the message
17347
17641
  get stageEntityName() {
17348
17642
  // An author-supplied entityName is their wording and is used verbatim.
17349
17643
  if (this.config?.entityName)
@@ -17474,6 +17768,25 @@ class TableComponent {
17474
17768
  return null;
17475
17769
  return field;
17476
17770
  }
17771
+ // Added: every field the CONFIG declares inline-editable, whether or not it has a cell on screen
17772
+ configuredInlineFields() {
17773
+ return (this.config?.columns ?? []).map(column => this.getInlineField(column)).filter(field => !!field);
17774
+ }
17775
+ // Added: the inline-editable fields that are ACTUALLY RENDERED. <app-inline-cell> lives inside a per-column
17776
+ // matColumnDef (table.component.html:101), so a column missing from displayedColumns has no editor at all.
17777
+ // displayedColumns is not config.columns: setColumns REPLACES it with the minColumns subset below 600px
17778
+ // (table-config.service.ts:26-27) and also drops columns pruned by a visible/hidden predicate.
17779
+ renderedInlineFields() {
17780
+ return (this.config?.columns ?? []).filter(column => this.displayedColumns?.includes(column.name)).map(column => this.getInlineField(column)).filter(field => !!field);
17781
+ }
17782
+ // Added: inline edit is only honest when EVERY editable field has a cell the operator can see and change.
17783
+ // Previously seedInlineDefaults and submitInlineEdit both walked config.columns, so a field cut by minColumns
17784
+ // was still seeded with its defaultValue, still passed the required check, and was still posted — the row
17785
+ // submitted values nobody ever saw (piglet weaned litters at an optimistic count on a phone, 2026-08-10).
17786
+ canEditInline() {
17787
+ const configured = this.configuredInlineFields();
17788
+ return configured.length > 0 && this.renderedInlineFields().length === configured.length;
17789
+ }
17477
17790
  startInlineEdit(row) {
17478
17791
  this.editingRow = row;
17479
17792
  this.editingModel = { ...row }; // shallow copy — scalars only change, cancel restores by discarding
@@ -17487,9 +17800,8 @@ class TableComponent {
17487
17800
  seedInlineDefaults(row) {
17488
17801
  if (!this.config.inlineEdit || !this.config.columns)
17489
17802
  return;
17490
- for (const column of this.config.columns) {
17491
- const field = this.getInlineField(column);
17492
- if (!field || field.defaultValue === undefined || field.defaultValue === null)
17803
+ for (const field of this.renderedInlineFields()) { // Changed: was config.columns — an unrendered field must never be silently seeded, because nobody can see or correct what it was seeded with
17804
+ if (field.defaultValue === undefined || field.defaultValue === null)
17493
17805
  continue;
17494
17806
  const current = this.editingModel[field.name];
17495
17807
  if (current !== undefined && current !== null && current !== '')
@@ -17505,7 +17817,7 @@ class TableComponent {
17505
17817
  if (!this.editingRow)
17506
17818
  return;
17507
17819
  // Validate only the fields that are editable inline, with the same rules the dialog form applies
17508
- const editableFields = this.config.columns.map(c => this.getInlineField(c)).filter(f => !!f);
17820
+ const editableFields = this.renderedInlineFields(); // Changed: was config.columns validating a field with no cell on screen let a required check pass against a value the operator never supplied
17509
17821
  const validationResult = Core.validateObject(editableFields, this.editingModel);
17510
17822
  if (validationResult !== '') {
17511
17823
  this.messageService.toast(validationResult);
@@ -17552,6 +17864,13 @@ class TableComponent {
17552
17864
  this.displayedColumns = this.tableConfigService.setColumns(this.config, this.smallScreen);
17553
17865
  this.displayedButtons = this.tableConfigService.getDisplayedButtons(this.config?.buttons, this.smallScreen, this.config);
17554
17866
  this.actionsWidth = this.tableConfigService.getActionsWidth(this.displayedButtons, this.smallScreen, this.config);
17867
+ // Added: the same defect through the resize door. A row opened for edit on a wide screen keeps its seeded
17868
+ // editingModel when the window narrows past 600px, but its cells vanish with the columns — so the tick would
17869
+ // post values the operator can no longer see. Discard the edit instead; cancel has always been non-destructive.
17870
+ if (this.editingRow && !this.canEditInline()) {
17871
+ this.cancelInlineEdit();
17872
+ this.messageService.toast('Editing was closed — this screen is too narrow to show every editable field');
17873
+ }
17555
17874
  if (this.config?.searchConfig) {
17556
17875
  this.showFilterButton = false;
17557
17876
  }
@@ -17574,6 +17893,7 @@ class TableComponent {
17574
17893
  }
17575
17894
  }
17576
17895
  loadData(action, data) {
17896
+ this.loadFailed = false; // Added: a retry must clear the previous failure before it can be re-decided — set here, the ONE entry point both modes pass through
17577
17897
  if (this.pagedMode) {
17578
17898
  this.loadDataPaged(action, data);
17579
17899
  return;
@@ -17611,10 +17931,36 @@ class TableComponent {
17611
17931
  }
17612
17932
  }
17613
17933
  else {
17934
+ this.loadFailed = true; // Added: the dialog is dismissable and dedupes across tables — without this the grid is left saying "Loading..." with nothing still loading
17614
17935
  this.apiErrorService.presentAppFailure(apiResponse, 'load', action.url); // Changed: a failed row load — dedupes with other loads so a broken backend yields one dialog, not one per table
17615
17936
  }
17616
- });
17617
- }
17937
+ },
17938
+ // Added: the TRANSPORT failure — a dead API, a dropped connection, a CORS refusal. It never reaches the
17939
+ // branch above because there is no ApiResponse at all: LoaderInterceptor shows the dialog and rethrows,
17940
+ // and this subscribe had no error callback, so the grid was left rendering "No Data" — the one thing
17941
+ // that was definitely NOT true. Rethrown so the error still reaches Angular's global handler exactly as
17942
+ // it did before; this callback only records WHY the grid is empty.
17943
+ (err) => { this.loadFailed = true; throw err; });
17944
+ }
17945
+ }
17946
+ // Added (phone density, opt-in via config.hideSinglePagePaginator): true when the pager is navigating a
17947
+ // single page on a phone, and is therefore 56px of chrome that can reach nowhere it is not already. The
17948
+ // paginator is still RENDERED and only hidden in CSS — the same reason the empty-data case is a class and
17949
+ // not an *ngIf: #tablePaginator is a ViewChild that setPaginator() attaches to the MatTableDataSource, and
17950
+ // removing it from the DOM would break paging for every table that does have a second page.
17951
+ // Desktop returns false on the smallScreen guard alone, so nothing above 600px can be affected.
17952
+ get singlePagePaginatorHidden() {
17953
+ if (!this.config?.hideSinglePagePaginator || !this.smallScreen)
17954
+ return false;
17955
+ // On a phone hidePageSize is on, so the size cannot be changed from the UI and the paginator's own value
17956
+ // and pageSizes[0] agree; reading the live one first keeps this honest if it was changed before a resize.
17957
+ const size = this.pagedMode ? this.pageSize : (this.tablePaginator?.pageSize || this.config?.pageSizes?.[0] || 10);
17958
+ if (!size)
17959
+ return false;
17960
+ const total = this.pagedMode
17961
+ ? (this.filterActive ? this.filteredRows.length : this.serverTotal + this.overlayDelta)
17962
+ : (this.dataSource?.length || 0);
17963
+ return total > 0 && total <= size; // zero rows is already handled by the existing empty-data rule
17618
17964
  }
17619
17965
  setPaginator() {
17620
17966
  if (this.pagedMode)
@@ -17674,9 +18020,11 @@ class TableComponent {
17674
18020
  }
17675
18021
  }
17676
18022
  else {
18023
+ this.loadFailed = true; // Added: same treatment as the unpaged path — a failed search load must not read as a load still running
17677
18024
  this.apiErrorService.presentAppFailure(apiResponse, 'load', action.url); // Changed: paged-mode search load — same classify + dedupe treatment as the unpaged path above
17678
18025
  }
17679
- });
18026
+ }, (err) => { this.loadFailed = true; throw err; } // Added: transport failure — same reasoning as the unpaged path above
18027
+ );
17680
18028
  return;
17681
18029
  }
17682
18030
  if (action.url !== this.pagedUrlBase) {
@@ -17708,6 +18056,7 @@ class TableComponent {
17708
18056
  console.log(apiResponse);
17709
18057
  }
17710
18058
  if (!apiResponse.success) {
18059
+ this.loadFailed = true; // Added: only ever SEEN on a first chunk (the message is gated on !dataSource) — a failed page-forward leaves the rows already on screen alone
17711
18060
  this.apiErrorService.presentAppFailure(apiResponse, 'load', pagedAction.url); // Changed: chunk fetch — scrolling a broken table fires these repeatedly, so deduping matters most here
17712
18061
  return;
17713
18062
  }
@@ -17735,7 +18084,7 @@ class TableComponent {
17735
18084
  if (apiResponse.message != "success" && apiResponse.message != "") {
17736
18085
  this.messageService.toast(apiResponse.message);
17737
18086
  }
17738
- }, () => { this.pageFetchInFlight = false; } // never leave the single-flight guard stuck on a failed request
18087
+ }, () => { this.pageFetchInFlight = false; this.loadFailed = true; } // never leave the single-flight guard stuck on a failed request; Added: a transport-level failure (no ApiResponse at all) is still a failed load
17739
18088
  );
17740
18089
  }
17741
18090
  // Appends skip/take to a URL copy at fetch time
@@ -17815,6 +18164,17 @@ class TableComponent {
17815
18164
  // so its text has to be relayed regardless of paged mode. Set before the paged-mode guard below — grouped
17816
18165
  // views are not paged, and returning early here is what would leave the header filter doing nothing.
17817
18166
  this.groupFilterText = (text ?? '').trim();
18167
+ // Added: build the filtered-empty headline here, where the text arrives for EVERY mode. spa-filter is the
18168
+ // only writer of MatTableDataSource.filter in the library and it emits on keyUp, clear and ngOnChanges
18169
+ // alike, so this field cannot drift from the filter the grid is actually applying. Long pastes are capped
18170
+ // so one field cannot push the message off a phone screen.
18171
+ const term = this.groupFilterText;
18172
+ const shown = term.length > 40 ? term.slice(0, 40) + '…' : term;
18173
+ this.filterEmptyTitle = term ? `No matches for “${shown}”` : 'No matches';
18174
+ // Added: the partial-window headline. Built here beside its sibling so both are computed once per filter
18175
+ // change rather than per change-detection pass. Neither string embeds a COUNT — counts are the banner's and
18176
+ // the paginator's to state — so neither can go stale if the loaded window grows while a filter is active.
18177
+ this.filterEmptyTitlePartial = term ? `No matches for “${shown}” in the rows loaded so far` : 'No matches in the rows loaded so far';
17818
18178
  if (!this.pagedMode)
17819
18179
  return;
17820
18180
  this.filterText = (text ?? '').trim();
@@ -17830,7 +18190,9 @@ class TableComponent {
17830
18190
  let config = new FormConfig;
17831
18191
  config.fields = [];
17832
18192
  this.config.columns.forEach(column => {
17833
- let field = { name: column.name, type: column.type };
18193
+ // Changed: monogram is a DISPLAY-only column type with no form editor — the value behind it is a plain
18194
+ // name string, so an auto-derived field falls back to text rather than leaking an unknown Field type.
18195
+ let field = { name: column.name, type: column.type === 'monogram' ? 'text' : column.type };
17834
18196
  config.fields.push(field);
17835
18197
  });
17836
18198
  this.config.formConfig = config;
@@ -17978,8 +18340,11 @@ class TableComponent {
17978
18340
  this.viewModel(actionData, actionButton);
17979
18341
  }
17980
18342
  else if (name === 'edit') {
17981
- // Changed: inlineEdit tables open the row cells in place instead of the dialog
17982
- if (this.config.inlineEdit) {
18343
+ // Changed: inlineEdit tables open the row cells in place instead of the dialog — but ONLY when every
18344
+ // editable field actually has a cell on screen. Inline edit is an optimisation of the dialog, so when it
18345
+ // cannot show the whole editor it falls back to the dialog it was optimising: same row, same edit action,
18346
+ // every field rendered (a form has no minColumns). Nothing is submitted that the operator could not see.
18347
+ if (this.config.inlineEdit && this.canEditInline()) {
17983
18348
  this.startInlineEdit(actionData);
17984
18349
  return;
17985
18350
  }
@@ -18067,6 +18432,8 @@ class TableComponent {
18067
18432
  return;
18068
18433
  }
18069
18434
  this.dialogService.openDefaultDetailsDialog(btn, row, this.nestingLevel).subscribe(result => {
18435
+ if (!result)
18436
+ return; // Added: Escape or a backdrop click closes with undefined — a dismissal is a normal outcome, so fall straight out rather than throwing on result.action. No branch below is a default, so doing nothing is exactly right: no refresh, no emit, no onSuccessButton.
18070
18437
  if (result.action === 'doAction') {
18071
18438
  this.doAction(result.name, result.row);
18072
18439
  this.actionClickedEmit(result.name, result.row);
@@ -18437,11 +18804,11 @@ class TableComponent {
18437
18804
  this.setPaginator();
18438
18805
  }
18439
18806
  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 }); }
18440
- 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 <!-- Changed: [isRealTime] binds the RESOLVED value, not the raw config. It was `config.realTime`, so the live\n indicator only ever appeared on a table that set the flag ITSELF \u2014 a table relying on the app-wide\n `tableDefaults.realTime` was genuinely subscribed to SignalR (setupRealTimeSubscriptions and\n realTimeRefreshOrFallback both resolve through effRealTime) and simply never showed the dot. That is\n how \"Trips and Loads have no real time\" got reported: a status light disagreeing with the system it\n reports on. effRealTime also gets the inverse right \u2014 realTime:true under a global false still lights,\n and realTime:false under a global true stays dark. -->\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]=\"effRealTime\" [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 <!-- Changed: hidePageSize on a phone. The \"Items per page\" label and its select cost a whole row of a\n narrow screen for a control almost nobody touches there \u2014 the default is what gets used. The range\n (\"1 \u2013 10 of 54\") and the arrows stay, which is the part that is actually navigated. Bound to\n smallScreen, the component's existing breakpoint (max-width 600px, live via BreakpointObserver), so\n the paginator agrees with how this table already decides what \"mobile\" means rather than\n introducing a third breakpoint. -->\n <mat-paginator *ngIf=\"!pagedMode\"\n #tablePaginator\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [hidePageSize]=\"smallScreen\"\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 [hidePageSize]=\"smallScreen\"\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" }] }); }
18807
+ 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 <!-- Changed: [isRealTime] binds the RESOLVED value, not the raw config. It was `config.realTime`, so the live\n indicator only ever appeared on a table that set the flag ITSELF \u2014 a table relying on the app-wide\n `tableDefaults.realTime` was genuinely subscribed to SignalR (setupRealTimeSubscriptions and\n realTimeRefreshOrFallback both resolve through effRealTime) and simply never showed the dot. That is\n how \"Trips and Loads have no real time\" got reported: a status light disagreeing with the system it\n reports on. effRealTime also gets the inverse right \u2014 realTime:true under a global false still lights,\n and realTime:false under a global true stays dark. -->\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]=\"effRealTime\" [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 Changed: the condition moved into filterScopePartial so this banner and the empty-state message below\n read the SAME predicate. Stated independently they drifted: this banner said \"only the 20 loaded rows\n of 240\" while the message underneath offered \"all 240 rows\". This banner owns SCOPE (what was searched)\n and the remedy; the message below owns the RESULT; the paginator owns the COUNT. One fact each. -->\n <div *ngIf=\"filterScopePartial\" 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 && !loadFailed\"><em>Loading...</em></p> <!-- Changed (Quiet Loading): the bare text is replaced by the stage below while a quiet load is on screen --><!-- Changed: ...and a FAILED load is not a running one \u2014 without the loadFailed guard this line sat there forever once the error dialog was dismissed -->\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 <!-- Changed: hidePageSize on a phone. The \"Items per page\" label and its select cost a whole row of a\n narrow screen for a control almost nobody touches there \u2014 the default is what gets used. The range\n (\"1 \u2013 10 of 54\") and the arrows stay, which is the part that is actually navigated. Bound to\n smallScreen, the component's existing breakpoint (max-width 600px, live via BreakpointObserver), so\n the paginator agrees with how this table already decides what \"mobile\" means rather than\n introducing a third breakpoint. -->\n <mat-paginator *ngIf=\"!pagedMode\"\n #tablePaginator\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [hidePageSize]=\"smallScreen\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial' || singlePagePaginatorHidden}\"\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 [hidePageSize]=\"smallScreen\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial' || singlePagePaginatorHidden}\"\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' && !loadFailed\"><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 --><!-- Changed: ...and NOT when the load failed. A failed read leaves dataSource as an empty array, so this line confidently reported \"No Data\" \u2014 an assertion about the data \u2014 when the truth was that we never got any. That state now has its own sentence below. -->\n\n <!-- Added: the load-failed state. \"There is nothing here\" and \"we could not find out\" are different facts and\n this grid stated the first for both. Gated on having no rows, so a failed REFRESH leaves the rows already\n on screen alone \u2014 the error dialog covers that case and an empty state under live rows would be a lie. -->\n <div *ngIf=\"!(dataSource?.length > 0) && !loadingStage && loadFailed\" class=\"tin-filter-empty\" role=\"status\">\n <p class=\"tin-filter-empty-title\"><em>Couldn\u2019t load {{stageEntityName}}</em></p>\n <p class=\"tin-filter-empty-hint\">Check your connection, then use the refresh button to try again.</p>\n </div>\n\n <!-- Added: the OTHER empty state. The line above means \"there is nothing here yet\"; this one means \"nothing\n matches what you typed\", and until now it rendered nothing at all \u2014 a header, a \"0 of 0\" paginator and\n no reason. filteredToNothing is false whenever dataSource is empty, so the two can never both show and\n the line above is reached on exactly the rows it always was. role=\"status\" announces the change to a\n screen reader, which otherwise gets silence when the rows disappear. -->\n <!-- Changed: ONE message became TWO, because \"no matches\" means two different things and only one of them\n was ever true. In paged mode the client filter runs over the loaded WINDOW and issues no fetch, so with\n a partial window \"No matches for X\" overstates what was searched and \"see all 240 rows\" implies the\n filter covered 240 when it covered 20 \u2014 while the banner at the top of the grid was simultaneously\n saying so. The split is driven by filterScopePartial, the same getter that banner binds to, so the two\n cannot contradict each other. Neither variant restates a COUNT the paginator or the banner already owns. -->\n\n <!-- COMPLETE scope: the filter saw every row there is (any unpaged grid, or a paged one holding the full\n set \u2014 search/POST mode and the no-total degrade both land here). The total is honest, so it is offered. -->\n <div *ngIf=\"filteredToNothing && !filterScopePartial\" class=\"tin-filter-empty\" role=\"status\">\n <p class=\"tin-filter-empty-title\"><em>{{filterEmptyTitle}}</em></p>\n <p class=\"tin-filter-empty-hint\">Clear the filter to see all {{filteredTotal}} {{filteredRowNoun}}.</p>\n </div>\n\n <!-- PARTIAL scope: the filter saw only the rows loaded so far. The headline says exactly that, and the hint\n stops at the way back \u2014 the banner above already carries both the counts and the \"use search\" remedy,\n so repeating either here would be the contradiction this split exists to remove. -->\n <div *ngIf=\"filteredToNothing && filterScopePartial\" class=\"tin-filter-empty\" role=\"status\">\n <p class=\"tin-filter-empty-title\"><em>{{filterEmptyTitlePartial}}</em></p>\n <p class=\"tin-filter-empty-hint\">Clear the filter to keep browsing.</p>\n </div>\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}.tin-filter-empty{text-align:center;padding:24px 12px}.tin-filter-empty-title{margin:0}.tin-filter-empty-hint{margin:6px 0 0;font-size:.85em;opacity:.7;overflow-wrap:anywhere}\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" }] }); }
18441
18808
  }
18442
18809
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableComponent, decorators: [{
18443
18810
  type: Component,
18444
- 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 <!-- Changed: [isRealTime] binds the RESOLVED value, not the raw config. It was `config.realTime`, so the live\n indicator only ever appeared on a table that set the flag ITSELF \u2014 a table relying on the app-wide\n `tableDefaults.realTime` was genuinely subscribed to SignalR (setupRealTimeSubscriptions and\n realTimeRefreshOrFallback both resolve through effRealTime) and simply never showed the dot. That is\n how \"Trips and Loads have no real time\" got reported: a status light disagreeing with the system it\n reports on. effRealTime also gets the inverse right \u2014 realTime:true under a global false still lights,\n and realTime:false under a global true stays dark. -->\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]=\"effRealTime\" [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 <!-- Changed: hidePageSize on a phone. The \"Items per page\" label and its select cost a whole row of a\n narrow screen for a control almost nobody touches there \u2014 the default is what gets used. The range\n (\"1 \u2013 10 of 54\") and the arrows stay, which is the part that is actually navigated. Bound to\n smallScreen, the component's existing breakpoint (max-width 600px, live via BreakpointObserver), so\n the paginator agrees with how this table already decides what \"mobile\" means rather than\n introducing a third breakpoint. -->\n <mat-paginator *ngIf=\"!pagedMode\"\n #tablePaginator\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [hidePageSize]=\"smallScreen\"\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 [hidePageSize]=\"smallScreen\"\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"] }]
18811
+ 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 <!-- Changed: [isRealTime] binds the RESOLVED value, not the raw config. It was `config.realTime`, so the live\n indicator only ever appeared on a table that set the flag ITSELF \u2014 a table relying on the app-wide\n `tableDefaults.realTime` was genuinely subscribed to SignalR (setupRealTimeSubscriptions and\n realTimeRefreshOrFallback both resolve through effRealTime) and simply never showed the dot. That is\n how \"Trips and Loads have no real time\" got reported: a status light disagreeing with the system it\n reports on. effRealTime also gets the inverse right \u2014 realTime:true under a global false still lights,\n and realTime:false under a global true stays dark. -->\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]=\"effRealTime\" [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 Changed: the condition moved into filterScopePartial so this banner and the empty-state message below\n read the SAME predicate. Stated independently they drifted: this banner said \"only the 20 loaded rows\n of 240\" while the message underneath offered \"all 240 rows\". This banner owns SCOPE (what was searched)\n and the remedy; the message below owns the RESULT; the paginator owns the COUNT. One fact each. -->\n <div *ngIf=\"filterScopePartial\" 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 && !loadFailed\"><em>Loading...</em></p> <!-- Changed (Quiet Loading): the bare text is replaced by the stage below while a quiet load is on screen --><!-- Changed: ...and a FAILED load is not a running one \u2014 without the loadFailed guard this line sat there forever once the error dialog was dismissed -->\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 <!-- Changed: hidePageSize on a phone. The \"Items per page\" label and its select cost a whole row of a\n narrow screen for a control almost nobody touches there \u2014 the default is what gets used. The range\n (\"1 \u2013 10 of 54\") and the arrows stay, which is the part that is actually navigated. Bound to\n smallScreen, the component's existing breakpoint (max-width 600px, live via BreakpointObserver), so\n the paginator agrees with how this table already decides what \"mobile\" means rather than\n introducing a third breakpoint. -->\n <mat-paginator *ngIf=\"!pagedMode\"\n #tablePaginator\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [hidePageSize]=\"smallScreen\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial' || singlePagePaginatorHidden}\"\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 [hidePageSize]=\"smallScreen\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial' || singlePagePaginatorHidden}\"\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' && !loadFailed\"><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 --><!-- Changed: ...and NOT when the load failed. A failed read leaves dataSource as an empty array, so this line confidently reported \"No Data\" \u2014 an assertion about the data \u2014 when the truth was that we never got any. That state now has its own sentence below. -->\n\n <!-- Added: the load-failed state. \"There is nothing here\" and \"we could not find out\" are different facts and\n this grid stated the first for both. Gated on having no rows, so a failed REFRESH leaves the rows already\n on screen alone \u2014 the error dialog covers that case and an empty state under live rows would be a lie. -->\n <div *ngIf=\"!(dataSource?.length > 0) && !loadingStage && loadFailed\" class=\"tin-filter-empty\" role=\"status\">\n <p class=\"tin-filter-empty-title\"><em>Couldn\u2019t load {{stageEntityName}}</em></p>\n <p class=\"tin-filter-empty-hint\">Check your connection, then use the refresh button to try again.</p>\n </div>\n\n <!-- Added: the OTHER empty state. The line above means \"there is nothing here yet\"; this one means \"nothing\n matches what you typed\", and until now it rendered nothing at all \u2014 a header, a \"0 of 0\" paginator and\n no reason. filteredToNothing is false whenever dataSource is empty, so the two can never both show and\n the line above is reached on exactly the rows it always was. role=\"status\" announces the change to a\n screen reader, which otherwise gets silence when the rows disappear. -->\n <!-- Changed: ONE message became TWO, because \"no matches\" means two different things and only one of them\n was ever true. In paged mode the client filter runs over the loaded WINDOW and issues no fetch, so with\n a partial window \"No matches for X\" overstates what was searched and \"see all 240 rows\" implies the\n filter covered 240 when it covered 20 \u2014 while the banner at the top of the grid was simultaneously\n saying so. The split is driven by filterScopePartial, the same getter that banner binds to, so the two\n cannot contradict each other. Neither variant restates a COUNT the paginator or the banner already owns. -->\n\n <!-- COMPLETE scope: the filter saw every row there is (any unpaged grid, or a paged one holding the full\n set \u2014 search/POST mode and the no-total degrade both land here). The total is honest, so it is offered. -->\n <div *ngIf=\"filteredToNothing && !filterScopePartial\" class=\"tin-filter-empty\" role=\"status\">\n <p class=\"tin-filter-empty-title\"><em>{{filterEmptyTitle}}</em></p>\n <p class=\"tin-filter-empty-hint\">Clear the filter to see all {{filteredTotal}} {{filteredRowNoun}}.</p>\n </div>\n\n <!-- PARTIAL scope: the filter saw only the rows loaded so far. The headline says exactly that, and the hint\n stops at the way back \u2014 the banner above already carries both the counts and the \"use search\" remedy,\n so repeating either here would be the contradiction this split exists to remove. -->\n <div *ngIf=\"filteredToNothing && filterScopePartial\" class=\"tin-filter-empty\" role=\"status\">\n <p class=\"tin-filter-empty-title\"><em>{{filterEmptyTitlePartial}}</em></p>\n <p class=\"tin-filter-empty-hint\">Clear the filter to keep browsing.</p>\n </div>\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}.tin-filter-empty{text-align:center;padding:24px 12px}.tin-filter-empty-title{margin:0}.tin-filter-empty-hint{margin:6px 0 0;font-size:.85em;opacity:.7;overflow-wrap:anywhere}\n"] }]
18445
18812
  }], 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: [{
18446
18813
  type: Optional
18447
18814
  }, {
@@ -18502,6 +18869,7 @@ class DayBookComponent {
18502
18869
  this.messageService = inject(MessageService); // Added: page actions confirm and toast like every other button in the suite
18503
18870
  this.apiErrorService = inject(ApiErrorService);
18504
18871
  this.buttonService = inject(ButtonService);
18872
+ this.zone = inject(NgZone, { optional: true }); // Added: optional so a bare test harness instantiating this class directly still constructs
18505
18873
  this.book = null;
18506
18874
  this.loading = false; // the re-entrancy guard — deliberately NOT bound in the template (see showLoading)
18507
18875
  // Added (NG0100): what the template actually binds. `loading` flips inside load(), and load() can be reached
@@ -18582,7 +18950,10 @@ class DayBookComponent {
18582
18950
  // pure config silently lost its Refresh. These come from config instead, and the ng-content slot is still
18583
18951
  // there beside them for anyone who does host the page themselves.
18584
18952
  buildActions() {
18585
- this.pageActions = (this.config?.actions || []).filter(b => Core.isItemVisible(b, this.book));
18953
+ // Changed: also honour the button's opt-in capability. Day Book actions carry NO formConfig, so nothing in
18954
+ // the library could ever gate them — hasCapability is the only gate that reaches here. Buttons that name no
18955
+ // capability are unaffected (hasCapability returns true); only isItemVisible ran before and it still runs.
18956
+ this.pageActions = (this.config?.actions || []).filter(b => Core.isItemVisible(b, this.book) && this.buttonService.hasCapability(b));
18586
18957
  }
18587
18958
  pageActionClicked(button) {
18588
18959
  if (button.onClick) {
@@ -18714,6 +19085,13 @@ class DayBookComponent {
18714
19085
  const cfg = { ...section.tableConfig };
18715
19086
  cfg.sectionConfig = section.tableConfig.sectionConfig ? { ...section.tableConfig.sectionConfig, collapsed: section.collapsed !== false } : undefined;
18716
19087
  cfg.elevation = cfg.elevation ?? 'none'; // flat tables on the section cards — Day Book design language
19088
+ // Added (phone density): a Day Book stacks 9-16 of these cards, and on a phone each one was spending
19089
+ // 56px on a pager that read "1 – 4 of 4" — the largest single item in the card, navigating nothing.
19090
+ // The count it reports is already on the section header's own badge, so nothing is lost. This is a Day
19091
+ // Book decision made ONCE in the library rather than repeated in five app configs, exactly as the
19092
+ // carousel stage strip is; an app that wants its pagers back still sets the flag false on its own
19093
+ // tableConfig. Desktop is untouched — the flag only ever acts below 600px.
19094
+ cfg.hideSinglePagePaginator = cfg.hideSinglePagePaginator ?? true;
18717
19095
  if (cfg.sectionConfig) {
18718
19096
  cfg.sectionConfig.hideWhenEmpty = cfg.sectionConfig.hideWhenEmpty !== false; // sections disappear when empty by default
18719
19097
  // Added: the section's "why this list exists" line rides INSIDE the table's own header, under the
@@ -18792,20 +19170,66 @@ class DayBookComponent {
18792
19170
  this.groupState[group.lane.key] = group.collapsed;
18793
19171
  localStorage.setItem('tin-daybook-groups', JSON.stringify(this.groupState));
18794
19172
  }
18795
- // Un-collapse BEFORE scrolling (piglet's fix, applied suite-wide) — a click must never land on a bare header
19173
+ // Un-collapse BEFORE scrolling (piglet's fix, applied suite-wide) — a click must never land on a bare header.
19174
+ //
19175
+ // Changed (owner, 2026-08-09): a chip scrolled to its section and left it SHUT, so the operator had to click
19176
+ // again to see the list he had just asked for. There are THREE doors between a chip and its rows, not one:
19177
+ //
19178
+ // chip → lane GROUP (.db-gbody is *ngIf="!g.collapsed") → SECTION (spa-table's own sectionCollapsed)
19179
+ //
19180
+ // and the previous code only really cleared the third. It opened the group by flipping `group.collapsed`, then
19181
+ // in the SAME synchronous tick asked `this.tables` for the section's table — but Angular had not re-rendered
19182
+ // yet, so that table did not exist in the QueryList and the expand silently no-opped. Which is exactly the
19183
+ // reported symptom, and it applied to every chip: chips are only rendered on a COLLAPSED group, so the broken
19184
+ // path was the only path a chip could ever take.
19185
+ //
19186
+ // The fix does not race the renderer. Level 2 seeds the section's own cloned config, so a table that has not
19187
+ // been created yet is BORN open (TableComponent.ngOnInit seeds sectionCollapsed off exactly this field), and
19188
+ // only expands a live instance if there already is one. Nothing here has to guess when Angular will catch up.
18796
19189
  goTo(key) {
18797
19190
  const target = this.sectionConfigs.find(x => x.section.key === key);
18798
19191
  if (target) {
18799
- // Changed: the same rule now has to clear TWO doors — a stage click, or a chip on a collapsed group,
18800
- // must open the group before it opens the section, or the scroll lands on nothing that is in the DOM.
19192
+ // Level 1 the lane group. Nothing inside a shut group is in the DOM at all.
18801
19193
  const group = this.visibleGroups.find(g => g.lane.key === target.section.lane);
18802
- if (group && group.collapsed)
18803
- this.toggleGroup(group);
18804
- const table = this.tables?.find(t => t.config === target.cfg); // Changed: one path the section is always a spa-table
18805
- if (table && table.sectionCollapsed)
18806
- table.toggleSection(); // goes through toggleSection so the paginator re-attaches
18807
- }
18808
- setTimeout(() => document.getElementById('sec-' + key)?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50);
19194
+ if (group?.collapsed)
19195
+ this.toggleGroup(group); // expand-only: an already-open group is left alone, and so is every OTHER group — this is not an accordion
19196
+ // Level 2 the section. Seeding the config covers the not-yet-created table; expandSection covers the live one.
19197
+ if (target.cfg?.sectionConfig)
19198
+ target.cfg.sectionConfig.collapsed = false; // cfg is this section's own clone (buildSections), never the app's shared singleton
19199
+ target.collapsed = false;
19200
+ this.tables?.find(t => t.config === target.cfg)?.expandSection(); // expand, never toggle a toggle would shut a section that was already open
19201
+ }
19202
+ // Level 3 — scroll, but only once the two expansions above have actually changed the layout.
19203
+ this.scrollWhenSettled('sec-' + key);
19204
+ }
19205
+ // Added: the scroll has to be the LAST thing that happens, and a fixed delay cannot promise that. A section
19206
+ // that is still collapsed when we scroll is ~110px of bare chrome: we land on that position, the section then
19207
+ // expands to several hundred px and grows UNDER the viewport, and the operator ends up parked below the list
19208
+ // he asked for — looking, from his side, exactly like the bug we are fixing. So instead of guessing a delay we
19209
+ // watch the target until its height stops changing (the same measurement two frames running), which covers the
19210
+ // group render, the table's own creation and the paginator re-attach without knowing anything about any of
19211
+ // them. The frame cap stops a section whose height never settles (a live-updating list) from never scrolling.
19212
+ // Runs outside Angular: this is a poll, and every frame of it would otherwise drag a change-detection pass
19213
+ // behind it for no reason — the house rule about not putting work on the CD path applies to timers too.
19214
+ scrollWhenSettled(id) {
19215
+ const smooth = !window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches; // a jump, not a glide, when the operator has asked for less motion
19216
+ const run = () => {
19217
+ let previous = -1;
19218
+ let frames = 0;
19219
+ const step = () => {
19220
+ const element = document.getElementById(id);
19221
+ const height = element ? element.getBoundingClientRect().height : -1;
19222
+ if ((element && height === previous) || frames >= 20) {
19223
+ element?.scrollIntoView({ behavior: smooth ? 'smooth' : 'auto', block: 'start' });
19224
+ return;
19225
+ }
19226
+ previous = height;
19227
+ frames++;
19228
+ requestAnimationFrame(step);
19229
+ };
19230
+ requestAnimationFrame(step);
19231
+ };
19232
+ this.zone ? this.zone.runOutsideAngular(run) : run();
18809
19233
  }
18810
19234
  //---------- Header facts ----------
18811
19235
  get booksFrom() {
@@ -18827,11 +19251,11 @@ class DayBookComponent {
18827
19251
  return stages.length > 0 && stages.every(s => !this.stageCount(s));
18828
19252
  }
18829
19253
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DayBookComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
18830
- 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" }] }); }
19254
+ 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}.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}@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;min-height:32px}.db-page{padding:10px 0;gap:12px}.db-section{padding:10px;border-left-width:3px}.db-why{margin-bottom:8px;font-size:12.5px}.db-group{gap:8px}.db-gbody{gap:10px}.db-ghead{padding:8px 10px;gap:8px;min-height:40px}.db-ghead>mat-icon{font-size:18px;width:18px;height:18px}.db-ghead .g-label{font-size:14px}.db-ghead .g-value{font-size:12.5px}.db-ghead .g-lists{font-size:11px}.db-ghead .g-chev{font-size:20px;width:20px;height:20px}.db-count{font-size:12px;padding:1px 7px}.db-chips{padding:7px 10px 8px;gap:6px}.db-chip{min-height:32px}.db-clear{padding:10px 12px;gap:8px}.db-loading{padding:24px}.db-empty{padding:32px 16px}}\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" }] }); }
18831
19255
  }
18832
19256
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DayBookComponent, decorators: [{
18833
19257
  type: Component,
18834
- 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"] }]
19258
+ 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}.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}@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;min-height:32px}.db-page{padding:10px 0;gap:12px}.db-section{padding:10px;border-left-width:3px}.db-why{margin-bottom:8px;font-size:12.5px}.db-group{gap:8px}.db-gbody{gap:10px}.db-ghead{padding:8px 10px;gap:8px;min-height:40px}.db-ghead>mat-icon{font-size:18px;width:18px;height:18px}.db-ghead .g-label{font-size:14px}.db-ghead .g-value{font-size:12.5px}.db-ghead .g-lists{font-size:11px}.db-ghead .g-chev{font-size:20px;width:20px;height:20px}.db-count{font-size:12px;padding:1px 7px}.db-chips{padding:7px 10px 8px;gap:6px}.db-chip{min-height:32px}.db-clear{padding:10px 12px;gap:8px}.db-loading{padding:24px}.db-empty{padding:32px 16px}}\n"] }]
18835
19259
  }], propDecorators: { tables: [{
18836
19260
  type: ViewChildren,
18837
19261
  args: [TableComponent]
@@ -19723,11 +20147,11 @@ class FormComponent {
19723
20147
  processForm() {
19724
20148
  }
19725
20149
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: FormComponent, deps: [{ token: MessageService }, { token: DataServiceLib }, { token: AuthService }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
19726
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: FormComponent, isStandalone: false, selector: "spa-form", inputs: { files: "files", data: "data", config: "config" }, outputs: { buttonClick: "buttonClick", inputChange: "inputChange" }, queries: [{ propertyName: "dynamicSelectTemplate", first: true, predicate: ["dynamicSelect"], descendants: true }], viewQueries: [{ propertyName: "defaultDynamicSelectTemplate", first: true, predicate: ["defaultDynamicSelect"], descendants: true, static: true }], ngImport: i0, template: "\n\n\n<div class=\"tin-form-container\" >\n <div [ngClass]=\"[multiColumn ? 'tin-grid' : 'tin-col', config.notesConfig ? 'width-75' : 'width-100']\" class=\"form-main-content\">\n\n <div *ngIf=\"!hasAccess\" class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n \n <div [ngClass]=\"field.span || field.type =='section' || field.type =='file' || field.type =='file-view' || field.type =='editor' ? 'span-col' : ''\" *ngFor=\"let field of visibleFields\"><!-- TS-11: bind cached visibleFields instead of getVisibleFields() per CD -->\n \n <ng-container>\n \n <ng-container [ngSwitch]=\"field.type\" class=\"highlight\">\n \n <div *ngSwitchCase=\"'section'\" class=\"title d-flex align-items-center\" (click)=\"toggleSection(field)\" style=\"cursor: pointer;\">\n <label style=\"font-size: larger;margin-right: 10px;\">{{field.alias ?? field.name | camelToWords}}</label>\n <mat-icon *ngIf=\"field.infoMessage\" (click)=\"onInfoClick($event, field.infoMessage)\" style=\"color: steelblue; font-size: 14px;\">info</mat-icon>\n <!-- <button mat-icon-button class=\"info-icon-button\" matTooltip=\"Info\" matTooltipPosition=\"above\">\n \n </button> -->\n <mat-icon *ngIf=\"hasSectionFields(field.name)\">{{shouldSectionCollapse(field) ? 'expand_more' : 'expand_less'}}</mat-icon>\n </div>\n \n <ng-container *ngSwitchCase=\"'file'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode !='view'\">\n <spa-attach [message]=\"field.alias ?? 'Drag and Drop files here'\" [(files)]=\"files\" [fileOptions]=\"field.fileOptions\"></spa-attach>\n </div>\n </ng-container>\n \n <ng-container *ngSwitchCase=\"'file-view'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode && config.mode !='create'\">\n <spa-viewer [fileAction]=\"field.loadAction\" [path]=\"field.path\" [folderName]=\"data[field.keyField]\" ></spa-viewer>\n </div>\n </ng-container>\n \n <spa-html *ngSwitchCase=\"'html'\" [display]=\"field.alias | camelToWords\" [value]=\"data[field.name]\" [maxHeight]=\"field.maxHeight\"></spa-html>\n \n <label *ngSwitchCase=\"'blank'\"></label>\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-label *ngSwitchCase=\"'label'\" [display]=\"field.alias ?? field.name | camelToWords\" [value]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [format]=\"field.format ?? 'text'\" [suffix]=\"field.suffix\" [size]=\"field.size\"></spa-label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [infoMessage]=\"field.infoMessage\" ></spa-check>\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [min]=\"field?.min\" [max]=\"field?.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [min]=\"field.min\" [max]=\"field.max\" [infoMessage]=\"field.infoMessage\" ></spa-datetime>\n \n <spa-email *ngSwitchCase=\"'email'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-email>\n \n <spa-text-mask *ngSwitchCase=\"'text-mask'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\"></spa-text-mask>\n \n \n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: field,\n field: field,\n data: data,\n testReadOnly: testReadOnly.bind(this),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n \n <spa-select-multi *ngSwitchCase=\"'select-multi'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [selectAll]=\"field.selectAll\">\n </spa-select-multi>\n \n <spa-text-multi *ngSwitchCase=\"'text-multi'\" [strict]=\"field.strict\" [display]=\"field.alias ?? field.name | camelToWords\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\"></spa-text-multi>\n \n \n \n <ng-container *ngSwitchCase=\"'composite'\">\n <div class=\"composite-field-container\">\n <div class=\"composite-field-group\">\n <ng-container *ngFor=\"let subfield of getVisibleSubfields(field)\">\n <ng-container [ngSwitch]=\"subfield.type\">\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [infoMessage]=\"subfield.infoMessage\" ></spa-check>\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [infoMessage]=\"subfield.infoMessage\" ></spa-datetime>\n \n <!-- Fixed: composite select subfields projected the PARENT composite field into the\n dynamic select template (wrong options/value binding) and omitted testRequired,\n so the template's testRequired(field) call threw and killed the whole dialog.\n Context now mirrors the top-level select outlet, with the SUBFIELD, and readonly\n cascades composite parent || subfield like every other subfield type. -->\n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: subfield,\n field: subfield,\n data: data,\n testReadOnly: compositeReadOnly(field),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [options]=\"subfield.options\" [optionDisplay]=\"subfield.optionDisplay ?? 'name'\" [optionValue]=\"subfield.optionValue ?? 'value'\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [loadAction]=\"resolveLoadAction(subfield)\" [regex]=\"subfield.regex\" [field]=\"subfield\" [data]=\"data\" [detailsConfig]=\"subfield.detailsConfig\" [masterField]=\"subfield.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [rows]=\"subfield.rows\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [editorConfig]=\"subfield.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text>\n \n \n </ng-container>\n </ng-container>\n </div>\n </div>\n </ng-container>\n \n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [regex]=\"field.regex\" [field]=\"field\" [data]=\"data\" [detailsConfig]=\"field.detailsConfig\" [masterField]=\"field.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [rows]=\"field.rows\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [editorConfig]=\"field.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text>\n \n </ng-container>\n \n </ng-container>\n \n </div>\n \n \n <div class=\"span-col-center\" *ngIf=\"config.button\">\n <button mat-raised-button color=\"primary\" (click)=\"buttonClicked()\" cdkFocusInitial>{{buttonDisplay}}</button>\n </div>\n \n \n </div>\n <!-- Fixed: built-in fallback select template \u2014 spa-form used consumers' projected #dynamicSelect\n (only detailsDialog provides one), so selects silently rendered NOTHING when spa-form was used\n directly. The projected template (if any) still wins; this default carries the same bindings. -->\n <ng-template #defaultDynamicSelect 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(field)\"\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\n <div class=\"notes-section\" *ngIf=\"config.notesConfig\">\n <spa-notes\n [title]=\"config.notesConfig.title || 'Notes'\"\n [notes]=\"config.notesConfig.notes || []\"\n [loadAction]=\"config.notesConfig.loadAction\"\n [loadIDField]=\"config.notesConfig.loadIDField\"\n [data]=\"data\"\n [nameField]=\"config.notesConfig.nameField || 'createdByName'\"\n [dateField]=\"config.notesConfig.dateField || 'createdDate'\"\n [commentField]=\"config.notesConfig.commentField || 'details'\">\n </spa-notes>\n </div>\n</div>\n\n", styles: [".title{margin-top:.5em;margin-bottom:.5em;font-size:larger;font-weight:300;color:#0b447e}.composite-field-group{display:flex;flex-direction:row;flex-wrap:wrap;gap:12px}.tin-form-container{display:flex;flex-direction:row;width:100%;gap:16px}.width-100{width:100%!important}.width-75{width:70%!important}.notes-section{width:400px;border-left:1px solid #e0e0e0}\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.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i1$2.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1$2.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1$2.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { 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: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextMaskComponent, selector: "spa-text-mask", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "regex", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextAreaComponent, selector: "spa-text-area", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "rows", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextSingleComponent, selector: "spa-text-single", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "width", "copyContent", "clearContent", "options", "optionDisplay", "optionValue", "loadAction", "required", "min", "max", "regex", "suffix", "infoMessage", "field", "data", "detailsConfig", "masterField"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: DateComponent, selector: "spa-date", inputs: ["required", "min", "max", "readonly", "hint", "value", "display", "placeholder", "width", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: DatetimeComponent, selector: "spa-datetime", inputs: ["display", "value", "readonly", "width", "min", "max", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: LabelComponent, selector: "spa-label", inputs: ["display", "value", "format", "suffix", "size"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: MoneyComponent, selector: "spa-money", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "currency", "required", "min", "max", "infoMessage", "copyContent", "clearContent", "suffix"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: AttachComponent, selector: "spa-attach", inputs: ["fileOptions", "message", "files", "enableUpload"], outputs: ["filesChange", "upload"] }, { kind: "component", type: NumberComponent, selector: "spa-number", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "step", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: ViewerComponent, selector: "spa-viewer", inputs: ["fileAction", "path", "folderName", "fileNames", "removable", "display", "title"], outputs: ["remove"] }, { kind: "component", type: EmailComponent, selector: "spa-email", inputs: ["display", "value", "readonly", "required", "hint", "suffix", "infoMessage", "copyContent", "clearContent", "options", "optionValue"], outputs: ["valueChange"] }, { kind: "component", type: TextMultiComponent, selector: "spa-text-multi", inputs: ["display", "value", "readonly", "required", "hint", "strict", "suffix", "infoMessage", "copyContent", "clearContent", "options", "optionDisplay", "optionValue", "loadAction"], outputs: ["valueChange", "hoverChange"] }, { kind: "component", type: SelectMultiComponent, selector: "spa-select-multi", inputs: ["display", "value", "readonly", "required", "hint", "options", "optionDisplay", "optionValue", "infoMessage", "copyContent", "clearContent", "nullable", "placeholder", "width", "suffix", "loadAction", "selectAll"], outputs: ["valueChange", "hoverChange"] }, { kind: "component", type: HtmlComponent, selector: "spa-html", inputs: ["value", "maxHeight", "display"] }, { kind: "component", type: EditorComponent, selector: "spa-editor", inputs: ["display", "value", "readonly", "required", "hint", "infoMessage", "placeholder", "width", "height", "minHeight", "defaultFontName", "editorConfig"], outputs: ["valueChange"] }, { kind: "component", type: NotesComponent, selector: "spa-notes", inputs: ["title", "notes", "loadAction", "loadIDField", "data", "nameField", "dateField", "commentField"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
20150
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: FormComponent, isStandalone: false, selector: "spa-form", inputs: { files: "files", data: "data", config: "config" }, outputs: { buttonClick: "buttonClick", inputChange: "inputChange" }, queries: [{ propertyName: "dynamicSelectTemplate", first: true, predicate: ["dynamicSelect"], descendants: true }], viewQueries: [{ propertyName: "defaultDynamicSelectTemplate", first: true, predicate: ["defaultDynamicSelect"], descendants: true, static: true }], ngImport: i0, template: "\n\n\n<div class=\"tin-form-container\" >\n <div [ngClass]=\"[multiColumn ? 'tin-grid' : 'tin-col', config.notesConfig ? 'width-75' : 'width-100']\" class=\"form-main-content\">\n\n <div *ngIf=\"!hasAccess\" class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n \n <div [ngClass]=\"field.span || field.type =='section' || field.type =='file' || field.type =='file-view' || field.type =='editor' ? 'span-col' : ''\" *ngFor=\"let field of visibleFields\"><!-- TS-11: bind cached visibleFields instead of getVisibleFields() per CD -->\n \n <ng-container>\n \n <ng-container [ngSwitch]=\"field.type\" class=\"highlight\">\n \n <div *ngSwitchCase=\"'section'\" class=\"title d-flex align-items-center\" (click)=\"toggleSection(field)\" style=\"cursor: pointer;\">\n <label style=\"font-size: larger;margin-right: 10px;\">{{field.alias ?? field.name | camelToWords}}</label>\n <mat-icon *ngIf=\"field.infoMessage\" (click)=\"onInfoClick($event, field.infoMessage)\" style=\"color: steelblue; font-size: 14px;\">info</mat-icon>\n <!-- <button mat-icon-button class=\"info-icon-button\" matTooltip=\"Info\" matTooltipPosition=\"above\">\n \n </button> -->\n <mat-icon *ngIf=\"hasSectionFields(field.name)\">{{shouldSectionCollapse(field) ? 'expand_more' : 'expand_less'}}</mat-icon>\n </div>\n \n <ng-container *ngSwitchCase=\"'file'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode !='view'\">\n <spa-attach [message]=\"field.alias ?? 'Drag and Drop files here'\" [(files)]=\"files\" [fileOptions]=\"field.fileOptions\"></spa-attach>\n </div>\n </ng-container>\n \n <ng-container *ngSwitchCase=\"'file-view'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode && config.mode !='create'\">\n <spa-viewer [fileAction]=\"field.loadAction\" [path]=\"field.path\" [folderName]=\"data[field.keyField]\" ></spa-viewer>\n </div>\n </ng-container>\n \n <spa-html *ngSwitchCase=\"'html'\" [display]=\"field.alias | camelToWords\" [value]=\"data[field.name]\" [maxHeight]=\"field.maxHeight\"></spa-html>\n \n <label *ngSwitchCase=\"'blank'\"></label>\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-label *ngSwitchCase=\"'label'\" [display]=\"field.alias ?? field.name | camelToWords\" [value]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [format]=\"field.format ?? 'text'\" [suffix]=\"field.suffix\" [size]=\"field.size\"></spa-label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [infoMessage]=\"field.infoMessage\" [hint]=\"field.hint\" ></spa-check><!-- Changed: field.hint was dropped on the floor for checkboxes only \u2014 every other type already passed it through -->\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [min]=\"field?.min\" [max]=\"field?.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [min]=\"field.min\" [max]=\"field.max\" [infoMessage]=\"field.infoMessage\" ></spa-datetime>\n \n <spa-email *ngSwitchCase=\"'email'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-email>\n \n <spa-text-mask *ngSwitchCase=\"'text-mask'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\"></spa-text-mask>\n \n \n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: field,\n field: field,\n data: data,\n testReadOnly: testReadOnly.bind(this),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n \n <spa-select-multi *ngSwitchCase=\"'select-multi'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [selectAll]=\"field.selectAll\">\n </spa-select-multi>\n \n <spa-text-multi *ngSwitchCase=\"'text-multi'\" [strict]=\"field.strict\" [display]=\"field.alias ?? field.name | camelToWords\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\"></spa-text-multi>\n \n \n \n <ng-container *ngSwitchCase=\"'composite'\">\n <div class=\"composite-field-container\">\n <div class=\"composite-field-group\">\n <ng-container *ngFor=\"let subfield of getVisibleSubfields(field)\">\n <ng-container [ngSwitch]=\"subfield.type\">\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [infoMessage]=\"subfield.infoMessage\" [hint]=\"subfield.hint\" ></spa-check><!-- Changed: same hint pass-through as the top-level checkbox above -->\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [infoMessage]=\"subfield.infoMessage\" ></spa-datetime>\n \n <!-- Fixed: composite select subfields projected the PARENT composite field into the\n dynamic select template (wrong options/value binding) and omitted testRequired,\n so the template's testRequired(field) call threw and killed the whole dialog.\n Context now mirrors the top-level select outlet, with the SUBFIELD, and readonly\n cascades composite parent || subfield like every other subfield type. -->\n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: subfield,\n field: subfield,\n data: data,\n testReadOnly: compositeReadOnly(field),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [options]=\"subfield.options\" [optionDisplay]=\"subfield.optionDisplay ?? 'name'\" [optionValue]=\"subfield.optionValue ?? 'value'\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [loadAction]=\"resolveLoadAction(subfield)\" [regex]=\"subfield.regex\" [field]=\"subfield\" [data]=\"data\" [detailsConfig]=\"subfield.detailsConfig\" [masterField]=\"subfield.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [rows]=\"subfield.rows\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [editorConfig]=\"subfield.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text>\n \n \n </ng-container>\n </ng-container>\n </div>\n </div>\n </ng-container>\n \n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [regex]=\"field.regex\" [field]=\"field\" [data]=\"data\" [detailsConfig]=\"field.detailsConfig\" [masterField]=\"field.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [rows]=\"field.rows\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [editorConfig]=\"field.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text>\n \n </ng-container>\n \n </ng-container>\n \n </div>\n \n \n <div class=\"span-col-center\" *ngIf=\"config.button\">\n <button mat-raised-button color=\"primary\" (click)=\"buttonClicked()\" cdkFocusInitial>{{buttonDisplay}}</button>\n </div>\n \n \n </div>\n <!-- Fixed: built-in fallback select template \u2014 spa-form used consumers' projected #dynamicSelect\n (only detailsDialog provides one), so selects silently rendered NOTHING when spa-form was used\n directly. The projected template (if any) still wins; this default carries the same bindings. -->\n <ng-template #defaultDynamicSelect 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(field)\"\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\n <div class=\"notes-section\" *ngIf=\"config.notesConfig\">\n <spa-notes\n [title]=\"config.notesConfig.title || 'Notes'\"\n [notes]=\"config.notesConfig.notes || []\"\n [loadAction]=\"config.notesConfig.loadAction\"\n [loadIDField]=\"config.notesConfig.loadIDField\"\n [data]=\"data\"\n [nameField]=\"config.notesConfig.nameField || 'createdByName'\"\n [dateField]=\"config.notesConfig.dateField || 'createdDate'\"\n [commentField]=\"config.notesConfig.commentField || 'details'\">\n </spa-notes>\n </div>\n</div>\n\n", styles: [".title{margin-top:.5em;margin-bottom:.5em;font-size:larger;font-weight:300;color:#0b447e}.composite-field-group{display:flex;flex-direction:row;flex-wrap:wrap;gap:12px}.tin-form-container{display:flex;flex-direction:row;width:100%;gap:16px}.width-100{width:100%!important}.width-75{width:70%!important}.notes-section{width:400px;border-left:1px solid #e0e0e0}\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.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i1$2.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1$2.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1$2.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { 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: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextMaskComponent, selector: "spa-text-mask", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "regex", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextAreaComponent, selector: "spa-text-area", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "rows", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextSingleComponent, selector: "spa-text-single", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "width", "copyContent", "clearContent", "options", "optionDisplay", "optionValue", "loadAction", "required", "min", "max", "regex", "suffix", "infoMessage", "field", "data", "detailsConfig", "masterField"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage", "hint"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: DateComponent, selector: "spa-date", inputs: ["required", "min", "max", "readonly", "hint", "value", "display", "placeholder", "width", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: DatetimeComponent, selector: "spa-datetime", inputs: ["display", "value", "readonly", "width", "min", "max", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: LabelComponent, selector: "spa-label", inputs: ["display", "value", "format", "suffix", "size"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: MoneyComponent, selector: "spa-money", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "currency", "required", "min", "max", "infoMessage", "copyContent", "clearContent", "suffix"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: AttachComponent, selector: "spa-attach", inputs: ["fileOptions", "message", "files", "enableUpload"], outputs: ["filesChange", "upload"] }, { kind: "component", type: NumberComponent, selector: "spa-number", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "step", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: ViewerComponent, selector: "spa-viewer", inputs: ["fileAction", "path", "folderName", "fileNames", "removable", "display", "title"], outputs: ["remove"] }, { kind: "component", type: EmailComponent, selector: "spa-email", inputs: ["display", "value", "readonly", "required", "hint", "suffix", "infoMessage", "copyContent", "clearContent", "options", "optionValue"], outputs: ["valueChange"] }, { kind: "component", type: TextMultiComponent, selector: "spa-text-multi", inputs: ["display", "value", "readonly", "required", "hint", "strict", "suffix", "infoMessage", "copyContent", "clearContent", "options", "optionDisplay", "optionValue", "loadAction"], outputs: ["valueChange", "hoverChange"] }, { kind: "component", type: SelectMultiComponent, selector: "spa-select-multi", inputs: ["display", "value", "readonly", "required", "hint", "options", "optionDisplay", "optionValue", "infoMessage", "copyContent", "clearContent", "nullable", "placeholder", "width", "suffix", "loadAction", "selectAll"], outputs: ["valueChange", "hoverChange"] }, { kind: "component", type: HtmlComponent, selector: "spa-html", inputs: ["value", "maxHeight", "display"] }, { kind: "component", type: EditorComponent, selector: "spa-editor", inputs: ["display", "value", "readonly", "required", "hint", "infoMessage", "placeholder", "width", "height", "minHeight", "defaultFontName", "editorConfig"], outputs: ["valueChange"] }, { kind: "component", type: NotesComponent, selector: "spa-notes", inputs: ["title", "notes", "loadAction", "loadIDField", "data", "nameField", "dateField", "commentField"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
19727
20151
  }
19728
20152
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: FormComponent, decorators: [{
19729
20153
  type: Component,
19730
- args: [{ selector: 'spa-form', standalone: false, template: "\n\n\n<div class=\"tin-form-container\" >\n <div [ngClass]=\"[multiColumn ? 'tin-grid' : 'tin-col', config.notesConfig ? 'width-75' : 'width-100']\" class=\"form-main-content\">\n\n <div *ngIf=\"!hasAccess\" class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n \n <div [ngClass]=\"field.span || field.type =='section' || field.type =='file' || field.type =='file-view' || field.type =='editor' ? 'span-col' : ''\" *ngFor=\"let field of visibleFields\"><!-- TS-11: bind cached visibleFields instead of getVisibleFields() per CD -->\n \n <ng-container>\n \n <ng-container [ngSwitch]=\"field.type\" class=\"highlight\">\n \n <div *ngSwitchCase=\"'section'\" class=\"title d-flex align-items-center\" (click)=\"toggleSection(field)\" style=\"cursor: pointer;\">\n <label style=\"font-size: larger;margin-right: 10px;\">{{field.alias ?? field.name | camelToWords}}</label>\n <mat-icon *ngIf=\"field.infoMessage\" (click)=\"onInfoClick($event, field.infoMessage)\" style=\"color: steelblue; font-size: 14px;\">info</mat-icon>\n <!-- <button mat-icon-button class=\"info-icon-button\" matTooltip=\"Info\" matTooltipPosition=\"above\">\n \n </button> -->\n <mat-icon *ngIf=\"hasSectionFields(field.name)\">{{shouldSectionCollapse(field) ? 'expand_more' : 'expand_less'}}</mat-icon>\n </div>\n \n <ng-container *ngSwitchCase=\"'file'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode !='view'\">\n <spa-attach [message]=\"field.alias ?? 'Drag and Drop files here'\" [(files)]=\"files\" [fileOptions]=\"field.fileOptions\"></spa-attach>\n </div>\n </ng-container>\n \n <ng-container *ngSwitchCase=\"'file-view'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode && config.mode !='create'\">\n <spa-viewer [fileAction]=\"field.loadAction\" [path]=\"field.path\" [folderName]=\"data[field.keyField]\" ></spa-viewer>\n </div>\n </ng-container>\n \n <spa-html *ngSwitchCase=\"'html'\" [display]=\"field.alias | camelToWords\" [value]=\"data[field.name]\" [maxHeight]=\"field.maxHeight\"></spa-html>\n \n <label *ngSwitchCase=\"'blank'\"></label>\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-label *ngSwitchCase=\"'label'\" [display]=\"field.alias ?? field.name | camelToWords\" [value]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [format]=\"field.format ?? 'text'\" [suffix]=\"field.suffix\" [size]=\"field.size\"></spa-label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [infoMessage]=\"field.infoMessage\" ></spa-check>\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [min]=\"field?.min\" [max]=\"field?.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [min]=\"field.min\" [max]=\"field.max\" [infoMessage]=\"field.infoMessage\" ></spa-datetime>\n \n <spa-email *ngSwitchCase=\"'email'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-email>\n \n <spa-text-mask *ngSwitchCase=\"'text-mask'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\"></spa-text-mask>\n \n \n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: field,\n field: field,\n data: data,\n testReadOnly: testReadOnly.bind(this),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n \n <spa-select-multi *ngSwitchCase=\"'select-multi'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [selectAll]=\"field.selectAll\">\n </spa-select-multi>\n \n <spa-text-multi *ngSwitchCase=\"'text-multi'\" [strict]=\"field.strict\" [display]=\"field.alias ?? field.name | camelToWords\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\"></spa-text-multi>\n \n \n \n <ng-container *ngSwitchCase=\"'composite'\">\n <div class=\"composite-field-container\">\n <div class=\"composite-field-group\">\n <ng-container *ngFor=\"let subfield of getVisibleSubfields(field)\">\n <ng-container [ngSwitch]=\"subfield.type\">\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [infoMessage]=\"subfield.infoMessage\" ></spa-check>\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [infoMessage]=\"subfield.infoMessage\" ></spa-datetime>\n \n <!-- Fixed: composite select subfields projected the PARENT composite field into the\n dynamic select template (wrong options/value binding) and omitted testRequired,\n so the template's testRequired(field) call threw and killed the whole dialog.\n Context now mirrors the top-level select outlet, with the SUBFIELD, and readonly\n cascades composite parent || subfield like every other subfield type. -->\n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: subfield,\n field: subfield,\n data: data,\n testReadOnly: compositeReadOnly(field),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [options]=\"subfield.options\" [optionDisplay]=\"subfield.optionDisplay ?? 'name'\" [optionValue]=\"subfield.optionValue ?? 'value'\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [loadAction]=\"resolveLoadAction(subfield)\" [regex]=\"subfield.regex\" [field]=\"subfield\" [data]=\"data\" [detailsConfig]=\"subfield.detailsConfig\" [masterField]=\"subfield.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [rows]=\"subfield.rows\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [editorConfig]=\"subfield.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text>\n \n \n </ng-container>\n </ng-container>\n </div>\n </div>\n </ng-container>\n \n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [regex]=\"field.regex\" [field]=\"field\" [data]=\"data\" [detailsConfig]=\"field.detailsConfig\" [masterField]=\"field.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [rows]=\"field.rows\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [editorConfig]=\"field.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text>\n \n </ng-container>\n \n </ng-container>\n \n </div>\n \n \n <div class=\"span-col-center\" *ngIf=\"config.button\">\n <button mat-raised-button color=\"primary\" (click)=\"buttonClicked()\" cdkFocusInitial>{{buttonDisplay}}</button>\n </div>\n \n \n </div>\n <!-- Fixed: built-in fallback select template \u2014 spa-form used consumers' projected #dynamicSelect\n (only detailsDialog provides one), so selects silently rendered NOTHING when spa-form was used\n directly. The projected template (if any) still wins; this default carries the same bindings. -->\n <ng-template #defaultDynamicSelect 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(field)\"\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\n <div class=\"notes-section\" *ngIf=\"config.notesConfig\">\n <spa-notes\n [title]=\"config.notesConfig.title || 'Notes'\"\n [notes]=\"config.notesConfig.notes || []\"\n [loadAction]=\"config.notesConfig.loadAction\"\n [loadIDField]=\"config.notesConfig.loadIDField\"\n [data]=\"data\"\n [nameField]=\"config.notesConfig.nameField || 'createdByName'\"\n [dateField]=\"config.notesConfig.dateField || 'createdDate'\"\n [commentField]=\"config.notesConfig.commentField || 'details'\">\n </spa-notes>\n </div>\n</div>\n\n", styles: [".title{margin-top:.5em;margin-bottom:.5em;font-size:larger;font-weight:300;color:#0b447e}.composite-field-group{display:flex;flex-direction:row;flex-wrap:wrap;gap:12px}.tin-form-container{display:flex;flex-direction:row;width:100%;gap:16px}.width-100{width:100%!important}.width-75{width:70%!important}.notes-section{width:400px;border-left:1px solid #e0e0e0}\n"] }]
20154
+ args: [{ selector: 'spa-form', standalone: false, template: "\n\n\n<div class=\"tin-form-container\" >\n <div [ngClass]=\"[multiColumn ? 'tin-grid' : 'tin-col', config.notesConfig ? 'width-75' : 'width-100']\" class=\"form-main-content\">\n\n <div *ngIf=\"!hasAccess\" class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n \n <div [ngClass]=\"field.span || field.type =='section' || field.type =='file' || field.type =='file-view' || field.type =='editor' ? 'span-col' : ''\" *ngFor=\"let field of visibleFields\"><!-- TS-11: bind cached visibleFields instead of getVisibleFields() per CD -->\n \n <ng-container>\n \n <ng-container [ngSwitch]=\"field.type\" class=\"highlight\">\n \n <div *ngSwitchCase=\"'section'\" class=\"title d-flex align-items-center\" (click)=\"toggleSection(field)\" style=\"cursor: pointer;\">\n <label style=\"font-size: larger;margin-right: 10px;\">{{field.alias ?? field.name | camelToWords}}</label>\n <mat-icon *ngIf=\"field.infoMessage\" (click)=\"onInfoClick($event, field.infoMessage)\" style=\"color: steelblue; font-size: 14px;\">info</mat-icon>\n <!-- <button mat-icon-button class=\"info-icon-button\" matTooltip=\"Info\" matTooltipPosition=\"above\">\n \n </button> -->\n <mat-icon *ngIf=\"hasSectionFields(field.name)\">{{shouldSectionCollapse(field) ? 'expand_more' : 'expand_less'}}</mat-icon>\n </div>\n \n <ng-container *ngSwitchCase=\"'file'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode !='view'\">\n <spa-attach [message]=\"field.alias ?? 'Drag and Drop files here'\" [(files)]=\"files\" [fileOptions]=\"field.fileOptions\"></spa-attach>\n </div>\n </ng-container>\n \n <ng-container *ngSwitchCase=\"'file-view'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode && config.mode !='create'\">\n <spa-viewer [fileAction]=\"field.loadAction\" [path]=\"field.path\" [folderName]=\"data[field.keyField]\" ></spa-viewer>\n </div>\n </ng-container>\n \n <spa-html *ngSwitchCase=\"'html'\" [display]=\"field.alias | camelToWords\" [value]=\"data[field.name]\" [maxHeight]=\"field.maxHeight\"></spa-html>\n \n <label *ngSwitchCase=\"'blank'\"></label>\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-label *ngSwitchCase=\"'label'\" [display]=\"field.alias ?? field.name | camelToWords\" [value]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [format]=\"field.format ?? 'text'\" [suffix]=\"field.suffix\" [size]=\"field.size\"></spa-label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [infoMessage]=\"field.infoMessage\" [hint]=\"field.hint\" ></spa-check><!-- Changed: field.hint was dropped on the floor for checkboxes only \u2014 every other type already passed it through -->\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [min]=\"field?.min\" [max]=\"field?.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [min]=\"field.min\" [max]=\"field.max\" [infoMessage]=\"field.infoMessage\" ></spa-datetime>\n \n <spa-email *ngSwitchCase=\"'email'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-email>\n \n <spa-text-mask *ngSwitchCase=\"'text-mask'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\"></spa-text-mask>\n \n \n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: field,\n field: field,\n data: data,\n testReadOnly: testReadOnly.bind(this),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n \n <spa-select-multi *ngSwitchCase=\"'select-multi'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [selectAll]=\"field.selectAll\">\n </spa-select-multi>\n \n <spa-text-multi *ngSwitchCase=\"'text-multi'\" [strict]=\"field.strict\" [display]=\"field.alias ?? field.name | camelToWords\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\"></spa-text-multi>\n \n \n \n <ng-container *ngSwitchCase=\"'composite'\">\n <div class=\"composite-field-container\">\n <div class=\"composite-field-group\">\n <ng-container *ngFor=\"let subfield of getVisibleSubfields(field)\">\n <ng-container [ngSwitch]=\"subfield.type\">\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [infoMessage]=\"subfield.infoMessage\" [hint]=\"subfield.hint\" ></spa-check><!-- Changed: same hint pass-through as the top-level checkbox above -->\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [infoMessage]=\"subfield.infoMessage\" ></spa-datetime>\n \n <!-- Fixed: composite select subfields projected the PARENT composite field into the\n dynamic select template (wrong options/value binding) and omitted testRequired,\n so the template's testRequired(field) call threw and killed the whole dialog.\n Context now mirrors the top-level select outlet, with the SUBFIELD, and readonly\n cascades composite parent || subfield like every other subfield type. -->\n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: subfield,\n field: subfield,\n data: data,\n testReadOnly: compositeReadOnly(field),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [options]=\"subfield.options\" [optionDisplay]=\"subfield.optionDisplay ?? 'name'\" [optionValue]=\"subfield.optionValue ?? 'value'\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [loadAction]=\"resolveLoadAction(subfield)\" [regex]=\"subfield.regex\" [field]=\"subfield\" [data]=\"data\" [detailsConfig]=\"subfield.detailsConfig\" [masterField]=\"subfield.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [rows]=\"subfield.rows\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [editorConfig]=\"subfield.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text>\n \n \n </ng-container>\n </ng-container>\n </div>\n </div>\n </ng-container>\n \n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [regex]=\"field.regex\" [field]=\"field\" [data]=\"data\" [detailsConfig]=\"field.detailsConfig\" [masterField]=\"field.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [rows]=\"field.rows\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [editorConfig]=\"field.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text>\n \n </ng-container>\n \n </ng-container>\n \n </div>\n \n \n <div class=\"span-col-center\" *ngIf=\"config.button\">\n <button mat-raised-button color=\"primary\" (click)=\"buttonClicked()\" cdkFocusInitial>{{buttonDisplay}}</button>\n </div>\n \n \n </div>\n <!-- Fixed: built-in fallback select template \u2014 spa-form used consumers' projected #dynamicSelect\n (only detailsDialog provides one), so selects silently rendered NOTHING when spa-form was used\n directly. The projected template (if any) still wins; this default carries the same bindings. -->\n <ng-template #defaultDynamicSelect 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(field)\"\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\n <div class=\"notes-section\" *ngIf=\"config.notesConfig\">\n <spa-notes\n [title]=\"config.notesConfig.title || 'Notes'\"\n [notes]=\"config.notesConfig.notes || []\"\n [loadAction]=\"config.notesConfig.loadAction\"\n [loadIDField]=\"config.notesConfig.loadIDField\"\n [data]=\"data\"\n [nameField]=\"config.notesConfig.nameField || 'createdByName'\"\n [dateField]=\"config.notesConfig.dateField || 'createdDate'\"\n [commentField]=\"config.notesConfig.commentField || 'details'\">\n </spa-notes>\n </div>\n</div>\n\n", styles: [".title{margin-top:.5em;margin-bottom:.5em;font-size:larger;font-weight:300;color:#0b447e}.composite-field-group{display:flex;flex-direction:row;flex-wrap:wrap;gap:12px}.tin-form-container{display:flex;flex-direction:row;width:100%;gap:16px}.width-100{width:100%!important}.width-75{width:70%!important}.notes-section{width:400px;border-left:1px solid #e0e0e0}\n"] }]
19731
20155
  }], ctorParameters: () => [{ type: MessageService }, { type: DataServiceLib }, { type: AuthService }, { type: ApiErrorService }], propDecorators: { dynamicSelectTemplate: [{
19732
20156
  type: ContentChild,
19733
20157
  args: ['dynamicSelect']
@@ -19768,6 +20192,11 @@ class AppConfigurationComponent {
19768
20192
  this.loading = false;
19769
20193
  this.saving = false;
19770
20194
  this.canEdit = true;
20195
+ // Added: the page's own record that the last save was REFUSED. Without it the only evidence was the 5s toast
20196
+ // (a business refusal) or a dialog the user dismisses (a technical failure) — after either one vanished, an
20197
+ // unsaved page looked exactly like a saved one while still displaying the value the server rejected. Held
20198
+ // until a save actually succeeds, NOT cleared on edit: editing does not make the shown values in force.
20199
+ this.saveFailure = null;
19771
20200
  // Materialized, never a template-bound getter — a getter returning a new array here would hand the
19772
20201
  // *ngFor a fresh reference on every change-detection pass (see the day-book/table livelock).
19773
20202
  this.cards = [];
@@ -19847,6 +20276,14 @@ class AppConfigurationComponent {
19847
20276
  onInputChange() {
19848
20277
  this.buildVisibleCards();
19849
20278
  }
20279
+ // Added: the escape hatch for the banner. Reverting the form automatically on a refusal was rejected — it
20280
+ // throws away every OTHER edit on a page that saves as one object, and on a business refusal it also destroys
20281
+ // the values the user needs in front of them to act on the message. Offered as a deliberate press instead, so
20282
+ // "show me what is actually saved" is one click away without being forced on anyone.
20283
+ discardChanges() {
20284
+ this.saveFailure = null;
20285
+ this.load();
20286
+ }
19850
20287
  //---------- Save ----------
19851
20288
  save() {
19852
20289
  if (this.saving || !this.canEdit)
@@ -19856,6 +20293,7 @@ class AppConfigurationComponent {
19856
20293
  next: (response) => {
19857
20294
  this.saving = false;
19858
20295
  if (response.success) {
20296
+ this.saveFailure = null; // Added: the values on screen are now the values in force, so the banner must go
19859
20297
  this.messageService.toast('Configuration updated successfully');
19860
20298
  this.load(); // re-read so computed/server-stamped values (and a first-read created row's key) come back
19861
20299
  this.configService.refresh(); // every app-side subscriber (nav gating, unit suffixes) sees the change now, not next login
@@ -19864,18 +20302,35 @@ class AppConfigurationComponent {
19864
20302
  // Changed: was toast(response.message ...) — the server's raw words for 5 seconds while the page
19865
20303
  // still showed every edited value, so it looked saved. 'submit' is exactly right and literally
19866
20304
  // true here: the user's configuration edits ARE still on the form and do not need re-entering.
19867
- this.apiErrorService.presentAppFailure(response, 'submit', (this.config?.saveAction || { url: 'configuration/update' }).url);
20305
+ const failureClass = this.apiErrorService.presentAppFailure(response, 'submit', (this.config?.saveAction || { url: 'configuration/update' }).url); // Changed: capture the class — it already distinguishes a deliberate refusal from a technical failure, so the banner does not have to re-derive it
20306
+ // Added: this page is a PERSISTENT page, not a dialog. Everywhere else in the library a failed submit
20307
+ // leaves a modal open, and the open modal is itself the visible "not saved" signal. Here there is no
20308
+ // modal: once the toast expires the page looks settled while showing values the server refused and the
20309
+ // database does not hold. The banner is that missing signal, and unlike the toast it does not expire.
20310
+ this.saveFailure = {
20311
+ reason: failureClass === 'business'
20312
+ ? this.apiErrorService.stripReference(response.message) // the server's own sentence names what to fix; stripReference drops the "(reference X)" suffix exactly as the toast does
20313
+ : 'A technical problem stopped the save.',
20314
+ technical: failureClass !== 'business'
20315
+ };
19868
20316
  }
19869
20317
  },
19870
- error: () => this.saving = false
20318
+ // Changed: was `error: () => this.saving = false` — a TRANSPORT failure (server down, DNS, CORS, dropped
20319
+ // connection) never reaches the next() handler above, so this page said nothing at all and simply went
20320
+ // un-busy, which is the most convincing "saved" signal of the lot. The interceptor's own network dialog is
20321
+ // dismissible and page-agnostic; the banner is what keeps the statement attached to the values on screen.
20322
+ error: () => {
20323
+ this.saving = false;
20324
+ this.saveFailure = { reason: 'A technical problem stopped the save.', technical: true };
20325
+ }
19871
20326
  });
19872
20327
  }
19873
20328
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AppConfigurationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
19874
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: AppConfigurationComponent, isStandalone: false, selector: "spa-app-configuration", ngImport: i0, template: "<div class=\"ac-page\">\n\n <div class=\"ac-head\">\n <h1>{{ title }}</h1>\n <span class=\"ac-caption\" *ngIf=\"!canEdit\">Read only \u2014 you do not have permission to change these settings</span>\n <!-- Added (Setup v3): this page used to carry its own module checkboxes, which wrote the same flags as the\n Getting Started picker but skipped its capability check, its \"modules confirmed\" step and its badge\n refresh. One write path now \u2014 the page keeps the settings, the picker owns what is switched on. -->\n <a class=\"ac-modules-link\" (click)=\"goToModules()\"><mat-icon>tune</mat-icon>Modules are chosen in Getting Started</a>\n </div>\n\n <!-- One card per section. The header carries the title, the optional module switch and the collapse chevron -->\n <mat-card class=\"ac-section\" *ngFor=\"let card of visibleCards\" [id]=\"'cfg-' + card.section.key\">\n\n <div class=\"ac-section-head\" [class.clickable]=\"card.section.collapsible !== false\" (click)=\"toggle(card)\">\n <mat-icon class=\"ac-section-icon\" *ngIf=\"card.section.icon\">{{ card.section.icon }}</mat-icon>\n\n <!-- toggleField sections put the module switch IN the header; children below go readonly while it is off -->\n <spa-check *ngIf=\"card.section.toggleField\" class=\"ac-section-switch\" [display]=\"card.section.title\" [(value)]=\"configuration[card.section.toggleField]\" (valueChange)=\"onInputChange()\" [readonly]=\"!canEdit\" (click)=\"$event.stopPropagation()\"></spa-check>\n <span class=\"ac-section-title\" *ngIf=\"!card.section.toggleField\">{{ card.section.title }}</span>\n\n <mat-icon class=\"ac-section-chevron\" *ngIf=\"card.section.collapsible !== false\">{{ card.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </div>\n\n <div class=\"ac-section-hint\" *ngIf=\"card.section.hint && !card.collapsed\">{{ card.section.hint }}</div>\n\n <div class=\"ac-section-body\" *ngIf=\"!card.collapsed\">\n <spa-form [config]=\"card.form\" [data]=\"configuration\" (inputChange)=\"onInputChange()\"></spa-form>\n </div>\n\n </mat-card>\n\n <!-- One Save for the whole page \u2014 the object posts as a unit, so the sections can never drift apart -->\n <div class=\"ac-actions\" *ngIf=\"canEdit && visibleCards.length\">\n <button mat-raised-button color=\"primary\" [disabled]=\"saving\" (click)=\"save()\">Save Configuration</button>\n </div>\n\n</div>\n", styles: [".ac-page{max-width:1100px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.ac-head{display:flex;align-items:baseline;gap:16px;flex-wrap:wrap}.ac-head h1{margin:0;font-size:24px}.ac-caption{font-size:12px;color:#0000008c}.ac-modules-link{display:inline-flex;align-items:center;gap:4px;margin-left:auto;font-size:12px;color:#1976d2;cursor:pointer}.ac-modules-link:hover{text-decoration:underline}.ac-modules-link mat-icon{font-size:16px;width:16px;height:16px}.ac-section{padding:12px 16px}.ac-section-head{display:flex;align-items:center;gap:10px}.ac-section-head.clickable{cursor:pointer}.ac-section-icon{color:#00000073}.ac-section-title{font-size:17px;font-weight:500}.ac-section-switch{flex:0 0 auto}.ac-section-chevron{margin-left:auto;color:#00000073}.ac-section-hint{font-size:12px;color:#0000008c;margin:2px 0 0 2px}.ac-section-body{margin-top:8px}.ac-actions{position:sticky;bottom:0;display:flex;justify-content:flex-end;padding:12px 0;background:linear-gradient(to top,var(--tin-page-bg, #fafafa) 60%,transparent)}\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: 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: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: FormComponent, selector: "spa-form", inputs: ["files", "data", "config"], outputs: ["buttonClick", "inputChange"] }] }); }
20329
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: AppConfigurationComponent, isStandalone: false, selector: "spa-app-configuration", ngImport: i0, template: "<div class=\"ac-page\">\n\n <div class=\"ac-head\">\n <h1>{{ title }}</h1>\n <span class=\"ac-caption\" *ngIf=\"!canEdit\">Read only \u2014 you do not have permission to change these settings</span>\n <!-- Added (Setup v3): this page used to carry its own module checkboxes, which wrote the same flags as the\n Getting Started picker but skipped its capability check, its \"modules confirmed\" step and its badge\n refresh. One write path now \u2014 the page keeps the settings, the picker owns what is switched on. -->\n <a class=\"ac-modules-link\" (click)=\"goToModules()\"><mat-icon>tune</mat-icon>Modules are chosen in Getting Started</a>\n </div>\n\n <!-- One card per section. The header carries the title, the optional module switch and the collapse chevron -->\n <mat-card class=\"ac-section\" *ngFor=\"let card of visibleCards\" [id]=\"'cfg-' + card.section.key\">\n\n <div class=\"ac-section-head\" [class.clickable]=\"card.section.collapsible !== false\" (click)=\"toggle(card)\">\n <mat-icon class=\"ac-section-icon\" *ngIf=\"card.section.icon\">{{ card.section.icon }}</mat-icon>\n\n <!-- toggleField sections put the module switch IN the header; children below go readonly while it is off -->\n <spa-check *ngIf=\"card.section.toggleField\" class=\"ac-section-switch\" [display]=\"card.section.title\" [(value)]=\"configuration[card.section.toggleField]\" (valueChange)=\"onInputChange()\" [readonly]=\"!canEdit\" (click)=\"$event.stopPropagation()\"></spa-check>\n <span class=\"ac-section-title\" *ngIf=\"!card.section.toggleField\">{{ card.section.title }}</span>\n\n <mat-icon class=\"ac-section-chevron\" *ngIf=\"card.section.collapsible !== false\">{{ card.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </div>\n\n <div class=\"ac-section-hint\" *ngIf=\"card.section.hint && !card.collapsed\">{{ card.section.hint }}</div>\n\n <div class=\"ac-section-body\" *ngIf=\"!card.collapsed\">\n <spa-form [config]=\"card.form\" [data]=\"configuration\" (inputChange)=\"onInputChange()\"></spa-form>\n </div>\n\n </mat-card>\n\n <!-- One Save for the whole page \u2014 the object posts as a unit, so the sections can never drift apart.\n The sticky footer now also carries the refusal banner, so the \"not saved\" statement is on screen at every\n scroll position rather than only for the 5 seconds the toast lives. -->\n <div class=\"ac-footer\" *ngIf=\"canEdit && visibleCards.length\">\n\n <!-- Added: persists until a save succeeds. role=alert so it is announced, not just drawn -->\n <div class=\"ac-save-alert\" role=\"alert\" *ngIf=\"saveFailure\">\n <mat-icon class=\"ac-save-alert-icon\">report_problem</mat-icon>\n <div class=\"ac-save-alert-text\">\n <strong>Not saved{{ saveFailure.technical ? '' : ' \u2014 ' + saveFailure.reason }}</strong>\n <span *ngIf=\"saveFailure.technical\">{{ saveFailure.reason }}</span>\n <span>The settings shown on this page are <b>not in force</b>. Your saved configuration is unchanged.</span>\n </div>\n <button mat-stroked-button class=\"ac-save-alert-discard\" (click)=\"discardChanges()\">Discard my changes</button>\n </div>\n\n <div class=\"ac-actions\">\n <button mat-raised-button color=\"primary\" [disabled]=\"saving\" (click)=\"save()\">Save Configuration</button>\n </div>\n\n </div>\n\n</div>\n", styles: [".ac-page{max-width:1100px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.ac-head{display:flex;align-items:baseline;gap:16px;flex-wrap:wrap}.ac-head h1{margin:0;font-size:24px}.ac-caption{font-size:12px;color:#0000008c}.ac-modules-link{display:inline-flex;align-items:center;gap:4px;margin-left:auto;font-size:12px;color:#1976d2;cursor:pointer}.ac-modules-link:hover{text-decoration:underline}.ac-modules-link mat-icon{font-size:16px;width:16px;height:16px}.ac-section{padding:12px 16px}.ac-section-head{display:flex;align-items:center;gap:10px}.ac-section-head.clickable{cursor:pointer}.ac-section-icon{color:#00000073}.ac-section-title{font-size:17px;font-weight:500}.ac-section-switch{flex:0 0 auto}.ac-section-chevron{margin-left:auto;color:#00000073}.ac-section-hint{font-size:12px;color:#0000008c;margin:2px 0 0 2px}.ac-section-body{margin-top:8px}.ac-footer{position:sticky;bottom:0;background:linear-gradient(to top,var(--tin-page-bg, #fafafa) 78%,transparent)}.ac-actions{display:flex;justify-content:flex-end;padding:12px 0}.ac-save-alert{display:flex;align-items:flex-start;gap:12px;padding:12px 14px;margin-top:12px;border:1px solid #f1a9a0;border-left:4px solid #c62828;border-radius:4px;background:#fdecea}.ac-save-alert-icon{flex:0 0 auto;color:#c62828}.ac-save-alert-text{display:flex;flex-direction:column;gap:4px;font-size:13px;line-height:1.45;color:#611a15;min-width:0}.ac-save-alert-text strong{font-size:14px}.ac-save-alert-discard{flex:0 0 auto;margin-left:auto;align-self:center}@media (max-width: 599px){.ac-save-alert{flex-wrap:wrap}.ac-save-alert-discard{margin-left:0;width:100%}}\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: 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: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage", "hint"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: FormComponent, selector: "spa-form", inputs: ["files", "data", "config"], outputs: ["buttonClick", "inputChange"] }] }); }
19875
20330
  }
19876
20331
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AppConfigurationComponent, decorators: [{
19877
20332
  type: Component,
19878
- args: [{ selector: 'spa-app-configuration', standalone: false, template: "<div class=\"ac-page\">\n\n <div class=\"ac-head\">\n <h1>{{ title }}</h1>\n <span class=\"ac-caption\" *ngIf=\"!canEdit\">Read only \u2014 you do not have permission to change these settings</span>\n <!-- Added (Setup v3): this page used to carry its own module checkboxes, which wrote the same flags as the\n Getting Started picker but skipped its capability check, its \"modules confirmed\" step and its badge\n refresh. One write path now \u2014 the page keeps the settings, the picker owns what is switched on. -->\n <a class=\"ac-modules-link\" (click)=\"goToModules()\"><mat-icon>tune</mat-icon>Modules are chosen in Getting Started</a>\n </div>\n\n <!-- One card per section. The header carries the title, the optional module switch and the collapse chevron -->\n <mat-card class=\"ac-section\" *ngFor=\"let card of visibleCards\" [id]=\"'cfg-' + card.section.key\">\n\n <div class=\"ac-section-head\" [class.clickable]=\"card.section.collapsible !== false\" (click)=\"toggle(card)\">\n <mat-icon class=\"ac-section-icon\" *ngIf=\"card.section.icon\">{{ card.section.icon }}</mat-icon>\n\n <!-- toggleField sections put the module switch IN the header; children below go readonly while it is off -->\n <spa-check *ngIf=\"card.section.toggleField\" class=\"ac-section-switch\" [display]=\"card.section.title\" [(value)]=\"configuration[card.section.toggleField]\" (valueChange)=\"onInputChange()\" [readonly]=\"!canEdit\" (click)=\"$event.stopPropagation()\"></spa-check>\n <span class=\"ac-section-title\" *ngIf=\"!card.section.toggleField\">{{ card.section.title }}</span>\n\n <mat-icon class=\"ac-section-chevron\" *ngIf=\"card.section.collapsible !== false\">{{ card.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </div>\n\n <div class=\"ac-section-hint\" *ngIf=\"card.section.hint && !card.collapsed\">{{ card.section.hint }}</div>\n\n <div class=\"ac-section-body\" *ngIf=\"!card.collapsed\">\n <spa-form [config]=\"card.form\" [data]=\"configuration\" (inputChange)=\"onInputChange()\"></spa-form>\n </div>\n\n </mat-card>\n\n <!-- One Save for the whole page \u2014 the object posts as a unit, so the sections can never drift apart -->\n <div class=\"ac-actions\" *ngIf=\"canEdit && visibleCards.length\">\n <button mat-raised-button color=\"primary\" [disabled]=\"saving\" (click)=\"save()\">Save Configuration</button>\n </div>\n\n</div>\n", styles: [".ac-page{max-width:1100px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.ac-head{display:flex;align-items:baseline;gap:16px;flex-wrap:wrap}.ac-head h1{margin:0;font-size:24px}.ac-caption{font-size:12px;color:#0000008c}.ac-modules-link{display:inline-flex;align-items:center;gap:4px;margin-left:auto;font-size:12px;color:#1976d2;cursor:pointer}.ac-modules-link:hover{text-decoration:underline}.ac-modules-link mat-icon{font-size:16px;width:16px;height:16px}.ac-section{padding:12px 16px}.ac-section-head{display:flex;align-items:center;gap:10px}.ac-section-head.clickable{cursor:pointer}.ac-section-icon{color:#00000073}.ac-section-title{font-size:17px;font-weight:500}.ac-section-switch{flex:0 0 auto}.ac-section-chevron{margin-left:auto;color:#00000073}.ac-section-hint{font-size:12px;color:#0000008c;margin:2px 0 0 2px}.ac-section-body{margin-top:8px}.ac-actions{position:sticky;bottom:0;display:flex;justify-content:flex-end;padding:12px 0;background:linear-gradient(to top,var(--tin-page-bg, #fafafa) 60%,transparent)}\n"] }]
20333
+ args: [{ selector: 'spa-app-configuration', standalone: false, template: "<div class=\"ac-page\">\n\n <div class=\"ac-head\">\n <h1>{{ title }}</h1>\n <span class=\"ac-caption\" *ngIf=\"!canEdit\">Read only \u2014 you do not have permission to change these settings</span>\n <!-- Added (Setup v3): this page used to carry its own module checkboxes, which wrote the same flags as the\n Getting Started picker but skipped its capability check, its \"modules confirmed\" step and its badge\n refresh. One write path now \u2014 the page keeps the settings, the picker owns what is switched on. -->\n <a class=\"ac-modules-link\" (click)=\"goToModules()\"><mat-icon>tune</mat-icon>Modules are chosen in Getting Started</a>\n </div>\n\n <!-- One card per section. The header carries the title, the optional module switch and the collapse chevron -->\n <mat-card class=\"ac-section\" *ngFor=\"let card of visibleCards\" [id]=\"'cfg-' + card.section.key\">\n\n <div class=\"ac-section-head\" [class.clickable]=\"card.section.collapsible !== false\" (click)=\"toggle(card)\">\n <mat-icon class=\"ac-section-icon\" *ngIf=\"card.section.icon\">{{ card.section.icon }}</mat-icon>\n\n <!-- toggleField sections put the module switch IN the header; children below go readonly while it is off -->\n <spa-check *ngIf=\"card.section.toggleField\" class=\"ac-section-switch\" [display]=\"card.section.title\" [(value)]=\"configuration[card.section.toggleField]\" (valueChange)=\"onInputChange()\" [readonly]=\"!canEdit\" (click)=\"$event.stopPropagation()\"></spa-check>\n <span class=\"ac-section-title\" *ngIf=\"!card.section.toggleField\">{{ card.section.title }}</span>\n\n <mat-icon class=\"ac-section-chevron\" *ngIf=\"card.section.collapsible !== false\">{{ card.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </div>\n\n <div class=\"ac-section-hint\" *ngIf=\"card.section.hint && !card.collapsed\">{{ card.section.hint }}</div>\n\n <div class=\"ac-section-body\" *ngIf=\"!card.collapsed\">\n <spa-form [config]=\"card.form\" [data]=\"configuration\" (inputChange)=\"onInputChange()\"></spa-form>\n </div>\n\n </mat-card>\n\n <!-- One Save for the whole page \u2014 the object posts as a unit, so the sections can never drift apart.\n The sticky footer now also carries the refusal banner, so the \"not saved\" statement is on screen at every\n scroll position rather than only for the 5 seconds the toast lives. -->\n <div class=\"ac-footer\" *ngIf=\"canEdit && visibleCards.length\">\n\n <!-- Added: persists until a save succeeds. role=alert so it is announced, not just drawn -->\n <div class=\"ac-save-alert\" role=\"alert\" *ngIf=\"saveFailure\">\n <mat-icon class=\"ac-save-alert-icon\">report_problem</mat-icon>\n <div class=\"ac-save-alert-text\">\n <strong>Not saved{{ saveFailure.technical ? '' : ' \u2014 ' + saveFailure.reason }}</strong>\n <span *ngIf=\"saveFailure.technical\">{{ saveFailure.reason }}</span>\n <span>The settings shown on this page are <b>not in force</b>. Your saved configuration is unchanged.</span>\n </div>\n <button mat-stroked-button class=\"ac-save-alert-discard\" (click)=\"discardChanges()\">Discard my changes</button>\n </div>\n\n <div class=\"ac-actions\">\n <button mat-raised-button color=\"primary\" [disabled]=\"saving\" (click)=\"save()\">Save Configuration</button>\n </div>\n\n </div>\n\n</div>\n", styles: [".ac-page{max-width:1100px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.ac-head{display:flex;align-items:baseline;gap:16px;flex-wrap:wrap}.ac-head h1{margin:0;font-size:24px}.ac-caption{font-size:12px;color:#0000008c}.ac-modules-link{display:inline-flex;align-items:center;gap:4px;margin-left:auto;font-size:12px;color:#1976d2;cursor:pointer}.ac-modules-link:hover{text-decoration:underline}.ac-modules-link mat-icon{font-size:16px;width:16px;height:16px}.ac-section{padding:12px 16px}.ac-section-head{display:flex;align-items:center;gap:10px}.ac-section-head.clickable{cursor:pointer}.ac-section-icon{color:#00000073}.ac-section-title{font-size:17px;font-weight:500}.ac-section-switch{flex:0 0 auto}.ac-section-chevron{margin-left:auto;color:#00000073}.ac-section-hint{font-size:12px;color:#0000008c;margin:2px 0 0 2px}.ac-section-body{margin-top:8px}.ac-footer{position:sticky;bottom:0;background:linear-gradient(to top,var(--tin-page-bg, #fafafa) 78%,transparent)}.ac-actions{display:flex;justify-content:flex-end;padding:12px 0}.ac-save-alert{display:flex;align-items:flex-start;gap:12px;padding:12px 14px;margin-top:12px;border:1px solid #f1a9a0;border-left:4px solid #c62828;border-radius:4px;background:#fdecea}.ac-save-alert-icon{flex:0 0 auto;color:#c62828}.ac-save-alert-text{display:flex;flex-direction:column;gap:4px;font-size:13px;line-height:1.45;color:#611a15;min-width:0}.ac-save-alert-text strong{font-size:14px}.ac-save-alert-discard{flex:0 0 auto;margin-left:auto;align-self:center}@media (max-width: 599px){.ac-save-alert{flex-wrap:wrap}.ac-save-alert-discard{margin-left:0;width:100%}}\n"] }]
19879
20334
  }] });
19880
20335
 
19881
20336
  // Renders the agent's replies as the markdown they are actually written in.
@@ -21802,11 +22257,11 @@ class NavMenuComponent {
21802
22257
  return !this.isMiniSidebar || this.isMiniHovered;
21803
22258
  }
21804
22259
  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 }); }
21805
- 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" }] }); }
22260
+ 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}}@media (max-width: 700px){.sm-content,.container-fluid.tin-bg-image,mat-sidenav-content.tin-bg-image{padding-left:10px!important;padding-right:10px!important}}\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" }] }); }
21806
22261
  }
21807
22262
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: NavMenuComponent, decorators: [{
21808
22263
  type: Component,
21809
- args: [{ selector: 'spa-nav-menu', standalone: false, 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"] }]
22264
+ args: [{ selector: 'spa-nav-menu', standalone: false, 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}}@media (max-width: 700px){.sm-content,.container-fluid.tin-bg-image,mat-sidenav-content.tin-bg-image{padding-left:10px!important;padding-right:10px!important}}\n"] }]
21810
22265
  }], ctorParameters: () => [{ type: i1$1.Router }, { type: AuthService }, { type: StorageService }, { type: NotificationsService }, { type: i1$4.BreakpointObserver }, { type: DataServiceLib }, { type: i4.MatDialog }, { type: SubscriptionService }, { type: SetupService }, { type: LastRouteService }, { type: OfflineService }], propDecorators: { onWindowScroll: [{
21811
22266
  type: HostListener,
21812
22267
  args: ['window:scroll']
@@ -22761,6 +23216,8 @@ class DetailsDialog {
22761
23216
  openNestedDetailsDialog(button) {
22762
23217
  // Changed: Use token pattern instead of direct DetailsDialog import, propagate nestingLevel
22763
23218
  this.dialogService.openDefaultDetailsDialog(button, this.details, this.nestingLevel).subscribe(result => {
23219
+ if (!result)
23220
+ return; // Added: same dismissal guard as table.component — Escape/backdrop closes a nested dialog with undefined, and a dismissal must not set actionPerformed or refresh
22764
23221
  if (result.action === 'inputChange') {
22765
23222
  this.inputChanged(result.change);
22766
23223
  }
@@ -22970,11 +23427,11 @@ class DetailsDialog {
22970
23427
  }
22971
23428
  }
22972
23429
  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 }); }
22973
- 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" }] }); }
23430
+ 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}.tin-filter-empty{text-align:center;padding:24px 12px}.tin-filter-empty-title{margin:0}.tin-filter-empty-hint{margin:6px 0 0;font-size:.85em;opacity:.7;overflow-wrap:anywhere}\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" }] }); }
22974
23431
  }
22975
23432
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DetailsDialog, decorators: [{
22976
23433
  type: Component,
22977
- 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"] }]
23434
+ 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}.tin-filter-empty{text-align:center;padding:24px 12px}.tin-filter-empty-title{margin:0}.tin-filter-empty-hint{margin:6px 0 0;font-size:.85em;opacity:.7;overflow-wrap:anywhere}\n"] }]
22978
23435
  }], ctorParameters: () => [{ type: i1$4.BreakpointObserver }, { type: LoaderService }, { type: DataServiceLib }, { type: MessageService }, { type: i4.MatDialogRef }, { type: DetailsDialogConfig, decorators: [{
22979
23436
  type: Inject,
22980
23437
  args: [MAT_DIALOG_DATA]
@@ -23589,7 +24046,7 @@ class TitleActionsComponent {
23589
24046
  this.actionValues[action.name] = value;
23590
24047
  }
23591
24048
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TitleActionsComponent, deps: [{ token: DataServiceLib }, { token: MessageService }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
23592
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TitleActionsComponent, isStandalone: false, selector: "spa-title-actions", inputs: { titleActions: "titleActions" }, outputs: { actionChange: "actionChange" }, ngImport: i0, template: "<div class=\"title-actions-container\" *ngIf=\"titleActions && titleActions.length > 0\">\n \n <ng-container *ngFor=\"let action of titleActions\">\n \n <!-- Date Field -->\n <spa-date \n *ngIf=\"action.type === 'date' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [width]=\"action.width\"\n [(value)]=\"actionValues[action.name]\"\n [required]=\"action.required\"\n [readonly]=\"isReadonly(action)\"\n [hint]=\"action.hint\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-date>\n\n <!-- DateTime Field -->\n <spa-datetime \n *ngIf=\"action.type === 'datetime' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [width]=\"action.width\"\n [(value)]=\"actionValues[action.name]\"\n [readonly]=\"isReadonly(action)\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-datetime>\n\n <!-- Select Field -->\n <spa-select \n *ngIf=\"action.type === 'select' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [width]=\"action.width\"\n [nullable]=\"action.nullable\"\n [options]=\"action.options\"\n [optionDisplay]=\"getOptionDisplay(action)\"\n [optionValue]=\"getOptionValue(action)\"\n [(value)]=\"actionValues[action.name]\"\n [defaultFirstValue]=\"action.defaultFirstValue\"\n [required]=\"action.required\"\n [readonly]=\"isReadonly(action)\"\n [hint]=\"action.hint\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-select>\n\n <!-- Multi-Select Field -->\n <spa-select-multi \n *ngIf=\"action.type === 'select-multi' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [width]=\"action.width\"\n [options]=\"action.options\"\n [optionDisplay]=\"getOptionDisplay(action)\"\n [optionValue]=\"getOptionValue(action)\"\n [(value)]=\"actionValues[action.name]\"\n [required]=\"action.required\"\n [readonly]=\"isReadonly(action)\"\n [hint]=\"action.hint\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-select-multi>\n\n <!-- Checkbox Field -->\n <spa-check \n *ngIf=\"action.type === 'checkbox' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [(value)]=\"actionValues[action.name]\"\n [readonly]=\"isReadonly(action)\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-check>\n\n <!-- Button Field -->\n <button \n *ngIf=\"action.type === 'button' && !isHidden(action)\"\n mat-icon-button\n [disabled]=\"isReadonly(action)\"\n [matTooltip]=\"action.hint ?? (action.alias ?? action.name | camelToWords)\"\n matTooltipPosition=\"above\"\n [color]=\"action.color ?? 'primary'\"\n (click)=\"onButtonClick(action)\">\n <mat-icon *ngIf=\"action.icon\">{{action.icon.name}}</mat-icon>\n <span *ngIf=\"!action.icon\">{{action.alias ?? action.name | camelToWords}}</span>\n </button>\n\n </ng-container>\n\n</div>\n", styles: [".title-actions-container{display:flex;align-items:flex-start;gap:15px;flex-wrap:wrap}.title-actions-container spa-date,.title-actions-container spa-datetime,.title-actions-container spa-select,.title-actions-container spa-multi-select{min-width:150px}.title-actions-container button{margin:0}@media (max-width: 600px){.title-actions-container{gap:10px}.title-actions-container spa-date,.title-actions-container spa-datetime,.title-actions-container spa-select,.title-actions-container spa-multi-select{min-width:120px}}\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: 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: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: DateComponent, selector: "spa-date", inputs: ["required", "min", "max", "readonly", "hint", "value", "display", "placeholder", "width", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: DatetimeComponent, selector: "spa-datetime", inputs: ["display", "value", "readonly", "width", "min", "max", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: SelectMultiComponent, selector: "spa-select-multi", inputs: ["display", "value", "readonly", "required", "hint", "options", "optionDisplay", "optionValue", "infoMessage", "copyContent", "clearContent", "nullable", "placeholder", "width", "suffix", "loadAction", "selectAll"], outputs: ["valueChange", "hoverChange"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
24049
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TitleActionsComponent, isStandalone: false, selector: "spa-title-actions", inputs: { titleActions: "titleActions" }, outputs: { actionChange: "actionChange" }, ngImport: i0, template: "<div class=\"title-actions-container\" *ngIf=\"titleActions && titleActions.length > 0\">\n \n <ng-container *ngFor=\"let action of titleActions\">\n \n <!-- Date Field -->\n <spa-date \n *ngIf=\"action.type === 'date' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [width]=\"action.width\"\n [(value)]=\"actionValues[action.name]\"\n [required]=\"action.required\"\n [readonly]=\"isReadonly(action)\"\n [hint]=\"action.hint\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-date>\n\n <!-- DateTime Field -->\n <spa-datetime \n *ngIf=\"action.type === 'datetime' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [width]=\"action.width\"\n [(value)]=\"actionValues[action.name]\"\n [readonly]=\"isReadonly(action)\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-datetime>\n\n <!-- Select Field -->\n <spa-select \n *ngIf=\"action.type === 'select' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [width]=\"action.width\"\n [nullable]=\"action.nullable\"\n [options]=\"action.options\"\n [optionDisplay]=\"getOptionDisplay(action)\"\n [optionValue]=\"getOptionValue(action)\"\n [(value)]=\"actionValues[action.name]\"\n [defaultFirstValue]=\"action.defaultFirstValue\"\n [required]=\"action.required\"\n [readonly]=\"isReadonly(action)\"\n [hint]=\"action.hint\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-select>\n\n <!-- Multi-Select Field -->\n <spa-select-multi \n *ngIf=\"action.type === 'select-multi' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [width]=\"action.width\"\n [options]=\"action.options\"\n [optionDisplay]=\"getOptionDisplay(action)\"\n [optionValue]=\"getOptionValue(action)\"\n [(value)]=\"actionValues[action.name]\"\n [required]=\"action.required\"\n [readonly]=\"isReadonly(action)\"\n [hint]=\"action.hint\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-select-multi>\n\n <!-- Checkbox Field -->\n <spa-check \n *ngIf=\"action.type === 'checkbox' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [(value)]=\"actionValues[action.name]\"\n [readonly]=\"isReadonly(action)\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-check>\n\n <!-- Button Field -->\n <button \n *ngIf=\"action.type === 'button' && !isHidden(action)\"\n mat-icon-button\n [disabled]=\"isReadonly(action)\"\n [matTooltip]=\"action.hint ?? (action.alias ?? action.name | camelToWords)\"\n matTooltipPosition=\"above\"\n [color]=\"action.color ?? 'primary'\"\n (click)=\"onButtonClick(action)\">\n <mat-icon *ngIf=\"action.icon\">{{action.icon.name}}</mat-icon>\n <span *ngIf=\"!action.icon\">{{action.alias ?? action.name | camelToWords}}</span>\n </button>\n\n </ng-container>\n\n</div>\n", styles: [".title-actions-container{display:flex;align-items:flex-start;gap:15px;flex-wrap:wrap}.title-actions-container spa-date,.title-actions-container spa-datetime,.title-actions-container spa-select,.title-actions-container spa-multi-select{min-width:150px}.title-actions-container button{margin:0}@media (max-width: 600px){.title-actions-container{gap:10px}.title-actions-container spa-date,.title-actions-container spa-datetime,.title-actions-container spa-select,.title-actions-container spa-multi-select{min-width:120px}}\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: 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: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage", "hint"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: DateComponent, selector: "spa-date", inputs: ["required", "min", "max", "readonly", "hint", "value", "display", "placeholder", "width", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: DatetimeComponent, selector: "spa-datetime", inputs: ["display", "value", "readonly", "width", "min", "max", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: SelectMultiComponent, selector: "spa-select-multi", inputs: ["display", "value", "readonly", "required", "hint", "options", "optionDisplay", "optionValue", "infoMessage", "copyContent", "clearContent", "nullable", "placeholder", "width", "suffix", "loadAction", "selectAll"], outputs: ["valueChange", "hoverChange"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
23593
24050
  }
23594
24051
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TitleActionsComponent, decorators: [{
23595
24052
  type: Component,
@@ -23669,7 +24126,7 @@ class PageComponent {
23669
24126
  this.dataLoad.emit(x);
23670
24127
  }
23671
24128
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PageComponent, deps: [{ token: DataServiceLib }, { token: MessageService }], target: i0.ɵɵFactoryTarget.Component }); }
23672
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PageComponent, isStandalone: false, selector: "spa-page", inputs: { config: "config" }, outputs: { searchModeActivated: "searchModeActivated", searchModeDeactivated: "searchModeDeactivated", refreshClick: "refreshClick", actionClick: "actionClick", actionResponse: "actionResponse", inputChange: "inputChange", createClick: "createClick", searchClick: "searchClick", dataLoad: "dataLoad", titleActionChange: "titleActionChange" }, ngImport: i0, template: "<div class=\"row\">\n\n <div class=\"col-auto\">\n <h4>{{config.title ?? 'Untitled'}} </h4>\n </div>\n\n <div class=\"col d-flex justify-content-end align-items-center\" style=\"font-size: 14px; gap: 15px;\">\n <!-- Added: Title actions component -->\n <spa-title-actions \n *ngIf=\"config.titleActions\" \n [titleActions]=\"config.titleActions\"\n (actionChange)=\"titleActionChanged($event)\">\n </spa-title-actions>\n \n <spa-check *ngIf=\"config.searchTableConfig\" [(value)]=\"searchMode\" display=\"Search Mode\" (valueChange)=\"toggleSearch()\" style=\"margin-right: 10px;\"></spa-check>\n </div>\n\n</div>\n\n<hr style=\"margin-top: 0px;\" />\n\n\n<div style=\" font-size: 14px;\">\n <!-- Normal -->\n <spa-table *ngIf=\"!searchMode\" [config]=\"normalTableConfig\" [reload]=\"tableReload\"\n (refreshClick)=\"refreshClicked()\" (actionClick)=\"actionClicked($event)\" (actionResponse)=\"actionResponded($event)\"\n (inputChange)=\"inputChanged($event)\" (createClick)=\"createClicked($event)\" (searchClick)=\"searchClicked($event)\" (dataLoad)=\"dataLoaded($event)\">\n </spa-table>\n\n <!-- Search -->\n <spa-table *ngIf=\"searchMode\" [config]=\"searchTableConfig\" [reload]=\"tableReload\"\n (refreshClick)=\"refreshClicked()\" (actionClick)=\"actionClicked($event)\" (actionResponse)=\"actionResponded($event)\"\n (inputChange)=\"inputChanged($event)\" (createClick)=\"createClicked($event)\" (searchClick)=\"searchClicked($event)\" (dataLoad)=\"dataLoaded($event)\">\n </spa-table>\n</div>\n\n", styles: [".mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}\n"], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { 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: "component", type: TitleActionsComponent, selector: "spa-title-actions", inputs: ["titleActions"], outputs: ["actionChange"] }] }); }
24129
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PageComponent, isStandalone: false, selector: "spa-page", inputs: { config: "config" }, outputs: { searchModeActivated: "searchModeActivated", searchModeDeactivated: "searchModeDeactivated", refreshClick: "refreshClick", actionClick: "actionClick", actionResponse: "actionResponse", inputChange: "inputChange", createClick: "createClick", searchClick: "searchClick", dataLoad: "dataLoad", titleActionChange: "titleActionChange" }, ngImport: i0, template: "<div class=\"row\">\n\n <div class=\"col-auto\">\n <h4>{{config.title ?? 'Untitled'}} </h4>\n </div>\n\n <div class=\"col d-flex justify-content-end align-items-center\" style=\"font-size: 14px; gap: 15px;\">\n <!-- Added: Title actions component -->\n <spa-title-actions \n *ngIf=\"config.titleActions\" \n [titleActions]=\"config.titleActions\"\n (actionChange)=\"titleActionChanged($event)\">\n </spa-title-actions>\n \n <spa-check *ngIf=\"config.searchTableConfig\" [(value)]=\"searchMode\" display=\"Search Mode\" (valueChange)=\"toggleSearch()\" style=\"margin-right: 10px;\"></spa-check>\n </div>\n\n</div>\n\n<hr style=\"margin-top: 0px;\" />\n\n\n<div style=\" font-size: 14px;\">\n <!-- Normal -->\n <spa-table *ngIf=\"!searchMode\" [config]=\"normalTableConfig\" [reload]=\"tableReload\"\n (refreshClick)=\"refreshClicked()\" (actionClick)=\"actionClicked($event)\" (actionResponse)=\"actionResponded($event)\"\n (inputChange)=\"inputChanged($event)\" (createClick)=\"createClicked($event)\" (searchClick)=\"searchClicked($event)\" (dataLoad)=\"dataLoaded($event)\">\n </spa-table>\n\n <!-- Search -->\n <spa-table *ngIf=\"searchMode\" [config]=\"searchTableConfig\" [reload]=\"tableReload\"\n (refreshClick)=\"refreshClicked()\" (actionClick)=\"actionClicked($event)\" (actionResponse)=\"actionResponded($event)\"\n (inputChange)=\"inputChanged($event)\" (createClick)=\"createClicked($event)\" (searchClick)=\"searchClicked($event)\" (dataLoad)=\"dataLoaded($event)\">\n </spa-table>\n</div>\n\n", styles: [".mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}\n"], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage", "hint"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { 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: "component", type: TitleActionsComponent, selector: "spa-title-actions", inputs: ["titleActions"], outputs: ["actionChange"] }] }); }
23673
24130
  }
23674
24131
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PageComponent, decorators: [{
23675
24132
  type: Component,
@@ -24368,7 +24825,7 @@ class TinSpaModule {
24368
24825
  AttachComponent, ChipsComponent, LoaderComponent, NavMenuComponent, TableComponent, DetailsDialog, FormComponent, FormSkeletonComponent, CamelToWordsPipe, AgentMarkdownPipe, NumberComponent, SearchComponent, ViewerComponent, viewerDialog,
24369
24826
  ListDialogComponent,
24370
24827
  InvitationsTableComponent,
24371
- TableHeaderComponent, TableRowComponent, TableActionComponent, InlineCellComponent, // Changed: added InlineCellComponent (inline row editing)
24828
+ TableHeaderComponent, TableRowComponent, TableActionComponent, InlineCellComponent, MonogramComponent, // Changed: added InlineCellComponent (inline row editing), MonogramComponent (monogram column type)
24372
24829
  AlertComponent, EmailComponent, PageComponent, SelectCommonComponent, SuffixComponent, SelectContextDirective, SelectLiteComponent,
24373
24830
  TextMultiComponent,
24374
24831
  SelectMultiComponent, SelectBitwiseComponent,
@@ -24392,6 +24849,7 @@ class TinSpaModule {
24392
24849
  DragDropModule // Changed: Added for CDK drag-drop between groups
24393
24850
  ], exports: [TinSpaComponent,
24394
24851
  SpaMatModule,
24852
+ MonogramComponent, // Added: monogram column type — exported so hosts can render a person outside a table too
24395
24853
  AlertComponent,
24396
24854
  TextComponent,
24397
24855
  TextMaskComponent,
@@ -24473,7 +24931,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
24473
24931
  AttachComponent, ChipsComponent, LoaderComponent, NavMenuComponent, TableComponent, DetailsDialog, FormComponent, FormSkeletonComponent, CamelToWordsPipe, AgentMarkdownPipe, NumberComponent, SearchComponent, ViewerComponent, viewerDialog,
24474
24932
  ListDialogComponent,
24475
24933
  InvitationsTableComponent,
24476
- TableHeaderComponent, TableRowComponent, TableActionComponent, InlineCellComponent, // Changed: added InlineCellComponent (inline row editing)
24934
+ TableHeaderComponent, TableRowComponent, TableActionComponent, InlineCellComponent, MonogramComponent, // Changed: added InlineCellComponent (inline row editing), MonogramComponent (monogram column type)
24477
24935
  AlertComponent, EmailComponent, PageComponent, SelectCommonComponent, SuffixComponent, SelectContextDirective, SelectLiteComponent,
24478
24936
  TextMultiComponent,
24479
24937
  SelectMultiComponent, SelectBitwiseComponent,
@@ -24501,6 +24959,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
24501
24959
  exports: [
24502
24960
  TinSpaComponent,
24503
24961
  SpaMatModule,
24962
+ MonogramComponent, // Added: monogram column type — exported so hosts can render a person outside a table too
24504
24963
  AlertComponent,
24505
24964
  TextComponent,
24506
24965
  TextMaskComponent,
@@ -25653,7 +26112,7 @@ class RolesComponent {
25653
26112
  const newRole = { roleName: '' };
25654
26113
  // Open add role dialog using the same approach as rename
25655
26114
  const dialogRef = this.dialogService.openConfiguredDetailsDialog(this.addRoleButton, newRole, DetailsDialog).subscribe(result => {
25656
- if (result.message === 'success') {
26115
+ if (result?.message === 'success') { // Changed: a dismissed role dialog closes with undefined — read through it rather than throwing
25657
26116
  this.loadRoles();
25658
26117
  }
25659
26118
  });
@@ -25694,7 +26153,7 @@ class RolesComponent {
25694
26153
  const roleData = { ...role };
25695
26154
  // Open rename dialog
25696
26155
  const dialogRef = this.dialogService.openConfiguredDetailsDialog(this.renameButton, roleData, DetailsDialog).subscribe(result => {
25697
- if (result.message === 'success') {
26156
+ if (result?.message === 'success') { // Changed: a dismissed role dialog closes with undefined — read through it rather than throwing
25698
26157
  this.loadRoles();
25699
26158
  }
25700
26159
  });
@@ -25795,7 +26254,7 @@ class CreateAccountComponent {
25795
26254
  this.router.navigate(["home/user/profile"]);
25796
26255
  }
25797
26256
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CreateAccountComponent, deps: [{ token: HttpService }, { token: MessageService }, { token: DataServiceLib }, { token: AuthService }, { token: i1$1.Router }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
25798
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: CreateAccountComponent, isStandalone: false, selector: "spa-create-account", inputs: { appConfig: "appConfig" }, ngImport: i0, template: "<h4>Create User</h4>\n\n<hr/>\n\n<div class=\"container tin-grid\" style=\"font-size:14px; max-width: 70%;\">\n\n <spa-text id=\"txtUserName\" display=\"Username\" [(value)]=\"register.userName\"></spa-text>\n\n &nbsp;\n\n <spa-select id=\"cboAuth\" display=\"Authentication Type\" [options]=\"authTypes\" optionDisplay=\"name\" optionValue=\"value\" [(value)]=\"register.authType\" (valueChange)=\"check()\" ></spa-select>\n\n &nbsp;\n\n <spa-text id=\"txtFirstName\" display=\"FirstName\" [(value)]=\"register.firstName\" [readonly]=\"register.authType =='AD'\"></spa-text>\n\n <spa-text id=\"txtLastName\" display=\"LastName\" [(value)]=\"register.lastName\" [readonly]=\"register.authType =='AD'\"></spa-text>\n\n\n <spa-text-mask *ngIf=\"register.authType == 'local'\" id=\"txtPassword\" display=\"Password\" [(value)]=\"register.password\" ></spa-text-mask>\n\n <spa-text-mask *ngIf=\"register.authType == 'local'\" id=\"txtConfirmPassword\" display=\"Confirm Password\" [(value)]=\"confirmPassword\" ></spa-text-mask>\n\n <spa-text id=\"txtEmail\" display=\"Email\" [(value)]=\"register.email\"></spa-text>\n\n &nbsp;\n\n <spa-select id=\"cboRole\" display=\"Role\" [options]=\"roles\" optionDisplay=\"roleName\" optionValue=\"roleID\" [(value)]=\"register.roleID\"></spa-select>\n\n &nbsp;\n\n <spa-check display=\"Open profile after creation\" [(value)]=\"openProfile\"></spa-check>\n\n <div class=\"span-col-center\">\n <button id=\"btnCreate\" [disabled]=\"register.authType ==''\" mat-raised-button color=\"primary\" (click)=\"create()\" cdkFocusInitial>Create</button>\n </div>\n\n\n\n</div>\n\n\n", styles: [""], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { 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: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextMaskComponent, selector: "spa-text-mask", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "regex", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }] }); }
26257
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: CreateAccountComponent, isStandalone: false, selector: "spa-create-account", inputs: { appConfig: "appConfig" }, ngImport: i0, template: "<h4>Create User</h4>\n\n<hr/>\n\n<div class=\"container tin-grid\" style=\"font-size:14px; max-width: 70%;\">\n\n <spa-text id=\"txtUserName\" display=\"Username\" [(value)]=\"register.userName\"></spa-text>\n\n &nbsp;\n\n <spa-select id=\"cboAuth\" display=\"Authentication Type\" [options]=\"authTypes\" optionDisplay=\"name\" optionValue=\"value\" [(value)]=\"register.authType\" (valueChange)=\"check()\" ></spa-select>\n\n &nbsp;\n\n <spa-text id=\"txtFirstName\" display=\"FirstName\" [(value)]=\"register.firstName\" [readonly]=\"register.authType =='AD'\"></spa-text>\n\n <spa-text id=\"txtLastName\" display=\"LastName\" [(value)]=\"register.lastName\" [readonly]=\"register.authType =='AD'\"></spa-text>\n\n\n <spa-text-mask *ngIf=\"register.authType == 'local'\" id=\"txtPassword\" display=\"Password\" [(value)]=\"register.password\" ></spa-text-mask>\n\n <spa-text-mask *ngIf=\"register.authType == 'local'\" id=\"txtConfirmPassword\" display=\"Confirm Password\" [(value)]=\"confirmPassword\" ></spa-text-mask>\n\n <spa-text id=\"txtEmail\" display=\"Email\" [(value)]=\"register.email\"></spa-text>\n\n &nbsp;\n\n <spa-select id=\"cboRole\" display=\"Role\" [options]=\"roles\" optionDisplay=\"roleName\" optionValue=\"roleID\" [(value)]=\"register.roleID\"></spa-select>\n\n &nbsp;\n\n <spa-check display=\"Open profile after creation\" [(value)]=\"openProfile\"></spa-check>\n\n <div class=\"span-col-center\">\n <button id=\"btnCreate\" [disabled]=\"register.authType ==''\" mat-raised-button color=\"primary\" (click)=\"create()\" cdkFocusInitial>Create</button>\n </div>\n\n\n\n</div>\n\n\n", styles: [""], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { 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: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextMaskComponent, selector: "spa-text-mask", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "regex", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage", "hint"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }] }); }
25799
26258
  }
25800
26259
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CreateAccountComponent, decorators: [{
25801
26260
  type: Component,
@@ -29271,7 +29730,7 @@ class OnboardingComponent {
29271
29730
  this.dataService.Navigate('home');
29272
29731
  }
29273
29732
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: OnboardingComponent, deps: [{ token: DataServiceLib }, { token: AuthService }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
29274
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: OnboardingComponent, isStandalone: false, selector: "spa-onboarding", ngImport: i0, template: "<label class=\"title\">Welcome, {{username}}</label>\n\n\n\n<!-- terms -->\n<div class=\"mt-3\" *ngIf=\"step=='terms'\">\n\n <label class=\"subtitle text-muted mb-2\" >We care about our users and are dedicated to protecting your data and privacy -\n thats why we want to be clear about what data we collect and how we use it to improve your experience.</label>\n\n <br>\n <spa-check display=\"I agree to the Terms and Privacy Policy\" [(value)]=\"agree\"></spa-check>\n</div>\n\n<!-- owner -->\n<div class=\"mt-3\" *ngIf=\"step=='name' && own\">\n\n <label class=\"subtitle text-muted\" style=\" margin-bottom: 20px;\">The follow steps will guide you to customise your application.</label>\n\n <div style=\"max-width: 400px;\">\n <spa-text display=\"Organisation Name\" [(value)]=\"myTenant.name\" ></spa-text>\n </div>\n\n <label class=\"text-muted\" style=\" font-size: 12px;\">You can change the Organisation's name to your team or company name.</label><br>\n <label class=\"text-muted\" style=\" font-size: 12px;margin-top: 10px;\">The name can be changed later.</label>\n\n</div>\n\n<!-- guest -->\n<div *ngIf=\"step=='hi' && !own\">\n <label class=\"subtitle text-muted\">You are now signed in to {{myTenant.name}}.</label>\n</div>\n\n\n<!-- invitations -->\n<div class=\"mt-3\" *ngIf=\"step=='invitations' && own\">\n\n <label class=\"subtitle text-muted\">You have been requested to join the following organisations. If you accept, you have the option to switch to that org now or stay in you org.</label><br>\n <label class=\"text-muted\" style=\" font-size: 12px;margin-top: 10px;\">You will be able to switch later.</label>\n <spa-invitations-table></spa-invitations-table>\n\n</div>\n\n\n<!-- Actions -->\n<div class=\"mt-3\">\n <button mat-stroked-button color=\"primary\" [disabled]=\"!agree\" (click)=\"next()\">Next <mat-icon>arrow_right_alt</mat-icon></button>\n</div>\n", styles: [".title{margin-top:1em;font-size:28px;font-weight:300}\n"], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { 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: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: InvitationsTableComponent, selector: "spa-invitations-table" }] }); }
29733
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: OnboardingComponent, isStandalone: false, selector: "spa-onboarding", ngImport: i0, template: "<label class=\"title\">Welcome, {{username}}</label>\n\n\n\n<!-- terms -->\n<div class=\"mt-3\" *ngIf=\"step=='terms'\">\n\n <label class=\"subtitle text-muted mb-2\" >We care about our users and are dedicated to protecting your data and privacy -\n thats why we want to be clear about what data we collect and how we use it to improve your experience.</label>\n\n <br>\n <spa-check display=\"I agree to the Terms and Privacy Policy\" [(value)]=\"agree\"></spa-check>\n</div>\n\n<!-- owner -->\n<div class=\"mt-3\" *ngIf=\"step=='name' && own\">\n\n <label class=\"subtitle text-muted\" style=\" margin-bottom: 20px;\">The follow steps will guide you to customise your application.</label>\n\n <div style=\"max-width: 400px;\">\n <spa-text display=\"Organisation Name\" [(value)]=\"myTenant.name\" ></spa-text>\n </div>\n\n <label class=\"text-muted\" style=\" font-size: 12px;\">You can change the Organisation's name to your team or company name.</label><br>\n <label class=\"text-muted\" style=\" font-size: 12px;margin-top: 10px;\">The name can be changed later.</label>\n\n</div>\n\n<!-- guest -->\n<div *ngIf=\"step=='hi' && !own\">\n <label class=\"subtitle text-muted\">You are now signed in to {{myTenant.name}}.</label>\n</div>\n\n\n<!-- invitations -->\n<div class=\"mt-3\" *ngIf=\"step=='invitations' && own\">\n\n <label class=\"subtitle text-muted\">You have been requested to join the following organisations. If you accept, you have the option to switch to that org now or stay in you org.</label><br>\n <label class=\"text-muted\" style=\" font-size: 12px;margin-top: 10px;\">You will be able to switch later.</label>\n <spa-invitations-table></spa-invitations-table>\n\n</div>\n\n\n<!-- Actions -->\n<div class=\"mt-3\">\n <button mat-stroked-button color=\"primary\" [disabled]=\"!agree\" (click)=\"next()\">Next <mat-icon>arrow_right_alt</mat-icon></button>\n</div>\n", styles: [".title{margin-top:1em;font-size:28px;font-weight:300}\n"], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { 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: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage", "hint"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: InvitationsTableComponent, selector: "spa-invitations-table" }] }); }
29275
29734
  }
29276
29735
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: OnboardingComponent, decorators: [{
29277
29736
  type: Component,
@@ -33079,5 +33538,5 @@ const ALSQUARE_SVG_WHITE = `<svg viewBox="0 0 80 80" fill="none" xmlns="http://w
33079
33538
  * Generated bundle index. Do not edit.
33080
33539
  */
33081
33540
 
33082
- 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 };
33541
+ 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, MONOGRAM_TONES, ManufacturingModule, MembershipComponent, MessageService, MoneyComponent, MonogramComponent, 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, monogramInitials, monogramKind, monogramPaletteIndex, provideTinSpaRuntime, resolveQuietLoading, silentContext, tinSpaLocationStrategyFactory, tinSpaMsalInstanceFactory, tinSpaRuntimeConfigFactory, viewerDialog };
33083
33542
  //# sourceMappingURL=tin-spa.mjs.map