tin-spa 20.14.26 → 20.14.32
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.
- package/fesm2022/tin-spa.mjs +597 -100
- package/fesm2022/tin-spa.mjs.map +1 -1
- package/index.d.ts +72 -10
- package/package.json +1 -1
package/fesm2022/tin-spa.mjs
CHANGED
|
@@ -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
|
-
|
|
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 = {};
|
|
@@ -364,7 +375,22 @@ class Core {
|
|
|
364
375
|
return false;
|
|
365
376
|
}
|
|
366
377
|
static getInitialValue(field) {
|
|
367
|
-
if (field.defaultValue)
|
|
378
|
+
// Changed (E21): was `if (field.defaultValue)` — a TRUTHINESS test, so a deliberately falsy default
|
|
379
|
+
// (0, false, '') was silently ignored and the field fell through to the type default below. For most
|
|
380
|
+
// types that is harmless, because the fallthrough happens to land on the same value anyway (number -> 0,
|
|
381
|
+
// checkbox -> false, text -> ''). For a SELECT it is not: the fallthrough is `return null`, and a null
|
|
382
|
+
// posted into a non-nullable backend enum is rejected at model binding with an HTTP 400 that the user
|
|
383
|
+
// sees as the generic "Something went wrong ... a technical problem in the app" panel
|
|
384
|
+
// (api-error.service.ts:138-144). Nothing is logged server-side, because a binding failure is not an
|
|
385
|
+
// exception — which is exactly why this stayed hidden through a 124k-line API log.
|
|
386
|
+
//
|
|
387
|
+
// Measured before changing it: across ng-space and all four consumer apps exactly 11 fields declare a
|
|
388
|
+
// falsy defaultValue. Seven are number/money/checkbox and are unaffected, their fallthrough already
|
|
389
|
+
// producing the same value. The other four are selects that asked for 0 and were silently getting null:
|
|
390
|
+
// accounting.service.ts:152 itemType, :574 kind, assets.service.ts:25 defaultDepreciationMethod and
|
|
391
|
+
// :115 disposalType — three of them required, so users were being made to re-pick a value the config had
|
|
392
|
+
// already chosen for them. This is therefore behaviour-neutral everywhere except where it repairs a bug.
|
|
393
|
+
if (field.defaultValue !== undefined && field.defaultValue !== null) {
|
|
368
394
|
if ((field.type == 'date' || field.type == 'datetime') && field.defaultValue == 'now')
|
|
369
395
|
return this.nowDate(true);
|
|
370
396
|
return field.defaultValue;
|
|
@@ -4443,7 +4469,7 @@ class DataServiceLib {
|
|
|
4443
4469
|
]
|
|
4444
4470
|
},
|
|
4445
4471
|
{ name: 'comments', type: 'text', },
|
|
4446
|
-
{ name: 'createdByName', type: '
|
|
4472
|
+
{ name: 'createdByName', type: 'monogram', alias: 'By' }, // Changed: monogram — the row still carries the full name, so filter/sort are untouched
|
|
4447
4473
|
],
|
|
4448
4474
|
buttons: [
|
|
4449
4475
|
{ name: 'view', dialog: true }
|
|
@@ -4551,7 +4577,7 @@ class DataServiceLib {
|
|
|
4551
4577
|
collapseButtons: true,
|
|
4552
4578
|
columns: [
|
|
4553
4579
|
{ name: 'createdDate', type: 'datetime', alias: 'Date' },
|
|
4554
|
-
{ name: 'createdByName', alias: 'Request By', type: '
|
|
4580
|
+
{ name: 'createdByName', alias: 'Request By', type: 'monogram' }, // Changed: monogram
|
|
4555
4581
|
{ name: 'message', type: 'text' },
|
|
4556
4582
|
{
|
|
4557
4583
|
name: 'typeName', alias: 'Type', type: 'chip',
|
|
@@ -4577,7 +4603,7 @@ class DataServiceLib {
|
|
|
4577
4603
|
{ name: 'schedule', color: '#FFC107', condition: x => x.status === 'Pending', tip: 'Pending' }
|
|
4578
4604
|
]
|
|
4579
4605
|
},
|
|
4580
|
-
{ name: 'updatedByName', alias: 'Actioned By', type: '
|
|
4606
|
+
{ name: 'updatedByName', alias: 'Actioned By', type: 'monogram' }, // Changed: monogram
|
|
4581
4607
|
{ name: 'updatedDateDisplay', type: 'datetime', alias: 'Actioned Date' },
|
|
4582
4608
|
],
|
|
4583
4609
|
buttons: [
|
|
@@ -5358,6 +5384,9 @@ class messageDialog {
|
|
|
5358
5384
|
this._messageSubject = this.data.subject;
|
|
5359
5385
|
this._messageDetails = this.data.details;
|
|
5360
5386
|
this._messageReference = this.data.reference; // Added: optional backend correlation code for this failure
|
|
5387
|
+
this._confirmLabel = this.data.confirmLabel; // Added: optional caller-supplied action label for the affirmative button
|
|
5388
|
+
this._cancelLabel = this.data.cancelLabel; // Added: optional caller-supplied label for the dismissive button
|
|
5389
|
+
this._okLabel = this.data.okLabel; // Added: optional caller-supplied label for the acknowledge button
|
|
5361
5390
|
// Added: resolve the style once, at open time. The panel class carries surface-level styling (radius,
|
|
5362
5391
|
// shadow) that component CSS cannot reach, because the Material surface is an ancestor of this view.
|
|
5363
5392
|
this.modern = !!this.dataService?.modernDialogs;
|
|
@@ -5399,6 +5428,12 @@ class messageDialog {
|
|
|
5399
5428
|
return ref.length ? ref : null;
|
|
5400
5429
|
}
|
|
5401
5430
|
get isConfirm() { return this.messageType === 'confirm'; }
|
|
5431
|
+
label(value, fallback) {
|
|
5432
|
+
return (value && String(value).trim()) ? String(value).trim() : fallback;
|
|
5433
|
+
}
|
|
5434
|
+
get confirmLabel() { return this.label(this._confirmLabel, 'Yes'); }
|
|
5435
|
+
get cancelLabel() { return this.label(this._cancelLabel, 'No'); }
|
|
5436
|
+
get okLabel() { return this.label(this._okLabel, 'OK'); }
|
|
5402
5437
|
// Added: heading for the CLASSIC branch. The original template hardcoded one word per type and only rendered
|
|
5403
5438
|
// the caller's subject for 'info'; here the subject still wins when supplied, so the "No connection" fix
|
|
5404
5439
|
// survives in classic mode too — this restores the old LOOK, not the old bug. The per-type words are the
|
|
@@ -5415,11 +5450,11 @@ class messageDialog {
|
|
|
5415
5450
|
this.dialogRef.close(resp);
|
|
5416
5451
|
}
|
|
5417
5452
|
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" }] }); }
|
|
5453
|
+
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
5454
|
}
|
|
5420
5455
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: messageDialog, decorators: [{
|
|
5421
5456
|
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"] }]
|
|
5457
|
+
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
5458
|
}], ctorParameters: () => [{ type: i4.MatDialogRef }, { type: undefined, decorators: [{
|
|
5424
5459
|
type: Inject,
|
|
5425
5460
|
args: [MAT_DIALOG_DATA]
|
|
@@ -5492,13 +5527,20 @@ class MessageService {
|
|
|
5492
5527
|
data: { type, subject, details },
|
|
5493
5528
|
});
|
|
5494
5529
|
}
|
|
5495
|
-
|
|
5530
|
+
// Changed: optional `confirmLabel` / `cancelLabel`, appended last so all 23 existing call sites are unaffected
|
|
5531
|
+
// and keep rendering "Yes" / "No". Supply confirmLabel to name the action being confirmed — confirm('Delete this
|
|
5532
|
+
// shelter?', 'Delete') — so a destructive prompt stops reading identically to a harmless one. Only the caller
|
|
5533
|
+
// knows the intent; the dialog is handed a sentence.
|
|
5534
|
+
// ⚠️ Do NOT change the defaults to make every confirm read better: piglet-spa (38) and shift-spa (13) select
|
|
5535
|
+
// these buttons by their literal text via getByRole('button', { name: 'Yes' }), and the owner reverted a
|
|
5536
|
+
// hardcoded relabel on 2026-08-06. Opt in per call site.
|
|
5537
|
+
confirm(msg, confirmLabel, cancelLabel) {
|
|
5496
5538
|
let type = "confirm";
|
|
5497
5539
|
let subject = "";
|
|
5498
5540
|
let details = msg;
|
|
5499
5541
|
const dialogRef = this.dialog.open(messageDialog, {
|
|
5500
5542
|
width: "400px",
|
|
5501
|
-
data: { type, subject, details },
|
|
5543
|
+
data: { type, subject, details, confirmLabel, cancelLabel },
|
|
5502
5544
|
});
|
|
5503
5545
|
return dialogRef.afterClosed().pipe(mergeMap((result) => {
|
|
5504
5546
|
return of(result);
|
|
@@ -6365,7 +6407,7 @@ class InventoryService {
|
|
|
6365
6407
|
minColumns: ['requisitionNumber', 'requesterName', 'requestDate'],
|
|
6366
6408
|
columns: [
|
|
6367
6409
|
{ name: 'requisitionNumber', type: 'text', alias: 'Requisition #' },
|
|
6368
|
-
{ name: 'requesterName', type: '
|
|
6410
|
+
{ name: 'requesterName', type: 'monogram', alias: 'Requested By' }, // Changed: monogram — the row still carries the full name, so filter/sort are untouched
|
|
6369
6411
|
{ name: 'departmentName', type: 'text', alias: 'Department' },
|
|
6370
6412
|
{ name: 'requestDate', type: 'date', alias: 'Request Date' },
|
|
6371
6413
|
{ name: 'issuedDate', type: 'date', alias: 'Issued Date' },
|
|
@@ -6739,7 +6781,13 @@ class AccountingService {
|
|
|
6739
6781
|
{ name: 'code', type: 'text', alias: 'Account Code', section: 'classification', infoMessage: 'Chart of accounts code (1xxx assets, 2xxx liabilities, 3xxx equity, 4xxx revenue, 5xxx expenses)' }, // Changed: COA code (B1); moved into the collapsed section
|
|
6740
6782
|
{ name: 'includeInCashTotal', type: 'checkbox', alias: 'Include in Cash Total', section: 'classification', hidden: (data) => data.type !== 0 }, // Changed: Only visible for Asset accounts (type 0)
|
|
6741
6783
|
{ name: 'includeInBankTotal', type: 'checkbox', alias: 'Include in Bank Total', section: 'classification', hidden: (data) => data.type !== 0 }, // Changed: Only visible for Asset accounts (type 0)
|
|
6742
|
-
|
|
6784
|
+
// Changed (E21): defaultValue added. An untouched select initialises to null (TinCore.getInitialValue
|
|
6785
|
+
// `case 'select': return null`), and Account.CashFlow is a NON-NULLABLE CashFlowCategory, so creating an
|
|
6786
|
+
// account without opening this collapsed section posted cashFlow:null and was rejected at model binding
|
|
6787
|
+
// with a 400 — surfaced to the user as "Something went wrong ... a technical problem in the app" and
|
|
6788
|
+
// logged nowhere. 0 is Operating, which is the model's own C# default (Account.cs:69), so this makes the
|
|
6789
|
+
// form agree with the entity rather than inventing a value.
|
|
6790
|
+
{ name: 'cashFlow', type: 'select', alias: 'Cash Flow Section', section: 'classification', defaultValue: 0, infoMessage: 'IAS 7 cash flow statement classification', // Changed: C5 cash flow category
|
|
6743
6791
|
options: [
|
|
6744
6792
|
{ name: 'Operating', value: 0 },
|
|
6745
6793
|
{ name: 'Investing', value: 1 },
|
|
@@ -6931,31 +6979,39 @@ class AccountingService {
|
|
|
6931
6979
|
formConfig: this.invoiceItemsFormConfig // Changed: Reference extracted form config
|
|
6932
6980
|
};
|
|
6933
6981
|
// Invoice action buttons
|
|
6934
|
-
|
|
6982
|
+
// Changed: the hand-rolled cap53 gate is now declared, not written. Every custom action is floored at Edit on
|
|
6983
|
+
// the controller's own capability (BaseController.CustomActionAccess), and InvoicesController raises
|
|
6984
|
+
// return/discard/write-off/provision to Full (InvoicesController.cs:540-546). capAccountingInvoices IS cap53,
|
|
6985
|
+
// which is BOTH InvoicesController.CapabilityName and InvoicePaymentsController's — so these read the same
|
|
6986
|
+
// column the server reads. `visible` is back to being about the ROW; the capability is about the ROLE.
|
|
6987
|
+
// Changed: `record-payment` opens a create dialog posting to invoicepayments?action=create, and BaseController
|
|
6988
|
+
// demands RoleAccess.Create for "create" (BaseController.cs:751) — NOT the Edit floor. It was wholly ungated:
|
|
6989
|
+
// a cap53 Edit holder was offered it and refused by the server.
|
|
6990
|
+
this.invoiceRecordPaymentButton = { name: 'record-payment', display: 'Record Payment', dialog: true, icon: { name: 'payment', color: 'blue' }, capability: this.dataService.capAccountingInvoices, requiredAccess: RoleAccess.Create,
|
|
6935
6991
|
detailsConfig: this.invoicePaymentsCreateDetailsDialogConfig,
|
|
6936
6992
|
visible: x => (x.status == InvoiceStatus.Submitted || x.status == InvoiceStatus.Paying) && x.outstandingAmount > 0 // Changed: Allow on both Submitted and Paying
|
|
6937
6993
|
}; // Changed: Use detailsConfig to open payment form dialog
|
|
6938
|
-
this.invoiceDiscardButton = { name: 'discard', inDialog: true, display: 'Discard', icon: { name: 'close', color: 'red' },
|
|
6994
|
+
this.invoiceDiscardButton = { name: 'discard', inDialog: true, display: 'Discard', icon: { name: 'close', color: 'red' }, capability: this.dataService.capAccountingInvoices, requiredAccess: RoleAccess.Full,
|
|
6939
6995
|
action: { url: 'invoices?action=discard', method: 'post', successMessage: 'Discarded' },
|
|
6940
6996
|
confirm: { message: 'Invoice will be marked as cancelled?' },
|
|
6941
|
-
visible: x => x.status == InvoiceStatus.Draft // Changed:
|
|
6997
|
+
visible: x => x.status == InvoiceStatus.Draft // Changed: `discard` is one of the four actions InvoicesController raises to Full — now declared above
|
|
6942
6998
|
};
|
|
6943
6999
|
// 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' },
|
|
7000
|
+
this.invoiceReturnButton = { name: 'return', inDialog: true, display: 'Return', icon: { name: 'undo', color: 'orange' }, capability: this.dataService.capAccountingInvoices, requiredAccess: RoleAccess.Full,
|
|
6945
7001
|
action: { url: 'invoices?action=return', method: 'post', successMessage: 'Returned to Draft' },
|
|
6946
7002
|
confirm: { message: 'Return invoice to draft? This will reverse the receivable and VAT transactions.' },
|
|
6947
|
-
visible: x => x.status == InvoiceStatus.Submitted
|
|
7003
|
+
visible: x => x.status == InvoiceStatus.Submitted // Changed: `return` requires Full at the server — now declared above
|
|
6948
7004
|
};
|
|
6949
7005
|
// 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' },
|
|
7006
|
+
this.invoiceWriteOffButton = { name: 'write-off', inDialog: true, display: 'Write Off', icon: { name: 'money_off', color: 'red' }, capability: this.dataService.capAccountingInvoices, requiredAccess: RoleAccess.Full,
|
|
6951
7007
|
action: { url: 'invoices?action=write-off', method: 'post' },
|
|
6952
7008
|
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
|
|
7009
|
+
visible: x => (x.status == InvoiceStatus.Submitted || x.status == InvoiceStatus.Paying) // Changed: `write-off` requires Full at the server — now declared above
|
|
6954
7010
|
};
|
|
6955
|
-
this.invoiceSubmitButton = { name: 'submit', inDialog: true, display: 'Submit', icon: { name: 'send', },
|
|
7011
|
+
this.invoiceSubmitButton = { name: 'submit', inDialog: true, display: 'Submit', icon: { name: 'send', }, capability: this.dataService.capAccountingInvoices, requiredAccess: RoleAccess.Edit,
|
|
6956
7012
|
action: { url: 'invoices?action=submit', method: 'post', successMessage: 'Submitted' },
|
|
6957
7013
|
confirm: { message: 'Submit invoice? This will record revenue and create an accounts receivable entry.' },
|
|
6958
|
-
visible: x => x.status == InvoiceStatus.Draft,
|
|
7014
|
+
visible: x => x.status == InvoiceStatus.Draft, // Changed: `submit` keeps the Edit floor — the comment on InvoicesController.CustomActionAccess says so explicitly
|
|
6959
7015
|
disabled: x => x.totalAmount == 0
|
|
6960
7016
|
};
|
|
6961
7017
|
this.invoiceEditButton = { name: 'edit', display: 'Save', dialog: true, action: { url: 'invoices?action=edit', method: 'post', },
|
|
@@ -10754,10 +10810,28 @@ class ButtonService {
|
|
|
10754
10810
|
getVisibleButtons(buttons, row, tableConfig) {
|
|
10755
10811
|
return buttons.filter(button => this.testVisible(button, row, tableConfig));
|
|
10756
10812
|
}
|
|
10813
|
+
// Added: the opt-in capability gate, on its own. Mirrors the server's real rule — a NAMED capability at a
|
|
10814
|
+
// NAMED level — because that is what BaseController checks (CustomActionAccess: Edit on the controller's own
|
|
10815
|
+
// CapabilityName). Reads the same role object the configs already read by hand: dataService.myRole is fed
|
|
10816
|
+
// from authService.myRoleObserv (datalib.service.ts:569), which is currentRoleSource.asObservable() — one
|
|
10817
|
+
// source, not a second way to read a role. A button that names no capability returns true immediately, so
|
|
10818
|
+
// every existing button is untouched.
|
|
10819
|
+
hasCapability(button) {
|
|
10820
|
+
if (!button?.capability)
|
|
10821
|
+
return true;
|
|
10822
|
+
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
|
|
10823
|
+
if (!capName)
|
|
10824
|
+
return true; // an unresolved capability must not gate anything — a silent deny is worse than the status quo
|
|
10825
|
+
const level = this.authService.currentRoleSource.value?.[capName] ?? RoleAccess.None;
|
|
10826
|
+
return level >= (button.requiredAccess ?? RoleAccess.Edit); // Edit is BaseController.CustomActionAccess's floor
|
|
10827
|
+
}
|
|
10757
10828
|
testVisible(button, row, tableConfig) {
|
|
10758
10829
|
if (!Core.isItemVisible(button, row)) { // Changed: unified visible/hidden (boolean | condition), hidden wins
|
|
10759
10830
|
return false;
|
|
10760
10831
|
}
|
|
10832
|
+
if (!this.hasCapability(button)) { // Added: opt-in capability gate — no-op unless the config names one
|
|
10833
|
+
return false;
|
|
10834
|
+
}
|
|
10761
10835
|
const currentRole = this.authService.currentRoleSource.value;
|
|
10762
10836
|
// Check if button has detailsConfig with form restrictions
|
|
10763
10837
|
if (button.detailsConfig?.formConfig?.security) {
|
|
@@ -11773,28 +11847,60 @@ class ConfigService {
|
|
|
11773
11847
|
get value() {
|
|
11774
11848
|
return this.config.value;
|
|
11775
11849
|
}
|
|
11850
|
+
static { this.maxFailedAttempts = 3; }
|
|
11776
11851
|
constructor(dataService) {
|
|
11777
11852
|
this.dataService = dataService;
|
|
11778
11853
|
this.config = new BehaviorSubject({});
|
|
11779
11854
|
this.config$ = this.config.asObservable();
|
|
11780
11855
|
this.loaded = false;
|
|
11856
|
+
// Added: one attempt at a time. Two callers racing on login used to be harmless only because the latch below
|
|
11857
|
+
// swallowed the second; with the latch corrected they would both hit the wire, so dedupe explicitly instead.
|
|
11858
|
+
this.inFlight = false;
|
|
11859
|
+
// Added: the bound. Retrying is CALLER-DRIVEN — there is no timer and no resubscribe here, so a retry only
|
|
11860
|
+
// ever happens because something asked again. This cap stops a chatty caller (a component that loads on every
|
|
11861
|
+
// navigation) from turning a genuinely down API into one request per navigation, forever.
|
|
11862
|
+
this.failedAttempts = 0;
|
|
11781
11863
|
}
|
|
11782
11864
|
// Called on login/bootstrap — the backend creates the row on first read, so this never comes back empty
|
|
11783
11865
|
load(force = false) {
|
|
11866
|
+
// Changed: `loaded` was set HERE, before the request, and every failure was swallowed — so the FIRST attempt
|
|
11867
|
+
// won the session outright. shift-spa fired one on the login screen (AuthService seeds currentRoleSource with
|
|
11868
|
+
// a truthy `new Role`, and a BehaviorSubject replays its seed synchronously), it 401'd with an empty bearer
|
|
11869
|
+
// token, and every later authenticated caller returned early forever. The app then ran on an empty config —
|
|
11870
|
+
// every flag false, ~70 call sites — while the database said otherwise. Only a page reload masked it, which
|
|
11871
|
+
// is why it survived: silent-restore replays the 401'd request, a fresh interactive login does not.
|
|
11872
|
+
if (force)
|
|
11873
|
+
this.failedAttempts = 0; // an explicit refresh is a deliberate act (the App Configuration page just saved), so it is allowed past the cap
|
|
11784
11874
|
if (this.loaded && !force)
|
|
11785
11875
|
return;
|
|
11786
|
-
this.
|
|
11876
|
+
if (this.inFlight && !force)
|
|
11877
|
+
return;
|
|
11878
|
+
if (this.failedAttempts >= ConfigService.maxFailedAttempts && !force)
|
|
11879
|
+
return;
|
|
11880
|
+
this.inFlight = true;
|
|
11787
11881
|
this.dataService.CallApi({ url: 'configuration/get', skipCache: true }).subscribe({
|
|
11788
11882
|
next: (apiResponse) => {
|
|
11883
|
+
this.inFlight = false;
|
|
11789
11884
|
// DELIBERATELY SILENT (WS-6 Phase 4 triage). This runs on login/bootstrap, before the user has asked
|
|
11790
|
-
// for anything, and
|
|
11791
|
-
//
|
|
11792
|
-
//
|
|
11793
|
-
//
|
|
11794
|
-
|
|
11885
|
+
// for anything, and greeting someone with an error dialog the instant they sign in, for an app that then
|
|
11886
|
+
// works, is a worse lie than saying nothing. The error callback below absorbs the transport case likewise.
|
|
11887
|
+
// Changed: the old comment claimed everything downstream FAILS OPEN. It does not. Config flags are read as
|
|
11888
|
+
// `!!config.enableX` and the database defaults are TRUE, so an empty config reads FALSE and HIDES things —
|
|
11889
|
+
// fail-CLOSED. Staying silent is still right; latching the failure was not. Retry stays possible instead.
|
|
11890
|
+
if (apiResponse.success) {
|
|
11891
|
+
this.loaded = true; // Changed: only a response that actually carried a config counts as loaded
|
|
11892
|
+
this.failedAttempts = 0;
|
|
11795
11893
|
this.config.next(apiResponse.data || {});
|
|
11894
|
+
}
|
|
11895
|
+
else
|
|
11896
|
+
this.failedAttempts++; // Added: a refusal (401/403 shaped as success:false) is a failure, not a loaded config
|
|
11796
11897
|
},
|
|
11797
|
-
error: () => {
|
|
11898
|
+
error: () => {
|
|
11899
|
+
// an app without a ConfigurationController simply keeps the empty object — and after maxFailedAttempts
|
|
11900
|
+
// stops asking, so the 404 costs three requests a session rather than one, and never more
|
|
11901
|
+
this.inFlight = false;
|
|
11902
|
+
this.failedAttempts++;
|
|
11903
|
+
}
|
|
11798
11904
|
});
|
|
11799
11905
|
}
|
|
11800
11906
|
refresh() {
|
|
@@ -12143,7 +12249,9 @@ class SetupGuideComponent {
|
|
|
12143
12249
|
const act = this.getAction(step);
|
|
12144
12250
|
if (act.button) {
|
|
12145
12251
|
this.dialogService.openDefaultDetailsDialog({ ...act.button }, {}).subscribe(result => {
|
|
12146
|
-
if (result
|
|
12252
|
+
if (!result)
|
|
12253
|
+
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.
|
|
12254
|
+
if (result.action === 'inputChange')
|
|
12147
12255
|
return; // Changed: an input change is not a close — nothing to recount or follow up
|
|
12148
12256
|
this.setupService.refresh(); // Recount after the dialog closes so progress advances immediately
|
|
12149
12257
|
this.openSuccessButton(act.button, result); // Added: complete CLAUDE.md's Two-Step Create Pattern from this page too
|
|
@@ -12428,11 +12536,11 @@ class SetupGuideComponent {
|
|
|
12428
12536
|
});
|
|
12429
12537
|
}
|
|
12430
12538
|
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"] }] }); }
|
|
12539
|
+
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
12540
|
}
|
|
12433
12541
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SetupGuideComponent, decorators: [{
|
|
12434
12542
|
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"] }]
|
|
12543
|
+
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
12544
|
}] });
|
|
12437
12545
|
|
|
12438
12546
|
// Quiet Loading — perceived-progress engine (FSD D1).
|
|
@@ -13342,7 +13450,7 @@ class TextSingleComponent {
|
|
|
13342
13450
|
let button = this.detailsConfig.buttons.find(b => b.name === mode);
|
|
13343
13451
|
button.detailsConfig = this.detailsConfig;
|
|
13344
13452
|
this.dialogService.openDefaultDetailsDialog(button, dynamicData).subscribe(result => {
|
|
13345
|
-
if (result
|
|
13453
|
+
if (result?.message === 'success') { // Changed: a dismissed quick-add dialog closes with undefined — read through it rather than throwing
|
|
13346
13454
|
// Added (dropdown quick-add): auto-select the record the user just created; refresh's initFilter
|
|
13347
13455
|
// then resolves the display text once the new option is in the list
|
|
13348
13456
|
if (mode === 'create')
|
|
@@ -13542,6 +13650,9 @@ class SelectCommonComponent {
|
|
|
13542
13650
|
this.loadIndicator = new FieldLoadIndicator();
|
|
13543
13651
|
// Added (dropdown quick-add): sentinel value for the always-first "add new" option — never a real option value
|
|
13544
13652
|
this.ADD_NEW_OPTION = "__spa_add_new__";
|
|
13653
|
+
// Added: the deferred default must not fire into a torn-down field (a dialog closed while its options were
|
|
13654
|
+
// still settling would otherwise write onto data nobody is showing any more)
|
|
13655
|
+
this.destroyed = false;
|
|
13545
13656
|
this.width = "100%";
|
|
13546
13657
|
this.readonly = false;
|
|
13547
13658
|
this.required = true;
|
|
@@ -13579,6 +13690,24 @@ class SelectCommonComponent {
|
|
|
13579
13690
|
// Added (dropdown quick-add): keep the restore point in sync with parent-driven value changes
|
|
13580
13691
|
if (this.value !== this.ADD_NEW_OPTION)
|
|
13581
13692
|
this.previousValue = this.value;
|
|
13693
|
+
// Added: defaultFirstValue was inert on every select that names a masterField. setDefaultValue() has only
|
|
13694
|
+
// two entry points — ngOnInit (options are still empty then) and the field.optionsSubject subscription —
|
|
13695
|
+
// and form.component.ts only creates an optionsSubject for fields WITHOUT a masterField, so a child field
|
|
13696
|
+
// has no second entry point at all. Its options arrive later, as a plain [options] input mutation from
|
|
13697
|
+
// updateChildOptions(), which lands here in ngOnChanges and nowhere else. Reproduced on shift-spa's driver
|
|
13698
|
+
// create dialog: Department self-selected, Position stayed empty with exactly one option in its list.
|
|
13699
|
+
// Deliberately scoped to masterField selects: every other select already gets its default through one of
|
|
13700
|
+
// the two existing entry points, so nothing else's timing moves. setDefaultValue() still declines when a
|
|
13701
|
+
// value is already set, so this can only fill a blank, never overwrite a choice.
|
|
13702
|
+
// Deferred by one microtask on purpose: setDefaultValue() emits valueChange, and emitting from inside
|
|
13703
|
+
// ngOnChanges writes the parent's data[field.name] after the parent's bindings were already checked, which
|
|
13704
|
+
// is NG0100 — the same shape spa-select-multi's selectAll had. The microtask runs once the pass has
|
|
13705
|
+
// finished, so the new value is picked up by the next one instead of invalidating this one.
|
|
13706
|
+
if (this.masterField && this.options !== this.defaultedOptions) {
|
|
13707
|
+
this.defaultedOptions = this.options;
|
|
13708
|
+
Promise.resolve().then(() => { if (!this.destroyed)
|
|
13709
|
+
this.setDefaultValue(); });
|
|
13710
|
+
}
|
|
13582
13711
|
if (this.readonlyMode != "" && this.options.length > 0) {
|
|
13583
13712
|
this.displayValue = this.options.filter((m) => m[`${this.optionValue}`] == this.value)[0][`${this.optionDisplay}`];
|
|
13584
13713
|
}
|
|
@@ -13749,6 +13878,7 @@ class SelectCommonComponent {
|
|
|
13749
13878
|
});
|
|
13750
13879
|
}
|
|
13751
13880
|
ngOnDestroy() {
|
|
13881
|
+
this.destroyed = true; // Added: cancels a master-driven default still queued on the microtask queue
|
|
13752
13882
|
if (this.subscription) {
|
|
13753
13883
|
this.subscription.unsubscribe();
|
|
13754
13884
|
}
|
|
@@ -13858,8 +13988,21 @@ class TextMultiComponent {
|
|
|
13858
13988
|
this.loadIndicator = new FieldLoadIndicator();
|
|
13859
13989
|
this.hoverChange = new EventEmitter();
|
|
13860
13990
|
} // Changed: injected ApiErrorService so a failed option load is no longer silent
|
|
13991
|
+
// Added: identical normalisation to SelectMultiComponent.toValueList, deliberately kept the same shape and
|
|
13992
|
+
// name so the two multi-value components cannot drift. `value` is documented as a ';'-delimited string, but
|
|
13993
|
+
// the truthiness guard below used to hand anything truthy straight to .split() — an array (any form whose
|
|
13994
|
+
// backend DTO takes a List<int> rewrites the bound property in place before posting) or a bare scalar both
|
|
13995
|
+
// threw "split is not a function". select-multi hit exactly that in piglet; nothing feeds text-multi an array
|
|
13996
|
+
// today, so this is hardening ahead of the first caller that does, not a repair.
|
|
13997
|
+
toValueList(raw) {
|
|
13998
|
+
if (Array.isArray(raw))
|
|
13999
|
+
return raw.filter(v => v !== null && v !== undefined && String(v).trim() !== '').map(v => String(v).trim()); // Added: the List<int> shape
|
|
14000
|
+
if (!raw)
|
|
14001
|
+
return []; // Added: preserves the previous falsy-means-nothing-entered rule for null/undefined/''
|
|
14002
|
+
return String(raw).split(';').filter(v => v.trim() !== '').map(v => v.trim()); // Changed: String() first, so a bare scalar no longer throws either
|
|
14003
|
+
}
|
|
13861
14004
|
ngOnInit() {
|
|
13862
|
-
this.values = this.value
|
|
14005
|
+
this.values = this.toValueList(this.value); // Changed: was this.value.split(';') directly on an @Input of unknown shape
|
|
13863
14006
|
this.setupAutoComplete();
|
|
13864
14007
|
this.getData(this.loadAction);
|
|
13865
14008
|
}
|
|
@@ -14057,6 +14200,7 @@ class SelectMultiComponent {
|
|
|
14057
14200
|
this.selectAll = false; // Changed: auto-select all options when no value is set
|
|
14058
14201
|
this.control = new FormControl([]);
|
|
14059
14202
|
this.selectedValues = [];
|
|
14203
|
+
this.destroyed = false; // Added: a field torn down before the microtask runs must not emit into a dead parent
|
|
14060
14204
|
this.isHovered = false;
|
|
14061
14205
|
// Added: drives the suffix refresh icon — held visible and spinning while the loadAction is in flight
|
|
14062
14206
|
this.loadIndicator = new FieldLoadIndicator();
|
|
@@ -14086,13 +14230,23 @@ class SelectMultiComponent {
|
|
|
14086
14230
|
}
|
|
14087
14231
|
this.initializeValues();
|
|
14088
14232
|
}
|
|
14233
|
+
// Added: `value` is documented as a ';'-delimited string, but real callers legitimately hold an array —
|
|
14234
|
+
// any form whose backend DTO takes a List<int> rewrites the bound property in place before posting
|
|
14235
|
+
// (piglet has four: wean, sell, move, treat). That array flowed straight back down [(value)] and
|
|
14236
|
+
// this.value.split(';') threw "split is not a function" on every subsequent change-detection pass.
|
|
14237
|
+
// Normalise on the way in rather than type-guarding at one call site, and keep emitting the ';' string
|
|
14238
|
+
// on the way out so no existing consumer's parsing changes.
|
|
14239
|
+
toValueList(raw) {
|
|
14240
|
+
if (Array.isArray(raw))
|
|
14241
|
+
return raw.filter(v => v !== null && v !== undefined && String(v).trim() !== '').map(v => String(v).trim()); // Added: the List<int> shape
|
|
14242
|
+
if (!raw)
|
|
14243
|
+
return []; // Added: preserves the previous falsy-means-nothing-selected rule for null/undefined/''
|
|
14244
|
+
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
|
|
14245
|
+
}
|
|
14089
14246
|
initializeValues() {
|
|
14090
|
-
|
|
14091
|
-
|
|
14092
|
-
|
|
14093
|
-
.map(v => this.options.find(opt => opt[this.optionValue]?.toString() === v)
|
|
14094
|
-
? v : null)
|
|
14095
|
-
.filter(v => v !== null);
|
|
14247
|
+
const requested = this.toValueList(this.value); // Changed: was this.value.split(';') directly on an @Input of unknown shape
|
|
14248
|
+
if (requested.length > 0) { // Changed: gate on the parsed request, not on truthiness of a raw value that may be an array
|
|
14249
|
+
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
14250
|
this.selectedValues = values;
|
|
14097
14251
|
this.control.setValue(values);
|
|
14098
14252
|
}
|
|
@@ -14101,13 +14255,24 @@ class SelectMultiComponent {
|
|
|
14101
14255
|
const allValues = this.options.map(opt => opt[this.optionValue]?.toString());
|
|
14102
14256
|
this.selectedValues = allValues;
|
|
14103
14257
|
this.control.setValue(allValues);
|
|
14104
|
-
this.updateValue()
|
|
14258
|
+
this.publishAutoSelection(); // Changed: was this.updateValue() — see publishAutoSelection for why the emit cannot happen here
|
|
14105
14259
|
}
|
|
14106
14260
|
else {
|
|
14107
14261
|
this.selectedValues = [];
|
|
14108
14262
|
this.control.setValue([]);
|
|
14109
14263
|
}
|
|
14110
14264
|
}
|
|
14265
|
+
// Added: initializeValues runs from ngOnChanges/ngOnInit — inside the parent's change-detection pass.
|
|
14266
|
+
// Emitting valueChange there writes the parent's bound property after its bindings were already checked,
|
|
14267
|
+
// which is exactly what NG0100 ExpressionChangedAfterItHasBeenCheckedError reports. It fired on every
|
|
14268
|
+
// selectAll open, independently of the array crash above. Deferring by one microtask moves the parent
|
|
14269
|
+
// write into the next turn. Same idiom as day-book.component.ts's showLoading.
|
|
14270
|
+
// Only the AUTOMATIC selection defers — selectionChange() still emits synchronously, because a user
|
|
14271
|
+
// click is not inside a checked pass and callers rely on the value being current when they read it.
|
|
14272
|
+
publishAutoSelection() {
|
|
14273
|
+
Promise.resolve().then(() => { if (!this.destroyed)
|
|
14274
|
+
this.updateValue(); });
|
|
14275
|
+
}
|
|
14111
14276
|
selectionChange(event) {
|
|
14112
14277
|
this.selectedValues = event.value || [];
|
|
14113
14278
|
this.updateValue();
|
|
@@ -14169,6 +14334,7 @@ class SelectMultiComponent {
|
|
|
14169
14334
|
});
|
|
14170
14335
|
}
|
|
14171
14336
|
ngOnDestroy() {
|
|
14337
|
+
this.destroyed = true; // Added: suppresses a deferred selectAll emit that lands after teardown
|
|
14172
14338
|
this.loadIndicator.destroy(); // Added: clear any pending delay/hold timer on a field torn down mid-read
|
|
14173
14339
|
}
|
|
14174
14340
|
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 +15337,7 @@ class TableHeaderComponent {
|
|
|
15171
15337
|
this.customClick.emit("upload");
|
|
15172
15338
|
this.dialogService.openDefaultDialog(this.uploadDetailsDialog).afterClosed.subscribe(result => {
|
|
15173
15339
|
// console.log(result)
|
|
15174
|
-
if (result
|
|
15340
|
+
if (result?.message == 'success') { // Changed: a dismissed upload dialog closes with undefined — read through it rather than throwing
|
|
15175
15341
|
this.refreshClick.emit();
|
|
15176
15342
|
this.messageService.toast("Upload successful");
|
|
15177
15343
|
}
|
|
@@ -15314,11 +15480,11 @@ class CheckComponent {
|
|
|
15314
15480
|
this.infoClick.emit();
|
|
15315
15481
|
}
|
|
15316
15482
|
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"] }] }); }
|
|
15483
|
+
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
15484
|
}
|
|
15319
15485
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CheckComponent, decorators: [{
|
|
15320
15486
|
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"] }]
|
|
15487
|
+
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
15488
|
}], ctorParameters: () => [{ type: MessageService }], propDecorators: { readonly: [{
|
|
15323
15489
|
type: Input
|
|
15324
15490
|
}], display: [{
|
|
@@ -15337,6 +15503,94 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
15337
15503
|
type: Input
|
|
15338
15504
|
}], infoClick: [{
|
|
15339
15505
|
type: Output
|
|
15506
|
+
}], hint: [{
|
|
15507
|
+
type: Input
|
|
15508
|
+
}] } });
|
|
15509
|
+
|
|
15510
|
+
// TinCore Constants.SystemUser = "System"; its Users row is FirstName "System" / LastName "User",
|
|
15511
|
+
// so a system-stamped row reaches the client as either of these. Kept tight and evidence-based —
|
|
15512
|
+
// speculative tokens ('job', 'seed') are NOT matched because nothing in this codebase produces them.
|
|
15513
|
+
const SYSTEM_TOKENS = ['system', 'system user'];
|
|
15514
|
+
// Ten hues carry people. Slate is NOT in the rotation — it is reserved for "not a person", so a
|
|
15515
|
+
// neutral circle can never be mistaken for someone whose colour merely happens to be grey.
|
|
15516
|
+
const MONOGRAM_TONES = ['indigo', 'blue', 'cyan', 'teal', 'green', 'moss', 'ochre', 'clay', 'rose', 'plum'];
|
|
15517
|
+
// Initials from a display name. Rules, in order:
|
|
15518
|
+
// - an email uses the local part, split on . _ + - john.smith@x.com -> JS, admin@x.com -> AD
|
|
15519
|
+
// - two or more words take FIRST + LAST, so middle names are ignored Mary Jane Watson -> MW
|
|
15520
|
+
// - one word takes its first two characters admin -> AD, Prince -> PR
|
|
15521
|
+
// - a single character stands alone X -> X
|
|
15522
|
+
function monogramInitials(display) {
|
|
15523
|
+
const s = (display ?? '').replace(/\s+/g, ' ').trim();
|
|
15524
|
+
if (!s)
|
|
15525
|
+
return '';
|
|
15526
|
+
if (s.includes('@')) {
|
|
15527
|
+
const local = s.split('@')[0];
|
|
15528
|
+
const parts = local.split(/[._+-]+/).filter(p => p.length);
|
|
15529
|
+
if (parts.length > 1)
|
|
15530
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
15531
|
+
return local.slice(0, 2).toUpperCase();
|
|
15532
|
+
}
|
|
15533
|
+
const words = s.split(' ').filter(w => w.length);
|
|
15534
|
+
if (words.length > 1)
|
|
15535
|
+
return (words[0][0] + words[words.length - 1][0]).toUpperCase();
|
|
15536
|
+
return words[0].slice(0, 2).toUpperCase();
|
|
15537
|
+
}
|
|
15538
|
+
// Classifies the actor. BaseBasic.CreatedByName is $"{CreatedByUser?.FirstName} {CreatedByUser?.LastName}",
|
|
15539
|
+
// which yields a bare " " when the user is missing — that trims to empty and lands here as 'unknown',
|
|
15540
|
+
// which is exactly why the trim above is load-bearing rather than cosmetic.
|
|
15541
|
+
function monogramKind(display, key) {
|
|
15542
|
+
const d = (display ?? '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
15543
|
+
const k = (key ?? '').trim().toLowerCase();
|
|
15544
|
+
if (SYSTEM_TOKENS.includes(k) || SYSTEM_TOKENS.includes(d))
|
|
15545
|
+
return 'system';
|
|
15546
|
+
if (!d && !k)
|
|
15547
|
+
return 'unknown';
|
|
15548
|
+
return 'person';
|
|
15549
|
+
}
|
|
15550
|
+
// FNV-1a over the stable key. Deterministic and well spread for short strings; Math.imul keeps the
|
|
15551
|
+
// multiply in 32-bit so the result is identical across engines.
|
|
15552
|
+
function monogramPaletteIndex(key, buckets) {
|
|
15553
|
+
const s = (key ?? '').trim().toLowerCase();
|
|
15554
|
+
let h = 0x811c9dc5;
|
|
15555
|
+
for (let i = 0; i < s.length; i++) {
|
|
15556
|
+
h ^= s.charCodeAt(i);
|
|
15557
|
+
h = Math.imul(h, 0x01000193);
|
|
15558
|
+
}
|
|
15559
|
+
return (h >>> 0) % buckets;
|
|
15560
|
+
}
|
|
15561
|
+
class MonogramComponent {
|
|
15562
|
+
constructor() {
|
|
15563
|
+
this.tapped = new EventEmitter(); // touch path — there is no hover on a phone
|
|
15564
|
+
this.initials = '';
|
|
15565
|
+
this.kind = 'unknown';
|
|
15566
|
+
this.tone = 'slate';
|
|
15567
|
+
this.label = 'Unknown';
|
|
15568
|
+
}
|
|
15569
|
+
ngOnChanges() {
|
|
15570
|
+
const display = (this.value ?? '').replace(/\s+/g, ' ').trim();
|
|
15571
|
+
this.kind = monogramKind(this.value, this.key);
|
|
15572
|
+
// Colour seeds off the stable key when there is one; the normalized display name is the fallback,
|
|
15573
|
+
// which is stable in practice because every audit name is formatted by one base class.
|
|
15574
|
+
const seed = (this.key ?? '').trim() || display;
|
|
15575
|
+
this.initials = monogramInitials(display || this.key);
|
|
15576
|
+
this.tone = this.kind === 'person' ? MONOGRAM_TONES[monogramPaletteIndex(seed, MONOGRAM_TONES.length)] : 'slate';
|
|
15577
|
+
this.label = this.kind === 'system' ? 'System' : (display || 'Unknown');
|
|
15578
|
+
}
|
|
15579
|
+
onTap() {
|
|
15580
|
+
this.tapped.emit(this.label); // tap/Enter surfaces the full name where hover cannot
|
|
15581
|
+
}
|
|
15582
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: MonogramComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15583
|
+
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 }); }
|
|
15584
|
+
}
|
|
15585
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: MonogramComponent, decorators: [{
|
|
15586
|
+
type: Component,
|
|
15587
|
+
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"] }]
|
|
15588
|
+
}], propDecorators: { value: [{
|
|
15589
|
+
type: Input
|
|
15590
|
+
}], key: [{
|
|
15591
|
+
type: Input
|
|
15592
|
+
}], tapped: [{
|
|
15593
|
+
type: Output
|
|
15340
15594
|
}] } });
|
|
15341
15595
|
|
|
15342
15596
|
class TableRowComponent {
|
|
@@ -15427,11 +15681,11 @@ class TableRowComponent {
|
|
|
15427
15681
|
return false;
|
|
15428
15682
|
}
|
|
15429
15683
|
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 }); }
|
|
15684
|
+
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
15685
|
}
|
|
15432
15686
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableRowComponent, decorators: [{
|
|
15433
15687
|
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"] }]
|
|
15688
|
+
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
15689
|
}], ctorParameters: () => [{ type: ButtonService }], propDecorators: { column: [{
|
|
15436
15690
|
type: Input
|
|
15437
15691
|
}], row: [{
|
|
@@ -15866,7 +16120,7 @@ class SelectComponent extends SelectCommonComponent {
|
|
|
15866
16120
|
let button = this.detailsConfig.buttons.find(b => b.name === mode);
|
|
15867
16121
|
button.detailsConfig = this.detailsConfig;
|
|
15868
16122
|
this.dialogService.openDefaultDetailsDialog(button, dynamicData).subscribe(result => {
|
|
15869
|
-
if (result
|
|
16123
|
+
if (result?.message === 'success') { // Changed: a dismissed quick-add dialog closes with undefined — read through it rather than throwing
|
|
15870
16124
|
// Added (dropdown quick-add): auto-select the record the user just created — the point of adding
|
|
15871
16125
|
// from a dropdown is to use the new record, so don't make them find it in the list afterwards
|
|
15872
16126
|
if (mode === 'create')
|
|
@@ -16594,7 +16848,7 @@ class InlineCellComponent {
|
|
|
16594
16848
|
<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
16849
|
|
|
16596
16850
|
</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"] }] }); }
|
|
16851
|
+
`, 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
16852
|
}
|
|
16599
16853
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: InlineCellComponent, decorators: [{
|
|
16600
16854
|
type: Component,
|
|
@@ -17055,7 +17309,7 @@ class GroupsComponent {
|
|
|
17055
17309
|
getVisibleButtons(item) {
|
|
17056
17310
|
let buttons = this.buttonCache.get(item);
|
|
17057
17311
|
if (!buttons) {
|
|
17058
|
-
buttons = this.displayedButtons ? this.displayedButtons.filter(button => Core.isItemVisible(button, item)) : []; // Changed: unified visible/hidden (boolean | condition)
|
|
17312
|
+
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
17313
|
this.buttonCache.set(item, buttons);
|
|
17060
17314
|
}
|
|
17061
17315
|
return buttons;
|
|
@@ -17129,6 +17383,7 @@ class TableComponent {
|
|
|
17129
17383
|
this.apiErrorService = apiErrorService;
|
|
17130
17384
|
this.runtimeConfig = runtimeConfig;
|
|
17131
17385
|
this.subs = []; // TS-8: breakpoint + parent-owned reload subscriptions leaked on table teardown; stacked reload subs also caused duplicate loadData fetches
|
|
17386
|
+
this.initialized = false; // Added: guards ensureInitialized — initialization must happen exactly once, whichever hook reaches it first
|
|
17132
17387
|
this.elevation = "mat-elevation-z5";
|
|
17133
17388
|
this.actionsWidth = "50px";
|
|
17134
17389
|
// Added: collapsible flat section header (sectionConfig) — Day Book sections
|
|
@@ -17201,6 +17456,15 @@ class TableComponent {
|
|
|
17201
17456
|
this.filterActive = false;
|
|
17202
17457
|
this.filterText = ''; // Added: current paged-mode filter text
|
|
17203
17458
|
this.groupFilterText = ''; // Added: relayed to spa-groups so the standard header filter drives the grouped view
|
|
17459
|
+
// Added: the headline for the "filter matched nothing" empty state. Built ONCE per filter change rather than
|
|
17460
|
+
// by a template getter — a getter that concatenates a string allocates on every change-detection pass.
|
|
17461
|
+
this.filterEmptyTitle = 'No matches';
|
|
17462
|
+
this.filterEmptyTitlePartial = 'No matches in the rows loaded so far'; // Added: the partial-window variant of the headline above (see filterScopePartial)
|
|
17463
|
+
// Added: a load that FAILED is not a load that returned nothing, and until now the grid could not tell them
|
|
17464
|
+
// apart. The failure paths never assign dataSource, so it stayed undefined and the template's
|
|
17465
|
+
// `!dataSource && !loadingStage` branch rendered "Loading..." — permanently, under a dismissed error dialog.
|
|
17466
|
+
// Set by every failure handler, cleared whenever a read starts or succeeds.
|
|
17467
|
+
this.loadFailed = false;
|
|
17204
17468
|
this.fetchSize = 20;
|
|
17205
17469
|
this.pagedUrlBase = '';
|
|
17206
17470
|
this.pageFetchInFlight = false;
|
|
@@ -17223,6 +17487,17 @@ class TableComponent {
|
|
|
17223
17487
|
}));
|
|
17224
17488
|
}
|
|
17225
17489
|
ngOnInit() {
|
|
17490
|
+
this.ensureInitialized(); // Changed: the body moved to ensureInitialized so ngOnChanges can pull it forward when it has to start a load first
|
|
17491
|
+
}
|
|
17492
|
+
// Added: Angular runs ngOnChanges BEFORE ngOnInit on the first pass. A lazy tab that is already active on the
|
|
17493
|
+
// first binding (a details dialog's tableConfigs[0]) therefore started its load from ngOnChanges while
|
|
17494
|
+
// setupPagination() had not yet run, so pagedMode was still false and the rows landed on the legacy path;
|
|
17495
|
+
// ngOnInit then flipped pagedMode on and the next updateSlice() drew from the empty loadedRows window and
|
|
17496
|
+
// replaced the good rows with nothing. Initialization is now idempotent so the load site can order it first.
|
|
17497
|
+
ensureInitialized() {
|
|
17498
|
+
if (this.initialized)
|
|
17499
|
+
return;
|
|
17500
|
+
this.initialized = true;
|
|
17226
17501
|
this.sectionCollapsed = !!this.config?.sectionConfig?.collapsed; // Added: seed section collapse from config — state lives on the component, never written back to the shared config
|
|
17227
17502
|
if (this.config?.formConfig) {
|
|
17228
17503
|
this.hasFormAccess = Core.hasFormAccess(this.config.formConfig, this.authService.currentRoleSource.value);
|
|
@@ -17245,6 +17520,7 @@ class TableComponent {
|
|
|
17245
17520
|
}
|
|
17246
17521
|
if (this.inTab && changes['activeTab']) {
|
|
17247
17522
|
if (!this.hasBeenActivated && this.activeTab && this.config?.lazyLoad && this.config.loadAction) {
|
|
17523
|
+
this.ensureInitialized(); // Changed: this is the ONLY load ngOnChanges can start, and it must not run ahead of setupPagination() — see ensureInitialized
|
|
17248
17524
|
this.loadData(this.config.loadAction, "");
|
|
17249
17525
|
this.hasBeenActivated = true;
|
|
17250
17526
|
}
|
|
@@ -17253,16 +17529,29 @@ class TableComponent {
|
|
|
17253
17529
|
get sectionHidden() {
|
|
17254
17530
|
return !!(this.config?.sectionConfig?.hideWhenEmpty && this.dataSource?.length === 0);
|
|
17255
17531
|
}
|
|
17532
|
+
// Changed: the expand half is delegated to expandSection so the two entry points cannot drift apart
|
|
17256
17533
|
toggleSection() {
|
|
17257
17534
|
if (this.config?.sectionConfig?.collapsible === false)
|
|
17258
17535
|
return;
|
|
17259
|
-
this.sectionCollapsed
|
|
17536
|
+
if (this.sectionCollapsed) {
|
|
17537
|
+
this.expandSection();
|
|
17538
|
+
return;
|
|
17539
|
+
}
|
|
17540
|
+
this.sectionCollapsed = true;
|
|
17541
|
+
}
|
|
17542
|
+
// Added: the expand-ONLY entry point, for a caller that wants this section open rather than flipped — a Day
|
|
17543
|
+
// Book chip or stage tile means "show me this list", and routing that through toggleSection() closes the very
|
|
17544
|
+
// thing the user asked to see whenever the section happened to be open already. Deliberately NOT gated on
|
|
17545
|
+
// `collapsible === false`: a section the operator may not collapse by hand is one that should be open anyway.
|
|
17546
|
+
expandSection() {
|
|
17260
17547
|
if (!this.sectionCollapsed)
|
|
17261
|
-
|
|
17548
|
+
return;
|
|
17549
|
+
this.sectionCollapsed = false;
|
|
17550
|
+
setTimeout(() => this.setPaginator()); // re-attach — the paginator ViewChild did not exist while collapsed
|
|
17262
17551
|
}
|
|
17263
17552
|
buildSectionHeader() {
|
|
17264
17553
|
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));
|
|
17554
|
+
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
17555
|
}
|
|
17267
17556
|
sectionChips() { return this.sectionChipList; } // Changed: returns the materialized field
|
|
17268
17557
|
sectionButtons() { return this.sectionButtonList; } // Changed: returns the materialized field
|
|
@@ -17344,6 +17633,45 @@ class TableComponent {
|
|
|
17344
17633
|
return 35;
|
|
17345
17634
|
return [72, 58, 86, 64, 78, 52][index % 6];
|
|
17346
17635
|
}
|
|
17636
|
+
// Added: "nothing here yet" and "nothing matches what you typed" are two different states and they get two
|
|
17637
|
+
// different sentences. The "No Data" line beside this one tests dataSource — the LOADED WINDOW — which a
|
|
17638
|
+
// filter never shrinks: filtering happens downstream of it (MatTableDataSource.filteredData when unpaged,
|
|
17639
|
+
// filteredRows when paged). So a filter matching nothing left dataSource full, the line suppressed, and the
|
|
17640
|
+
// user staring at a header, a "0 of 0" paginator and no explanation. This getter is the missing case only —
|
|
17641
|
+
// it is FALSE whenever dataSource is empty, so the genuinely-empty state keeps the line and the words it
|
|
17642
|
+
// always had. Reads lengths and flags only; it allocates nothing, so it is safe to bind in the template.
|
|
17643
|
+
get filteredToNothing() {
|
|
17644
|
+
if (this.loadingStage === 'initial')
|
|
17645
|
+
return false; // the loading stage owns the space until it completes
|
|
17646
|
+
if (!(this.dataSource?.length > 0))
|
|
17647
|
+
return false; // genuinely empty — "No Data" answers for that state
|
|
17648
|
+
// Only the table view filters what it renders. Cards and capsules are bound to dataSource directly and show
|
|
17649
|
+
// every row regardless of the filter, and the grouped view filters on its own predicate inside spa-groups —
|
|
17650
|
+
// claiming "no matches" over a screen still full of cards would be a worse lie than saying nothing.
|
|
17651
|
+
if (this.config?.viewType && this.config.viewType !== 'table')
|
|
17652
|
+
return false;
|
|
17653
|
+
if (this.pagedMode)
|
|
17654
|
+
return this.filterActive && this.filteredRows?.length === 0;
|
|
17655
|
+
return !!this.tableDataSource?.filter && this.tableDataSource.filteredData?.length === 0;
|
|
17656
|
+
}
|
|
17657
|
+
// Added: THE predicate for "the filter did not see everything". In paged mode the client filter runs over
|
|
17658
|
+
// loadedRows — the accumulated window — and filterChanged issues no fetch, so with a partial window a filter
|
|
17659
|
+
// searched the loaded rows ONLY. This getter is deliberately the single definition of that fact: the hint
|
|
17660
|
+
// banner above the grid and the empty-state message below it both bind to it, so the two physically cannot
|
|
17661
|
+
// disagree about how much was searched. They previously stated it independently and did — the banner said
|
|
17662
|
+
// "only the 20 loaded rows of 240" while the message underneath offered "all 240 rows", implying the filter
|
|
17663
|
+
// had covered the lot. Reads lengths and flags only; allocates nothing, so it is safe to bind in a template.
|
|
17664
|
+
get filterScopePartial() {
|
|
17665
|
+
return this.pagedMode && this.filterActive && this.loadedRows.length < this.serverTotal;
|
|
17666
|
+
}
|
|
17667
|
+
// Added: what the user gets back by clearing the filter. Paged mode's dataSource is only the loaded window, so
|
|
17668
|
+
// the server total (the same figure its own paginator shows) is the honest number there. Only ever rendered in
|
|
17669
|
+
// the COMPLETE-scope state — where the window is partial the count would misdescribe what was searched, which
|
|
17670
|
+
// is exactly why that state gets its own sentence rather than this one.
|
|
17671
|
+
get filteredTotal() {
|
|
17672
|
+
return this.pagedMode ? this.serverTotal + this.overlayDelta : (this.dataSource?.length ?? 0);
|
|
17673
|
+
}
|
|
17674
|
+
get filteredRowNoun() { return this.filteredTotal === 1 ? 'row' : 'rows'; } // Added: keeps "all 1 rows" out of the message
|
|
17347
17675
|
get stageEntityName() {
|
|
17348
17676
|
// An author-supplied entityName is their wording and is used verbatim.
|
|
17349
17677
|
if (this.config?.entityName)
|
|
@@ -17474,6 +17802,25 @@ class TableComponent {
|
|
|
17474
17802
|
return null;
|
|
17475
17803
|
return field;
|
|
17476
17804
|
}
|
|
17805
|
+
// Added: every field the CONFIG declares inline-editable, whether or not it has a cell on screen
|
|
17806
|
+
configuredInlineFields() {
|
|
17807
|
+
return (this.config?.columns ?? []).map(column => this.getInlineField(column)).filter(field => !!field);
|
|
17808
|
+
}
|
|
17809
|
+
// Added: the inline-editable fields that are ACTUALLY RENDERED. <app-inline-cell> lives inside a per-column
|
|
17810
|
+
// matColumnDef (table.component.html:101), so a column missing from displayedColumns has no editor at all.
|
|
17811
|
+
// displayedColumns is not config.columns: setColumns REPLACES it with the minColumns subset below 600px
|
|
17812
|
+
// (table-config.service.ts:26-27) and also drops columns pruned by a visible/hidden predicate.
|
|
17813
|
+
renderedInlineFields() {
|
|
17814
|
+
return (this.config?.columns ?? []).filter(column => this.displayedColumns?.includes(column.name)).map(column => this.getInlineField(column)).filter(field => !!field);
|
|
17815
|
+
}
|
|
17816
|
+
// Added: inline edit is only honest when EVERY editable field has a cell the operator can see and change.
|
|
17817
|
+
// Previously seedInlineDefaults and submitInlineEdit both walked config.columns, so a field cut by minColumns
|
|
17818
|
+
// was still seeded with its defaultValue, still passed the required check, and was still posted — the row
|
|
17819
|
+
// submitted values nobody ever saw (piglet weaned litters at an optimistic count on a phone, 2026-08-10).
|
|
17820
|
+
canEditInline() {
|
|
17821
|
+
const configured = this.configuredInlineFields();
|
|
17822
|
+
return configured.length > 0 && this.renderedInlineFields().length === configured.length;
|
|
17823
|
+
}
|
|
17477
17824
|
startInlineEdit(row) {
|
|
17478
17825
|
this.editingRow = row;
|
|
17479
17826
|
this.editingModel = { ...row }; // shallow copy — scalars only change, cancel restores by discarding
|
|
@@ -17487,9 +17834,8 @@ class TableComponent {
|
|
|
17487
17834
|
seedInlineDefaults(row) {
|
|
17488
17835
|
if (!this.config.inlineEdit || !this.config.columns)
|
|
17489
17836
|
return;
|
|
17490
|
-
for (const
|
|
17491
|
-
|
|
17492
|
-
if (!field || field.defaultValue === undefined || field.defaultValue === null)
|
|
17837
|
+
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
|
|
17838
|
+
if (field.defaultValue === undefined || field.defaultValue === null)
|
|
17493
17839
|
continue;
|
|
17494
17840
|
const current = this.editingModel[field.name];
|
|
17495
17841
|
if (current !== undefined && current !== null && current !== '')
|
|
@@ -17505,7 +17851,7 @@ class TableComponent {
|
|
|
17505
17851
|
if (!this.editingRow)
|
|
17506
17852
|
return;
|
|
17507
17853
|
// Validate only the fields that are editable inline, with the same rules the dialog form applies
|
|
17508
|
-
const editableFields = this.config.columns
|
|
17854
|
+
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
17855
|
const validationResult = Core.validateObject(editableFields, this.editingModel);
|
|
17510
17856
|
if (validationResult !== '') {
|
|
17511
17857
|
this.messageService.toast(validationResult);
|
|
@@ -17552,6 +17898,13 @@ class TableComponent {
|
|
|
17552
17898
|
this.displayedColumns = this.tableConfigService.setColumns(this.config, this.smallScreen);
|
|
17553
17899
|
this.displayedButtons = this.tableConfigService.getDisplayedButtons(this.config?.buttons, this.smallScreen, this.config);
|
|
17554
17900
|
this.actionsWidth = this.tableConfigService.getActionsWidth(this.displayedButtons, this.smallScreen, this.config);
|
|
17901
|
+
// Added: the same defect through the resize door. A row opened for edit on a wide screen keeps its seeded
|
|
17902
|
+
// editingModel when the window narrows past 600px, but its cells vanish with the columns — so the tick would
|
|
17903
|
+
// post values the operator can no longer see. Discard the edit instead; cancel has always been non-destructive.
|
|
17904
|
+
if (this.editingRow && !this.canEditInline()) {
|
|
17905
|
+
this.cancelInlineEdit();
|
|
17906
|
+
this.messageService.toast('Editing was closed — this screen is too narrow to show every editable field');
|
|
17907
|
+
}
|
|
17555
17908
|
if (this.config?.searchConfig) {
|
|
17556
17909
|
this.showFilterButton = false;
|
|
17557
17910
|
}
|
|
@@ -17574,6 +17927,7 @@ class TableComponent {
|
|
|
17574
17927
|
}
|
|
17575
17928
|
}
|
|
17576
17929
|
loadData(action, data) {
|
|
17930
|
+
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
17931
|
if (this.pagedMode) {
|
|
17578
17932
|
this.loadDataPaged(action, data);
|
|
17579
17933
|
return;
|
|
@@ -17611,10 +17965,36 @@ class TableComponent {
|
|
|
17611
17965
|
}
|
|
17612
17966
|
}
|
|
17613
17967
|
else {
|
|
17968
|
+
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
17969
|
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
17970
|
}
|
|
17616
|
-
}
|
|
17617
|
-
|
|
17971
|
+
},
|
|
17972
|
+
// Added: the TRANSPORT failure — a dead API, a dropped connection, a CORS refusal. It never reaches the
|
|
17973
|
+
// branch above because there is no ApiResponse at all: LoaderInterceptor shows the dialog and rethrows,
|
|
17974
|
+
// and this subscribe had no error callback, so the grid was left rendering "No Data" — the one thing
|
|
17975
|
+
// that was definitely NOT true. Rethrown so the error still reaches Angular's global handler exactly as
|
|
17976
|
+
// it did before; this callback only records WHY the grid is empty.
|
|
17977
|
+
(err) => { this.loadFailed = true; throw err; });
|
|
17978
|
+
}
|
|
17979
|
+
}
|
|
17980
|
+
// Added (phone density, opt-in via config.hideSinglePagePaginator): true when the pager is navigating a
|
|
17981
|
+
// single page on a phone, and is therefore 56px of chrome that can reach nowhere it is not already. The
|
|
17982
|
+
// paginator is still RENDERED and only hidden in CSS — the same reason the empty-data case is a class and
|
|
17983
|
+
// not an *ngIf: #tablePaginator is a ViewChild that setPaginator() attaches to the MatTableDataSource, and
|
|
17984
|
+
// removing it from the DOM would break paging for every table that does have a second page.
|
|
17985
|
+
// Desktop returns false on the smallScreen guard alone, so nothing above 600px can be affected.
|
|
17986
|
+
get singlePagePaginatorHidden() {
|
|
17987
|
+
if (!this.config?.hideSinglePagePaginator || !this.smallScreen)
|
|
17988
|
+
return false;
|
|
17989
|
+
// On a phone hidePageSize is on, so the size cannot be changed from the UI and the paginator's own value
|
|
17990
|
+
// and pageSizes[0] agree; reading the live one first keeps this honest if it was changed before a resize.
|
|
17991
|
+
const size = this.pagedMode ? this.pageSize : (this.tablePaginator?.pageSize || this.config?.pageSizes?.[0] || 10);
|
|
17992
|
+
if (!size)
|
|
17993
|
+
return false;
|
|
17994
|
+
const total = this.pagedMode
|
|
17995
|
+
? (this.filterActive ? this.filteredRows.length : this.serverTotal + this.overlayDelta)
|
|
17996
|
+
: (this.dataSource?.length || 0);
|
|
17997
|
+
return total > 0 && total <= size; // zero rows is already handled by the existing empty-data rule
|
|
17618
17998
|
}
|
|
17619
17999
|
setPaginator() {
|
|
17620
18000
|
if (this.pagedMode)
|
|
@@ -17674,9 +18054,11 @@ class TableComponent {
|
|
|
17674
18054
|
}
|
|
17675
18055
|
}
|
|
17676
18056
|
else {
|
|
18057
|
+
this.loadFailed = true; // Added: same treatment as the unpaged path — a failed search load must not read as a load still running
|
|
17677
18058
|
this.apiErrorService.presentAppFailure(apiResponse, 'load', action.url); // Changed: paged-mode search load — same classify + dedupe treatment as the unpaged path above
|
|
17678
18059
|
}
|
|
17679
|
-
});
|
|
18060
|
+
}, (err) => { this.loadFailed = true; throw err; } // Added: transport failure — same reasoning as the unpaged path above
|
|
18061
|
+
);
|
|
17680
18062
|
return;
|
|
17681
18063
|
}
|
|
17682
18064
|
if (action.url !== this.pagedUrlBase) {
|
|
@@ -17708,6 +18090,7 @@ class TableComponent {
|
|
|
17708
18090
|
console.log(apiResponse);
|
|
17709
18091
|
}
|
|
17710
18092
|
if (!apiResponse.success) {
|
|
18093
|
+
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
18094
|
this.apiErrorService.presentAppFailure(apiResponse, 'load', pagedAction.url); // Changed: chunk fetch — scrolling a broken table fires these repeatedly, so deduping matters most here
|
|
17712
18095
|
return;
|
|
17713
18096
|
}
|
|
@@ -17735,7 +18118,7 @@ class TableComponent {
|
|
|
17735
18118
|
if (apiResponse.message != "success" && apiResponse.message != "") {
|
|
17736
18119
|
this.messageService.toast(apiResponse.message);
|
|
17737
18120
|
}
|
|
17738
|
-
}, () => { this.pageFetchInFlight = false; } // never leave the single-flight guard stuck on a failed request
|
|
18121
|
+
}, () => { 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
18122
|
);
|
|
17740
18123
|
}
|
|
17741
18124
|
// Appends skip/take to a URL copy at fetch time
|
|
@@ -17815,6 +18198,17 @@ class TableComponent {
|
|
|
17815
18198
|
// so its text has to be relayed regardless of paged mode. Set before the paged-mode guard below — grouped
|
|
17816
18199
|
// views are not paged, and returning early here is what would leave the header filter doing nothing.
|
|
17817
18200
|
this.groupFilterText = (text ?? '').trim();
|
|
18201
|
+
// Added: build the filtered-empty headline here, where the text arrives for EVERY mode. spa-filter is the
|
|
18202
|
+
// only writer of MatTableDataSource.filter in the library and it emits on keyUp, clear and ngOnChanges
|
|
18203
|
+
// alike, so this field cannot drift from the filter the grid is actually applying. Long pastes are capped
|
|
18204
|
+
// so one field cannot push the message off a phone screen.
|
|
18205
|
+
const term = this.groupFilterText;
|
|
18206
|
+
const shown = term.length > 40 ? term.slice(0, 40) + '…' : term;
|
|
18207
|
+
this.filterEmptyTitle = term ? `No matches for “${shown}”` : 'No matches';
|
|
18208
|
+
// Added: the partial-window headline. Built here beside its sibling so both are computed once per filter
|
|
18209
|
+
// change rather than per change-detection pass. Neither string embeds a COUNT — counts are the banner's and
|
|
18210
|
+
// the paginator's to state — so neither can go stale if the loaded window grows while a filter is active.
|
|
18211
|
+
this.filterEmptyTitlePartial = term ? `No matches for “${shown}” in the rows loaded so far` : 'No matches in the rows loaded so far';
|
|
17818
18212
|
if (!this.pagedMode)
|
|
17819
18213
|
return;
|
|
17820
18214
|
this.filterText = (text ?? '').trim();
|
|
@@ -17830,7 +18224,9 @@ class TableComponent {
|
|
|
17830
18224
|
let config = new FormConfig;
|
|
17831
18225
|
config.fields = [];
|
|
17832
18226
|
this.config.columns.forEach(column => {
|
|
17833
|
-
|
|
18227
|
+
// Changed: monogram is a DISPLAY-only column type with no form editor — the value behind it is a plain
|
|
18228
|
+
// name string, so an auto-derived field falls back to text rather than leaking an unknown Field type.
|
|
18229
|
+
let field = { name: column.name, type: column.type === 'monogram' ? 'text' : column.type };
|
|
17834
18230
|
config.fields.push(field);
|
|
17835
18231
|
});
|
|
17836
18232
|
this.config.formConfig = config;
|
|
@@ -17978,8 +18374,11 @@ class TableComponent {
|
|
|
17978
18374
|
this.viewModel(actionData, actionButton);
|
|
17979
18375
|
}
|
|
17980
18376
|
else if (name === 'edit') {
|
|
17981
|
-
// Changed: inlineEdit tables open the row cells in place instead of the dialog
|
|
17982
|
-
|
|
18377
|
+
// Changed: inlineEdit tables open the row cells in place instead of the dialog — but ONLY when every
|
|
18378
|
+
// editable field actually has a cell on screen. Inline edit is an optimisation of the dialog, so when it
|
|
18379
|
+
// cannot show the whole editor it falls back to the dialog it was optimising: same row, same edit action,
|
|
18380
|
+
// every field rendered (a form has no minColumns). Nothing is submitted that the operator could not see.
|
|
18381
|
+
if (this.config.inlineEdit && this.canEditInline()) {
|
|
17983
18382
|
this.startInlineEdit(actionData);
|
|
17984
18383
|
return;
|
|
17985
18384
|
}
|
|
@@ -18067,6 +18466,8 @@ class TableComponent {
|
|
|
18067
18466
|
return;
|
|
18068
18467
|
}
|
|
18069
18468
|
this.dialogService.openDefaultDetailsDialog(btn, row, this.nestingLevel).subscribe(result => {
|
|
18469
|
+
if (!result)
|
|
18470
|
+
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
18471
|
if (result.action === 'doAction') {
|
|
18071
18472
|
this.doAction(result.name, result.row);
|
|
18072
18473
|
this.actionClickedEmit(result.name, result.row);
|
|
@@ -18181,13 +18582,17 @@ class TableComponent {
|
|
|
18181
18582
|
return "mat-elevation-z5";
|
|
18182
18583
|
}
|
|
18183
18584
|
}
|
|
18184
|
-
// Changed:
|
|
18585
|
+
// Changed: this now ALWAYS refreshes. The old body refreshed only when realTime was OFF or SignalR was DOWN,
|
|
18586
|
+
// so a healthy real-time table did nothing after a write and simply waited for a broadcast. But the SignalR
|
|
18587
|
+
// streams below are filtered on this table's own entityName, and an action that writes a DIFFERENT entity
|
|
18588
|
+
// (invoicing a rental writes an Invoice) never emits a matching one — grip's rentals grid kept rendering
|
|
18589
|
+
// "Returned" for a row already Invoiced in the database. Nothing correlates a broadcast back to the action
|
|
18590
|
+
// just performed, so "refresh unless a broadcast arrives" is not implementable without new machinery.
|
|
18591
|
+
// A same-URL re-read after a user-initiated write is idempotent; a broadcast that does arrive patches the same rows.
|
|
18185
18592
|
// In paged mode this is NOT a full reload — refreshClicked routes through loadDataPaged's same-URL branch,
|
|
18186
18593
|
// which re-fetches only the loaded window (skip=0, take=loadedCount), never the whole dataset.
|
|
18187
18594
|
realTimeRefreshOrFallback() {
|
|
18188
|
-
if (!this.effRealTime || !this.isSignalRConnected)
|
|
18189
|
-
this.refreshClicked();
|
|
18190
|
-
}
|
|
18595
|
+
this.refreshClicked(); // Changed: unconditional — was `if (!this.effRealTime || !this.isSignalRConnected)`, which made the healthy real-time case a silent no-op
|
|
18191
18596
|
}
|
|
18192
18597
|
//---------------- TinSync offline support ----------------
|
|
18193
18598
|
// Registers the table's URLs with the offline service, starts the sync engine, and wires live overlay updates
|
|
@@ -18437,11 +18842,11 @@ class TableComponent {
|
|
|
18437
18842
|
this.setPaginator();
|
|
18438
18843
|
}
|
|
18439
18844
|
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" }] }); }
|
|
18845
|
+
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
18846
|
}
|
|
18442
18847
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableComponent, decorators: [{
|
|
18443
18848
|
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"] }]
|
|
18849
|
+
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
18850
|
}], 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
18851
|
type: Optional
|
|
18447
18852
|
}, {
|
|
@@ -18502,6 +18907,7 @@ class DayBookComponent {
|
|
|
18502
18907
|
this.messageService = inject(MessageService); // Added: page actions confirm and toast like every other button in the suite
|
|
18503
18908
|
this.apiErrorService = inject(ApiErrorService);
|
|
18504
18909
|
this.buttonService = inject(ButtonService);
|
|
18910
|
+
this.zone = inject(NgZone, { optional: true }); // Added: optional so a bare test harness instantiating this class directly still constructs
|
|
18505
18911
|
this.book = null;
|
|
18506
18912
|
this.loading = false; // the re-entrancy guard — deliberately NOT bound in the template (see showLoading)
|
|
18507
18913
|
// Added (NG0100): what the template actually binds. `loading` flips inside load(), and load() can be reached
|
|
@@ -18582,7 +18988,10 @@ class DayBookComponent {
|
|
|
18582
18988
|
// pure config silently lost its Refresh. These come from config instead, and the ng-content slot is still
|
|
18583
18989
|
// there beside them for anyone who does host the page themselves.
|
|
18584
18990
|
buildActions() {
|
|
18585
|
-
|
|
18991
|
+
// Changed: also honour the button's opt-in capability. Day Book actions carry NO formConfig, so nothing in
|
|
18992
|
+
// the library could ever gate them — hasCapability is the only gate that reaches here. Buttons that name no
|
|
18993
|
+
// capability are unaffected (hasCapability returns true); only isItemVisible ran before and it still runs.
|
|
18994
|
+
this.pageActions = (this.config?.actions || []).filter(b => Core.isItemVisible(b, this.book) && this.buttonService.hasCapability(b));
|
|
18586
18995
|
}
|
|
18587
18996
|
pageActionClicked(button) {
|
|
18588
18997
|
if (button.onClick) {
|
|
@@ -18714,6 +19123,13 @@ class DayBookComponent {
|
|
|
18714
19123
|
const cfg = { ...section.tableConfig };
|
|
18715
19124
|
cfg.sectionConfig = section.tableConfig.sectionConfig ? { ...section.tableConfig.sectionConfig, collapsed: section.collapsed !== false } : undefined;
|
|
18716
19125
|
cfg.elevation = cfg.elevation ?? 'none'; // flat tables on the section cards — Day Book design language
|
|
19126
|
+
// Added (phone density): a Day Book stacks 9-16 of these cards, and on a phone each one was spending
|
|
19127
|
+
// 56px on a pager that read "1 – 4 of 4" — the largest single item in the card, navigating nothing.
|
|
19128
|
+
// The count it reports is already on the section header's own badge, so nothing is lost. This is a Day
|
|
19129
|
+
// Book decision made ONCE in the library rather than repeated in five app configs, exactly as the
|
|
19130
|
+
// carousel stage strip is; an app that wants its pagers back still sets the flag false on its own
|
|
19131
|
+
// tableConfig. Desktop is untouched — the flag only ever acts below 600px.
|
|
19132
|
+
cfg.hideSinglePagePaginator = cfg.hideSinglePagePaginator ?? true;
|
|
18717
19133
|
if (cfg.sectionConfig) {
|
|
18718
19134
|
cfg.sectionConfig.hideWhenEmpty = cfg.sectionConfig.hideWhenEmpty !== false; // sections disappear when empty by default
|
|
18719
19135
|
// Added: the section's "why this list exists" line rides INSIDE the table's own header, under the
|
|
@@ -18792,20 +19208,66 @@ class DayBookComponent {
|
|
|
18792
19208
|
this.groupState[group.lane.key] = group.collapsed;
|
|
18793
19209
|
localStorage.setItem('tin-daybook-groups', JSON.stringify(this.groupState));
|
|
18794
19210
|
}
|
|
18795
|
-
// Un-collapse BEFORE scrolling (piglet's fix, applied suite-wide) — a click must never land on a bare header
|
|
19211
|
+
// Un-collapse BEFORE scrolling (piglet's fix, applied suite-wide) — a click must never land on a bare header.
|
|
19212
|
+
//
|
|
19213
|
+
// Changed (owner, 2026-08-09): a chip scrolled to its section and left it SHUT, so the operator had to click
|
|
19214
|
+
// again to see the list he had just asked for. There are THREE doors between a chip and its rows, not one:
|
|
19215
|
+
//
|
|
19216
|
+
// chip → lane GROUP (.db-gbody is *ngIf="!g.collapsed") → SECTION (spa-table's own sectionCollapsed)
|
|
19217
|
+
//
|
|
19218
|
+
// and the previous code only really cleared the third. It opened the group by flipping `group.collapsed`, then
|
|
19219
|
+
// in the SAME synchronous tick asked `this.tables` for the section's table — but Angular had not re-rendered
|
|
19220
|
+
// yet, so that table did not exist in the QueryList and the expand silently no-opped. Which is exactly the
|
|
19221
|
+
// reported symptom, and it applied to every chip: chips are only rendered on a COLLAPSED group, so the broken
|
|
19222
|
+
// path was the only path a chip could ever take.
|
|
19223
|
+
//
|
|
19224
|
+
// The fix does not race the renderer. Level 2 seeds the section's own cloned config, so a table that has not
|
|
19225
|
+
// been created yet is BORN open (TableComponent.ngOnInit seeds sectionCollapsed off exactly this field), and
|
|
19226
|
+
// only expands a live instance if there already is one. Nothing here has to guess when Angular will catch up.
|
|
18796
19227
|
goTo(key) {
|
|
18797
19228
|
const target = this.sectionConfigs.find(x => x.section.key === key);
|
|
18798
19229
|
if (target) {
|
|
18799
|
-
//
|
|
18800
|
-
// must open the group before it opens the section, or the scroll lands on nothing that is in the DOM.
|
|
19230
|
+
// Level 1 — the lane group. Nothing inside a shut group is in the DOM at all.
|
|
18801
19231
|
const group = this.visibleGroups.find(g => g.lane.key === target.section.lane);
|
|
18802
|
-
if (group
|
|
18803
|
-
this.toggleGroup(group);
|
|
18804
|
-
|
|
18805
|
-
if (
|
|
18806
|
-
|
|
18807
|
-
|
|
18808
|
-
|
|
19232
|
+
if (group?.collapsed)
|
|
19233
|
+
this.toggleGroup(group); // expand-only: an already-open group is left alone, and so is every OTHER group — this is not an accordion
|
|
19234
|
+
// Level 2 — the section. Seeding the config covers the not-yet-created table; expandSection covers the live one.
|
|
19235
|
+
if (target.cfg?.sectionConfig)
|
|
19236
|
+
target.cfg.sectionConfig.collapsed = false; // cfg is this section's own clone (buildSections), never the app's shared singleton
|
|
19237
|
+
target.collapsed = false;
|
|
19238
|
+
this.tables?.find(t => t.config === target.cfg)?.expandSection(); // expand, never toggle — a toggle would shut a section that was already open
|
|
19239
|
+
}
|
|
19240
|
+
// Level 3 — scroll, but only once the two expansions above have actually changed the layout.
|
|
19241
|
+
this.scrollWhenSettled('sec-' + key);
|
|
19242
|
+
}
|
|
19243
|
+
// Added: the scroll has to be the LAST thing that happens, and a fixed delay cannot promise that. A section
|
|
19244
|
+
// that is still collapsed when we scroll is ~110px of bare chrome: we land on that position, the section then
|
|
19245
|
+
// expands to several hundred px and grows UNDER the viewport, and the operator ends up parked below the list
|
|
19246
|
+
// he asked for — looking, from his side, exactly like the bug we are fixing. So instead of guessing a delay we
|
|
19247
|
+
// watch the target until its height stops changing (the same measurement two frames running), which covers the
|
|
19248
|
+
// group render, the table's own creation and the paginator re-attach without knowing anything about any of
|
|
19249
|
+
// them. The frame cap stops a section whose height never settles (a live-updating list) from never scrolling.
|
|
19250
|
+
// Runs outside Angular: this is a poll, and every frame of it would otherwise drag a change-detection pass
|
|
19251
|
+
// behind it for no reason — the house rule about not putting work on the CD path applies to timers too.
|
|
19252
|
+
scrollWhenSettled(id) {
|
|
19253
|
+
const smooth = !window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches; // a jump, not a glide, when the operator has asked for less motion
|
|
19254
|
+
const run = () => {
|
|
19255
|
+
let previous = -1;
|
|
19256
|
+
let frames = 0;
|
|
19257
|
+
const step = () => {
|
|
19258
|
+
const element = document.getElementById(id);
|
|
19259
|
+
const height = element ? element.getBoundingClientRect().height : -1;
|
|
19260
|
+
if ((element && height === previous) || frames >= 20) {
|
|
19261
|
+
element?.scrollIntoView({ behavior: smooth ? 'smooth' : 'auto', block: 'start' });
|
|
19262
|
+
return;
|
|
19263
|
+
}
|
|
19264
|
+
previous = height;
|
|
19265
|
+
frames++;
|
|
19266
|
+
requestAnimationFrame(step);
|
|
19267
|
+
};
|
|
19268
|
+
requestAnimationFrame(step);
|
|
19269
|
+
};
|
|
19270
|
+
this.zone ? this.zone.runOutsideAngular(run) : run();
|
|
18809
19271
|
}
|
|
18810
19272
|
//---------- Header facts ----------
|
|
18811
19273
|
get booksFrom() {
|
|
@@ -18827,11 +19289,11 @@ class DayBookComponent {
|
|
|
18827
19289
|
return stages.length > 0 && stages.every(s => !this.stageCount(s));
|
|
18828
19290
|
}
|
|
18829
19291
|
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" }] }); }
|
|
19292
|
+
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
19293
|
}
|
|
18832
19294
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DayBookComponent, decorators: [{
|
|
18833
19295
|
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"] }]
|
|
19296
|
+
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
19297
|
}], propDecorators: { tables: [{
|
|
18836
19298
|
type: ViewChildren,
|
|
18837
19299
|
args: [TableComponent]
|
|
@@ -19723,11 +20185,11 @@ class FormComponent {
|
|
|
19723
20185
|
processForm() {
|
|
19724
20186
|
}
|
|
19725
20187
|
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" }] }); }
|
|
20188
|
+
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
20189
|
}
|
|
19728
20190
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: FormComponent, decorators: [{
|
|
19729
20191
|
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"] }]
|
|
20192
|
+
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
20193
|
}], ctorParameters: () => [{ type: MessageService }, { type: DataServiceLib }, { type: AuthService }, { type: ApiErrorService }], propDecorators: { dynamicSelectTemplate: [{
|
|
19732
20194
|
type: ContentChild,
|
|
19733
20195
|
args: ['dynamicSelect']
|
|
@@ -19768,6 +20230,11 @@ class AppConfigurationComponent {
|
|
|
19768
20230
|
this.loading = false;
|
|
19769
20231
|
this.saving = false;
|
|
19770
20232
|
this.canEdit = true;
|
|
20233
|
+
// Added: the page's own record that the last save was REFUSED. Without it the only evidence was the 5s toast
|
|
20234
|
+
// (a business refusal) or a dialog the user dismisses (a technical failure) — after either one vanished, an
|
|
20235
|
+
// unsaved page looked exactly like a saved one while still displaying the value the server rejected. Held
|
|
20236
|
+
// until a save actually succeeds, NOT cleared on edit: editing does not make the shown values in force.
|
|
20237
|
+
this.saveFailure = null;
|
|
19771
20238
|
// Materialized, never a template-bound getter — a getter returning a new array here would hand the
|
|
19772
20239
|
// *ngFor a fresh reference on every change-detection pass (see the day-book/table livelock).
|
|
19773
20240
|
this.cards = [];
|
|
@@ -19847,6 +20314,14 @@ class AppConfigurationComponent {
|
|
|
19847
20314
|
onInputChange() {
|
|
19848
20315
|
this.buildVisibleCards();
|
|
19849
20316
|
}
|
|
20317
|
+
// Added: the escape hatch for the banner. Reverting the form automatically on a refusal was rejected — it
|
|
20318
|
+
// throws away every OTHER edit on a page that saves as one object, and on a business refusal it also destroys
|
|
20319
|
+
// the values the user needs in front of them to act on the message. Offered as a deliberate press instead, so
|
|
20320
|
+
// "show me what is actually saved" is one click away without being forced on anyone.
|
|
20321
|
+
discardChanges() {
|
|
20322
|
+
this.saveFailure = null;
|
|
20323
|
+
this.load();
|
|
20324
|
+
}
|
|
19850
20325
|
//---------- Save ----------
|
|
19851
20326
|
save() {
|
|
19852
20327
|
if (this.saving || !this.canEdit)
|
|
@@ -19856,6 +20331,7 @@ class AppConfigurationComponent {
|
|
|
19856
20331
|
next: (response) => {
|
|
19857
20332
|
this.saving = false;
|
|
19858
20333
|
if (response.success) {
|
|
20334
|
+
this.saveFailure = null; // Added: the values on screen are now the values in force, so the banner must go
|
|
19859
20335
|
this.messageService.toast('Configuration updated successfully');
|
|
19860
20336
|
this.load(); // re-read so computed/server-stamped values (and a first-read created row's key) come back
|
|
19861
20337
|
this.configService.refresh(); // every app-side subscriber (nav gating, unit suffixes) sees the change now, not next login
|
|
@@ -19864,18 +20340,35 @@ class AppConfigurationComponent {
|
|
|
19864
20340
|
// Changed: was toast(response.message ...) — the server's raw words for 5 seconds while the page
|
|
19865
20341
|
// still showed every edited value, so it looked saved. 'submit' is exactly right and literally
|
|
19866
20342
|
// 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);
|
|
20343
|
+
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
|
|
20344
|
+
// Added: this page is a PERSISTENT page, not a dialog. Everywhere else in the library a failed submit
|
|
20345
|
+
// leaves a modal open, and the open modal is itself the visible "not saved" signal. Here there is no
|
|
20346
|
+
// modal: once the toast expires the page looks settled while showing values the server refused and the
|
|
20347
|
+
// database does not hold. The banner is that missing signal, and unlike the toast it does not expire.
|
|
20348
|
+
this.saveFailure = {
|
|
20349
|
+
reason: failureClass === 'business'
|
|
20350
|
+
? 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
|
|
20351
|
+
: 'A technical problem stopped the save.',
|
|
20352
|
+
technical: failureClass !== 'business'
|
|
20353
|
+
};
|
|
19868
20354
|
}
|
|
19869
20355
|
},
|
|
19870
|
-
error: () => this.saving = false
|
|
20356
|
+
// Changed: was `error: () => this.saving = false` — a TRANSPORT failure (server down, DNS, CORS, dropped
|
|
20357
|
+
// connection) never reaches the next() handler above, so this page said nothing at all and simply went
|
|
20358
|
+
// un-busy, which is the most convincing "saved" signal of the lot. The interceptor's own network dialog is
|
|
20359
|
+
// dismissible and page-agnostic; the banner is what keeps the statement attached to the values on screen.
|
|
20360
|
+
error: () => {
|
|
20361
|
+
this.saving = false;
|
|
20362
|
+
this.saveFailure = { reason: 'A technical problem stopped the save.', technical: true };
|
|
20363
|
+
}
|
|
19871
20364
|
});
|
|
19872
20365
|
}
|
|
19873
20366
|
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-
|
|
20367
|
+
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
20368
|
}
|
|
19876
20369
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AppConfigurationComponent, decorators: [{
|
|
19877
20370
|
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-
|
|
20371
|
+
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
20372
|
}] });
|
|
19880
20373
|
|
|
19881
20374
|
// Renders the agent's replies as the markdown they are actually written in.
|
|
@@ -21802,11 +22295,11 @@ class NavMenuComponent {
|
|
|
21802
22295
|
return !this.isMiniSidebar || this.isMiniHovered;
|
|
21803
22296
|
}
|
|
21804
22297
|
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> {{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;\">© {{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 © {{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" }] }); }
|
|
22298
|
+
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> {{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;\">© {{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 © {{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
22299
|
}
|
|
21807
22300
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: NavMenuComponent, decorators: [{
|
|
21808
22301
|
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> {{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;\">© {{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 © {{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"] }]
|
|
22302
|
+
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> {{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;\">© {{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 © {{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
22303
|
}], 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
22304
|
type: HostListener,
|
|
21812
22305
|
args: ['window:scroll']
|
|
@@ -22761,6 +23254,8 @@ class DetailsDialog {
|
|
|
22761
23254
|
openNestedDetailsDialog(button) {
|
|
22762
23255
|
// Changed: Use token pattern instead of direct DetailsDialog import, propagate nestingLevel
|
|
22763
23256
|
this.dialogService.openDefaultDetailsDialog(button, this.details, this.nestingLevel).subscribe(result => {
|
|
23257
|
+
if (!result)
|
|
23258
|
+
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
23259
|
if (result.action === 'inputChange') {
|
|
22765
23260
|
this.inputChanged(result.change);
|
|
22766
23261
|
}
|
|
@@ -22970,11 +23465,11 @@ class DetailsDialog {
|
|
|
22970
23465
|
}
|
|
22971
23466
|
}
|
|
22972
23467
|
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" }] }); }
|
|
23468
|
+
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
23469
|
}
|
|
22975
23470
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DetailsDialog, decorators: [{
|
|
22976
23471
|
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"] }]
|
|
23472
|
+
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
23473
|
}], ctorParameters: () => [{ type: i1$4.BreakpointObserver }, { type: LoaderService }, { type: DataServiceLib }, { type: MessageService }, { type: i4.MatDialogRef }, { type: DetailsDialogConfig, decorators: [{
|
|
22979
23474
|
type: Inject,
|
|
22980
23475
|
args: [MAT_DIALOG_DATA]
|
|
@@ -23589,7 +24084,7 @@ class TitleActionsComponent {
|
|
|
23589
24084
|
this.actionValues[action.name] = value;
|
|
23590
24085
|
}
|
|
23591
24086
|
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" }] }); }
|
|
24087
|
+
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
24088
|
}
|
|
23594
24089
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TitleActionsComponent, decorators: [{
|
|
23595
24090
|
type: Component,
|
|
@@ -23669,7 +24164,7 @@ class PageComponent {
|
|
|
23669
24164
|
this.dataLoad.emit(x);
|
|
23670
24165
|
}
|
|
23671
24166
|
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"] }] }); }
|
|
24167
|
+
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
24168
|
}
|
|
23674
24169
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PageComponent, decorators: [{
|
|
23675
24170
|
type: Component,
|
|
@@ -24368,7 +24863,7 @@ class TinSpaModule {
|
|
|
24368
24863
|
AttachComponent, ChipsComponent, LoaderComponent, NavMenuComponent, TableComponent, DetailsDialog, FormComponent, FormSkeletonComponent, CamelToWordsPipe, AgentMarkdownPipe, NumberComponent, SearchComponent, ViewerComponent, viewerDialog,
|
|
24369
24864
|
ListDialogComponent,
|
|
24370
24865
|
InvitationsTableComponent,
|
|
24371
|
-
TableHeaderComponent, TableRowComponent, TableActionComponent, InlineCellComponent, // Changed: added InlineCellComponent (inline row editing)
|
|
24866
|
+
TableHeaderComponent, TableRowComponent, TableActionComponent, InlineCellComponent, MonogramComponent, // Changed: added InlineCellComponent (inline row editing), MonogramComponent (monogram column type)
|
|
24372
24867
|
AlertComponent, EmailComponent, PageComponent, SelectCommonComponent, SuffixComponent, SelectContextDirective, SelectLiteComponent,
|
|
24373
24868
|
TextMultiComponent,
|
|
24374
24869
|
SelectMultiComponent, SelectBitwiseComponent,
|
|
@@ -24392,6 +24887,7 @@ class TinSpaModule {
|
|
|
24392
24887
|
DragDropModule // Changed: Added for CDK drag-drop between groups
|
|
24393
24888
|
], exports: [TinSpaComponent,
|
|
24394
24889
|
SpaMatModule,
|
|
24890
|
+
MonogramComponent, // Added: monogram column type — exported so hosts can render a person outside a table too
|
|
24395
24891
|
AlertComponent,
|
|
24396
24892
|
TextComponent,
|
|
24397
24893
|
TextMaskComponent,
|
|
@@ -24473,7 +24969,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
24473
24969
|
AttachComponent, ChipsComponent, LoaderComponent, NavMenuComponent, TableComponent, DetailsDialog, FormComponent, FormSkeletonComponent, CamelToWordsPipe, AgentMarkdownPipe, NumberComponent, SearchComponent, ViewerComponent, viewerDialog,
|
|
24474
24970
|
ListDialogComponent,
|
|
24475
24971
|
InvitationsTableComponent,
|
|
24476
|
-
TableHeaderComponent, TableRowComponent, TableActionComponent, InlineCellComponent, // Changed: added InlineCellComponent (inline row editing)
|
|
24972
|
+
TableHeaderComponent, TableRowComponent, TableActionComponent, InlineCellComponent, MonogramComponent, // Changed: added InlineCellComponent (inline row editing), MonogramComponent (monogram column type)
|
|
24477
24973
|
AlertComponent, EmailComponent, PageComponent, SelectCommonComponent, SuffixComponent, SelectContextDirective, SelectLiteComponent,
|
|
24478
24974
|
TextMultiComponent,
|
|
24479
24975
|
SelectMultiComponent, SelectBitwiseComponent,
|
|
@@ -24501,6 +24997,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
24501
24997
|
exports: [
|
|
24502
24998
|
TinSpaComponent,
|
|
24503
24999
|
SpaMatModule,
|
|
25000
|
+
MonogramComponent, // Added: monogram column type — exported so hosts can render a person outside a table too
|
|
24504
25001
|
AlertComponent,
|
|
24505
25002
|
TextComponent,
|
|
24506
25003
|
TextMaskComponent,
|
|
@@ -25653,7 +26150,7 @@ class RolesComponent {
|
|
|
25653
26150
|
const newRole = { roleName: '' };
|
|
25654
26151
|
// Open add role dialog using the same approach as rename
|
|
25655
26152
|
const dialogRef = this.dialogService.openConfiguredDetailsDialog(this.addRoleButton, newRole, DetailsDialog).subscribe(result => {
|
|
25656
|
-
if (result
|
|
26153
|
+
if (result?.message === 'success') { // Changed: a dismissed role dialog closes with undefined — read through it rather than throwing
|
|
25657
26154
|
this.loadRoles();
|
|
25658
26155
|
}
|
|
25659
26156
|
});
|
|
@@ -25694,7 +26191,7 @@ class RolesComponent {
|
|
|
25694
26191
|
const roleData = { ...role };
|
|
25695
26192
|
// Open rename dialog
|
|
25696
26193
|
const dialogRef = this.dialogService.openConfiguredDetailsDialog(this.renameButton, roleData, DetailsDialog).subscribe(result => {
|
|
25697
|
-
if (result
|
|
26194
|
+
if (result?.message === 'success') { // Changed: a dismissed role dialog closes with undefined — read through it rather than throwing
|
|
25698
26195
|
this.loadRoles();
|
|
25699
26196
|
}
|
|
25700
26197
|
});
|
|
@@ -25795,7 +26292,7 @@ class CreateAccountComponent {
|
|
|
25795
26292
|
this.router.navigate(["home/user/profile"]);
|
|
25796
26293
|
}
|
|
25797
26294
|
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 \n\n <spa-select id=\"cboAuth\" display=\"Authentication Type\" [options]=\"authTypes\" optionDisplay=\"name\" optionValue=\"value\" [(value)]=\"register.authType\" (valueChange)=\"check()\" ></spa-select>\n\n \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 \n\n <spa-select id=\"cboRole\" display=\"Role\" [options]=\"roles\" optionDisplay=\"roleName\" optionValue=\"roleID\" [(value)]=\"register.roleID\"></spa-select>\n\n \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"] }] }); }
|
|
26295
|
+
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 \n\n <spa-select id=\"cboAuth\" display=\"Authentication Type\" [options]=\"authTypes\" optionDisplay=\"name\" optionValue=\"value\" [(value)]=\"register.authType\" (valueChange)=\"check()\" ></spa-select>\n\n \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 \n\n <spa-select id=\"cboRole\" display=\"Role\" [options]=\"roles\" optionDisplay=\"roleName\" optionValue=\"roleID\" [(value)]=\"register.roleID\"></spa-select>\n\n \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
26296
|
}
|
|
25800
26297
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CreateAccountComponent, decorators: [{
|
|
25801
26298
|
type: Component,
|
|
@@ -29271,7 +29768,7 @@ class OnboardingComponent {
|
|
|
29271
29768
|
this.dataService.Navigate('home');
|
|
29272
29769
|
}
|
|
29273
29770
|
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" }] }); }
|
|
29771
|
+
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
29772
|
}
|
|
29276
29773
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: OnboardingComponent, decorators: [{
|
|
29277
29774
|
type: Component,
|
|
@@ -33079,5 +33576,5 @@ const ALSQUARE_SVG_WHITE = `<svg viewBox="0 0 80 80" fill="none" xmlns="http://w
|
|
|
33079
33576
|
* Generated bundle index. Do not edit.
|
|
33080
33577
|
*/
|
|
33081
33578
|
|
|
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 };
|
|
33579
|
+
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
33580
|
//# sourceMappingURL=tin-spa.mjs.map
|