tin-spa 20.14.26 → 20.14.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/tin-spa.mjs +553 -94
- package/fesm2022/tin-spa.mjs.map +1 -1
- package/index.d.ts +70 -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 = {};
|
|
@@ -4443,7 +4454,7 @@ class DataServiceLib {
|
|
|
4443
4454
|
]
|
|
4444
4455
|
},
|
|
4445
4456
|
{ name: 'comments', type: 'text', },
|
|
4446
|
-
{ name: 'createdByName', type: '
|
|
4457
|
+
{ name: 'createdByName', type: 'monogram', alias: 'By' }, // Changed: monogram — the row still carries the full name, so filter/sort are untouched
|
|
4447
4458
|
],
|
|
4448
4459
|
buttons: [
|
|
4449
4460
|
{ name: 'view', dialog: true }
|
|
@@ -4551,7 +4562,7 @@ class DataServiceLib {
|
|
|
4551
4562
|
collapseButtons: true,
|
|
4552
4563
|
columns: [
|
|
4553
4564
|
{ name: 'createdDate', type: 'datetime', alias: 'Date' },
|
|
4554
|
-
{ name: 'createdByName', alias: 'Request By', type: '
|
|
4565
|
+
{ name: 'createdByName', alias: 'Request By', type: 'monogram' }, // Changed: monogram
|
|
4555
4566
|
{ name: 'message', type: 'text' },
|
|
4556
4567
|
{
|
|
4557
4568
|
name: 'typeName', alias: 'Type', type: 'chip',
|
|
@@ -4577,7 +4588,7 @@ class DataServiceLib {
|
|
|
4577
4588
|
{ name: 'schedule', color: '#FFC107', condition: x => x.status === 'Pending', tip: 'Pending' }
|
|
4578
4589
|
]
|
|
4579
4590
|
},
|
|
4580
|
-
{ name: 'updatedByName', alias: 'Actioned By', type: '
|
|
4591
|
+
{ name: 'updatedByName', alias: 'Actioned By', type: 'monogram' }, // Changed: monogram
|
|
4581
4592
|
{ name: 'updatedDateDisplay', type: 'datetime', alias: 'Actioned Date' },
|
|
4582
4593
|
],
|
|
4583
4594
|
buttons: [
|
|
@@ -5358,6 +5369,9 @@ class messageDialog {
|
|
|
5358
5369
|
this._messageSubject = this.data.subject;
|
|
5359
5370
|
this._messageDetails = this.data.details;
|
|
5360
5371
|
this._messageReference = this.data.reference; // Added: optional backend correlation code for this failure
|
|
5372
|
+
this._confirmLabel = this.data.confirmLabel; // Added: optional caller-supplied action label for the affirmative button
|
|
5373
|
+
this._cancelLabel = this.data.cancelLabel; // Added: optional caller-supplied label for the dismissive button
|
|
5374
|
+
this._okLabel = this.data.okLabel; // Added: optional caller-supplied label for the acknowledge button
|
|
5361
5375
|
// Added: resolve the style once, at open time. The panel class carries surface-level styling (radius,
|
|
5362
5376
|
// shadow) that component CSS cannot reach, because the Material surface is an ancestor of this view.
|
|
5363
5377
|
this.modern = !!this.dataService?.modernDialogs;
|
|
@@ -5399,6 +5413,12 @@ class messageDialog {
|
|
|
5399
5413
|
return ref.length ? ref : null;
|
|
5400
5414
|
}
|
|
5401
5415
|
get isConfirm() { return this.messageType === 'confirm'; }
|
|
5416
|
+
label(value, fallback) {
|
|
5417
|
+
return (value && String(value).trim()) ? String(value).trim() : fallback;
|
|
5418
|
+
}
|
|
5419
|
+
get confirmLabel() { return this.label(this._confirmLabel, 'Yes'); }
|
|
5420
|
+
get cancelLabel() { return this.label(this._cancelLabel, 'No'); }
|
|
5421
|
+
get okLabel() { return this.label(this._okLabel, 'OK'); }
|
|
5402
5422
|
// Added: heading for the CLASSIC branch. The original template hardcoded one word per type and only rendered
|
|
5403
5423
|
// the caller's subject for 'info'; here the subject still wins when supplied, so the "No connection" fix
|
|
5404
5424
|
// survives in classic mode too — this restores the old LOOK, not the old bug. The per-type words are the
|
|
@@ -5415,11 +5435,11 @@ class messageDialog {
|
|
|
5415
5435
|
this.dialogRef.close(resp);
|
|
5416
5436
|
}
|
|
5417
5437
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: messageDialog, deps: [{ token: i4.MatDialogRef }, { token: MAT_DIALOG_DATA }, { token: DataServiceLib, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
5418
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: messageDialog, isStandalone: false, selector: "lib-app-message", ngImport: i0, template: "<!-- Changed: two presentations behind one component. `modern` (appConfig.dialogStyle === 'modern') renders the\n structured layout; anything else renders the long-standing look, so an app that never opts in still gets a\n familiar dialog. Changed: classic is a SUPPORTED escape hatch, not frozen legacy \u2014 it is expected to render\n the same information as modern, and its three rendering bugs were repaired on 2026-08-06. Button ids\n (btnYes / btnNo / btnOK) are identical in BOTH branches \u2014 existing E2E specs select on them and must not\n care which style is active. -->\n\n<ng-container *ngIf=\"modern; else classic\">\n\n <!-- Rebuilt from a hardcoded per-type heading plus one undifferentiated paragraph into a structured layout \u2014\n icon chip, real title, lead paragraph, supporting detail. The type drives colour via a single class on\n the root, so adding a type is one entry in the component rather than three template branches. -->\n <div class=\"tin-msg\" [ngClass]=\"'tin-msg--' + messageType\">\n\n <div class=\"tin-msg__head\">\n <!-- The icon carries the type at a glance. It sits in a soft tinted disc rather than on a saturated\n banner: the intent is to inform, not to alarm someone who has usually done nothing wrong. -->\n <div class=\"tin-msg__icon\">\n <mat-icon>{{ iconName }}</mat-icon>\n </div>\n <h2 class=\"tin-msg__title\">{{ title }}</h2>\n </div>\n\n <mat-dialog-content class=\"tin-msg__body\">\n <!-- First paragraph is the lead (what happened); the rest is supporting detail (what to do next).\n Splitting on blank lines is what gives the eye somewhere to land \u2014 the old single run of text is\n why nobody read past the first line. -->\n <p *ngFor=\"let p of paragraphs; let first = first\" class=\"tin-msg__p\" [class.tin-msg__p--lead]=\"first\">{{ p }}</p>\n\n <!-- The backend's correlation code. Deliberately the quietest thing in the dialog \u2014 the user does not\n need to act on it, they only need to be able to read it out if they call support, at which point it\n turns a \"something broke this morning\" report into one log row. Hidden entirely when absent. -->\n <p class=\"tin-msg__ref\" *ngIf=\"reference\">If you contact your administrator, quote reference <span class=\"tin-msg__ref-code\">{{ reference }}</span>.</p>\n </mat-dialog-content>\n\n <mat-dialog-actions class=\"tin-msg__actions\">\n <!-- Changed: labels reverted to \"No\" / \"Yes\" (owner's call, 2026-08-06). The redesign had shipped them as\n \"Cancel\" / \"Yes, continue\"; these are the most-clicked buttons in the app and the original wording is\n what everybody here reads. Text only \u2014 the ids btnNo / btnYes are untouched and identical to the\n classic branch, so every E2E selector (#btnYes) and every caller is unaffected. -->\n <ng-container *ngIf=\"isConfirm; else modernAcknowledge\">\n <button id=\"btnNo\" mat-button class=\"tin-msg__btn-quiet\" (click)=\"response('no')\">No</button>\n <button id=\"btnYes\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('yes')\" cdkFocusInitial>Yes</button>\n </ng-container>\n <ng-template #modernAcknowledge>\n <button id=\"btnOK\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('ok')\" cdkFocusInitial>OK</button>\n </ng-template>\n </mat-dialog-actions>\n\n </div>\n\n</ng-container>\n\n\n<!-- Classic: the original LOOK, kept deliberately as the escape hatch (owner's call, 2026-08-06 \u2014 retire was\n recommended and declined). It is the original markup plus FIXES, never style changes, because reverting a\n bug alongside a look would be a regression. It renders the same INFORMATION as the modern branch, in its\n own visual language:\n 1. the caller's subject is used as the heading when one was supplied. The old template hardcoded the\n heading per type and rendered the subject for 'info' only, which is why a friendly title like\n \"No connection\" showed up as the bare word \"Error\".\n 2. 'warning' and 'success' are handled. They did not exist when this markup was written, so they would\n otherwise fall through every branch and render an empty dialog.\n 3. 'error' now has an icon; the reference line is no longer trapped in the error-only branch; and the\n body renders real paragraphs instead of one collapsed run of text.\n \u26A0\uFE0F Anything added to the modern branch above has to be considered here too \u2014 that omission is exactly how\n the three bugs in 3. got here. -->\n\n<ng-template #classic>\n <div class=\"tin-msg-classic\">\n\n <h2>\n <div class=\"tin-between\">\n <mat-label>{{ classicHeading }}</mat-label>\n <!-- Added: the missing 'error' icon. Every other type had one; error \u2014 the type that most needs a cue \u2014\n rendered its heading beside empty flex space. Colour matches the modern branch's error accent\n (#c62828) so the two presentations agree on what \"error\" looks like. -->\n <mat-icon *ngIf=\"messageType=='error'\" style=\"color: #c62828;\">error</mat-icon>\n <mat-icon *ngIf=\"messageType=='confirm'\">question_mark</mat-icon>\n <mat-icon *ngIf=\"messageType=='info'\" style=\"color: steelblue;\">info</mat-icon>\n <mat-icon *ngIf=\"messageType=='warning'\" style=\"color: #ef6c00;\">warning</mat-icon>\n <mat-icon *ngIf=\"messageType=='success'\" style=\"color: #2e7d32;\">check_circle</mat-icon>\n </div>\n </h2>\n\n <mat-dialog-content>\n\n <!-- Changed: was `{{ _messageDetails }}` rendered raw, so the blank-line-separated paragraphs the messages\n are actually written in (api-error.service.ts) collapsed into one undifferentiated run of text. That\n collapse is the original complaint that started WS-7 and it was still live here. Now renders the same\n `paragraphs` split the modern branch uses \u2014 same information, classic's own plain markup. -->\n <div *ngIf=\"messageType=='error'\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <div *ngIf=\"messageType!='error'\" class=\"alert alert-secondary\" role=\"alert\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <!-- Changed: lifted OUT of the error-only branch above. The reference is a correlation code, not an\n error-specific field \u2014 a warning or info that carried one used to drop it silently. -->\n <div class=\"tin-msg-classic__ref\" *ngIf=\"reference\">Reference: {{ reference }}</div>\n\n </mat-dialog-content>\n\n <mat-dialog-actions>\n\n <button id=\"btnYes\" mat-stroked-button style=\"color: green;\" *ngIf=\"isConfirm\" (click)=\"response('yes')\" cdkFocusInitial>Yes</button>\n\n <button id=\"btnNo\" mat-stroked-button style=\"color: red;\" *ngIf=\"isConfirm\" (click)=\"response('no')\">No</button>\n\n <button id=\"btnOK\" mat-stroked-button *ngIf=\"!isConfirm\" color=\"primary\" (click)=\"response('ok')\" cdkFocusInitial>OK</button>\n\n </mat-dialog-actions>\n\n </div>\n</ng-template>\n", styles: [":host{display:block}.tin-msg-classic h2{margin:0!important;padding:16px 16px 0!important;font-size:20px!important;font-weight:500!important;line-height:normal!important}.tin-msg-classic .tin-between{display:flex;justify-content:space-between;align-items:center;margin:0!important;padding:0!important}.tin-msg-classic__p{margin:0 0 10px}.tin-msg-classic__p:last-child{margin-bottom:0}.tin-msg-classic__ref{margin-top:10px;font-size:12px;color:#0000008c;letter-spacing:.04em}.tin-msg{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1);--tin-title: rgba(0, 0, 0, .87);--tin-body: rgba(0, 0, 0, .6);position:relative;padding:4px 4px 0}.tin-msg:before{content:\"\";position:absolute;top:0;left:0;right:0;height:3px;background:var(--tin-accent);border-radius:3px 3px 0 0}.tin-msg--error{--tin-accent: #c62828;--tin-accent-tint: rgba(198, 40, 40, .1)}.tin-msg--warning{--tin-accent: #ef6c00;--tin-accent-tint: rgba(239, 108, 0, .12)}.tin-msg--info{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1)}.tin-msg--success{--tin-accent: #2e7d32;--tin-accent-tint: rgba(46, 125, 50, .12)}.tin-msg--confirm{--tin-accent: #5e35b1;--tin-accent-tint: rgba(94, 53, 177, .1)}.tin-msg__head{display:flex;align-items:center;gap:14px;padding:20px 20px 8px}.tin-msg__icon{flex:0 0 auto;width:40px;height:40px;border-radius:50%;background:var(--tin-accent-tint);display:inline-flex;align-items:center;justify-content:center}.tin-msg__icon .mat-icon,.tin-msg__icon mat-icon{color:var(--tin-accent);font-size:22px;width:22px;height:22px;line-height:22px}.tin-msg__title{margin:0!important;padding:0!important;font-size:18px!important;font-weight:600!important;line-height:1.3!important;letter-spacing:-.1px;color:var(--tin-title)}.tin-msg__body{padding:0 20px 4px 74px!important;max-height:60vh}.tin-msg__p{margin:0 0 12px;font-size:13.5px;line-height:1.6;color:var(--tin-body)}.tin-msg__p--lead{font-size:14.5px;font-weight:500;color:var(--tin-title)}.tin-msg__p:last-child{margin-bottom:0}.tin-msg__ref{margin:14px 0 0;padding-top:10px;border-top:1px solid rgba(0,0,0,.08);font-size:12px;line-height:1.5;color:var(--tin-muted, rgba(0, 0, 0, .55))}.tin-msg__ref-code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;letter-spacing:.06em;color:var(--tin-title, rgba(0, 0, 0, .8));white-space:nowrap;-webkit-user-select:all;user-select:all}.tin-msg__actions{display:flex;justify-content:flex-end;gap:8px;padding:12px 20px 16px!important;margin:0!important;min-height:0!important}.tin-msg__btn-primary{background:var(--tin-accent)!important;color:#fff!important;min-width:88px;font-weight:500;letter-spacing:.2px}.tin-msg__btn-quiet{color:var(--tin-body)!important;font-weight:500}.tin-msg__icon{animation:tin-msg-pop .22s cubic-bezier(.2,.7,.3,1) both}@keyframes tin-msg-pop{0%{transform:scale(.82);opacity:0}to{transform:scale(1);opacity:1}}@media (prefers-reduced-motion: reduce){.tin-msg__icon{animation:none}}@media (max-width: 480px){.tin-msg__head{padding:16px 16px 8px;gap:12px}.tin-msg__body{padding:0 16px 4px!important}.tin-msg__actions{padding:12px 16px 14px!important}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i4.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i4.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }] }); }
|
|
5438
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: messageDialog, isStandalone: false, selector: "lib-app-message", ngImport: i0, template: "<!-- Changed: two presentations behind one component. `modern` (appConfig.dialogStyle === 'modern') renders the\n structured layout; anything else renders the long-standing look, so an app that never opts in still gets a\n familiar dialog. Changed: classic is a SUPPORTED escape hatch, not frozen legacy \u2014 it is expected to render\n the same information as modern, and its three rendering bugs were repaired on 2026-08-06. Button ids\n (btnYes / btnNo / btnOK) are identical in BOTH branches \u2014 existing E2E specs select on them and must not\n care which style is active. -->\n\n<ng-container *ngIf=\"modern; else classic\">\n\n <!-- Rebuilt from a hardcoded per-type heading plus one undifferentiated paragraph into a structured layout \u2014\n icon chip, real title, lead paragraph, supporting detail. The type drives colour via a single class on\n the root, so adding a type is one entry in the component rather than three template branches. -->\n <div class=\"tin-msg\" [ngClass]=\"'tin-msg--' + messageType\">\n\n <div class=\"tin-msg__head\">\n <!-- The icon carries the type at a glance. It sits in a soft tinted disc rather than on a saturated\n banner: the intent is to inform, not to alarm someone who has usually done nothing wrong. -->\n <div class=\"tin-msg__icon\">\n <mat-icon>{{ iconName }}</mat-icon>\n </div>\n <h2 class=\"tin-msg__title\">{{ title }}</h2>\n </div>\n\n <mat-dialog-content class=\"tin-msg__body\">\n <!-- First paragraph is the lead (what happened); the rest is supporting detail (what to do next).\n Splitting on blank lines is what gives the eye somewhere to land \u2014 the old single run of text is\n why nobody read past the first line. -->\n <p *ngFor=\"let p of paragraphs; let first = first\" class=\"tin-msg__p\" [class.tin-msg__p--lead]=\"first\">{{ p }}</p>\n\n <!-- The backend's correlation code. Deliberately the quietest thing in the dialog \u2014 the user does not\n need to act on it, they only need to be able to read it out if they call support, at which point it\n turns a \"something broke this morning\" report into one log row. Hidden entirely when absent. -->\n <p class=\"tin-msg__ref\" *ngIf=\"reference\">If you contact your administrator, quote reference <span class=\"tin-msg__ref-code\">{{ reference }}</span>.</p>\n </mat-dialog-content>\n\n <mat-dialog-actions class=\"tin-msg__actions\">\n <!-- Changed: labels reverted to \"No\" / \"Yes\" (owner's call, 2026-08-06). The redesign had shipped them as\n \"Cancel\" / \"Yes, continue\"; these are the most-clicked buttons in the app and the original wording is\n what everybody here reads. Text only \u2014 the ids btnNo / btnYes are untouched and identical to the\n classic branch, so every E2E selector (#btnYes) and every caller is unaffected.\n Changed: the literal text is now {{ confirmLabel }} / {{ cancelLabel }} / {{ okLabel }}, which STILL\n render \"Yes\" / \"No\" / \"OK\" unless the caller passes a label. A caller that knows it is confirming a\n destructive action can name it (\"Delete\") so it no longer reads identically to a harmless one. -->\n <ng-container *ngIf=\"isConfirm; else modernAcknowledge\">\n <button id=\"btnNo\" mat-button class=\"tin-msg__btn-quiet\" (click)=\"response('no')\">{{ cancelLabel }}</button>\n <button id=\"btnYes\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('yes')\" cdkFocusInitial>{{ confirmLabel }}</button>\n </ng-container>\n <ng-template #modernAcknowledge>\n <button id=\"btnOK\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('ok')\" cdkFocusInitial>{{ okLabel }}</button>\n </ng-template>\n </mat-dialog-actions>\n\n </div>\n\n</ng-container>\n\n\n<!-- Classic: the original LOOK, kept deliberately as the escape hatch (owner's call, 2026-08-06 \u2014 retire was\n recommended and declined). It is the original markup plus FIXES, never style changes, because reverting a\n bug alongside a look would be a regression. It renders the same INFORMATION as the modern branch, in its\n own visual language:\n 1. the caller's subject is used as the heading when one was supplied. The old template hardcoded the\n heading per type and rendered the subject for 'info' only, which is why a friendly title like\n \"No connection\" showed up as the bare word \"Error\".\n 2. 'warning' and 'success' are handled. They did not exist when this markup was written, so they would\n otherwise fall through every branch and render an empty dialog.\n 3. 'error' now has an icon; the reference line is no longer trapped in the error-only branch; and the\n body renders real paragraphs instead of one collapsed run of text.\n \u26A0\uFE0F Anything added to the modern branch above has to be considered here too \u2014 that omission is exactly how\n the three bugs in 3. got here. -->\n\n<ng-template #classic>\n <div class=\"tin-msg-classic\">\n\n <h2>\n <div class=\"tin-between\">\n <mat-label>{{ classicHeading }}</mat-label>\n <!-- Added: the missing 'error' icon. Every other type had one; error \u2014 the type that most needs a cue \u2014\n rendered its heading beside empty flex space. Colour matches the modern branch's error accent\n (#c62828) so the two presentations agree on what \"error\" looks like. -->\n <mat-icon *ngIf=\"messageType=='error'\" style=\"color: #c62828;\">error</mat-icon>\n <mat-icon *ngIf=\"messageType=='confirm'\">question_mark</mat-icon>\n <mat-icon *ngIf=\"messageType=='info'\" style=\"color: steelblue;\">info</mat-icon>\n <mat-icon *ngIf=\"messageType=='warning'\" style=\"color: #ef6c00;\">warning</mat-icon>\n <mat-icon *ngIf=\"messageType=='success'\" style=\"color: #2e7d32;\">check_circle</mat-icon>\n </div>\n </h2>\n\n <mat-dialog-content>\n\n <!-- Changed: was `{{ _messageDetails }}` rendered raw, so the blank-line-separated paragraphs the messages\n are actually written in (api-error.service.ts) collapsed into one undifferentiated run of text. That\n collapse is the original complaint that started WS-7 and it was still live here. Now renders the same\n `paragraphs` split the modern branch uses \u2014 same information, classic's own plain markup. -->\n <div *ngIf=\"messageType=='error'\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <div *ngIf=\"messageType!='error'\" class=\"alert alert-secondary\" role=\"alert\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <!-- Changed: lifted OUT of the error-only branch above. The reference is a correlation code, not an\n error-specific field \u2014 a warning or info that carried one used to drop it silently. -->\n <div class=\"tin-msg-classic__ref\" *ngIf=\"reference\">Reference: {{ reference }}</div>\n\n </mat-dialog-content>\n\n <mat-dialog-actions>\n\n <!-- Changed: same caller-supplied labels as the modern branch, defaulting identically to \"Yes\" / \"No\" /\n \"OK\". Classic is expected to render the same INFORMATION as modern in its own visual language, so a\n caller that names its action must be honoured here too \u2014 otherwise the next person to pass a label\n would silently get \"Yes\" on every untracked consumer still running classic. Colours and ids untouched. -->\n <button id=\"btnYes\" mat-stroked-button style=\"color: green;\" *ngIf=\"isConfirm\" (click)=\"response('yes')\" cdkFocusInitial>{{ confirmLabel }}</button>\n\n <button id=\"btnNo\" mat-stroked-button style=\"color: red;\" *ngIf=\"isConfirm\" (click)=\"response('no')\">{{ cancelLabel }}</button>\n\n <button id=\"btnOK\" mat-stroked-button *ngIf=\"!isConfirm\" color=\"primary\" (click)=\"response('ok')\" cdkFocusInitial>{{ okLabel }}</button>\n\n </mat-dialog-actions>\n\n </div>\n</ng-template>\n", styles: [":host{display:block}.tin-msg-classic h2{margin:0!important;padding:16px 16px 0!important;font-size:20px!important;font-weight:500!important;line-height:normal!important}.tin-msg-classic .tin-between{display:flex;justify-content:space-between;align-items:center;margin:0!important;padding:0!important}.tin-msg-classic__p{margin:0 0 10px}.tin-msg-classic__p:last-child{margin-bottom:0}.tin-msg-classic__ref{margin-top:10px;font-size:12px;color:#0000008c;letter-spacing:.04em}.tin-msg{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1);--tin-title: rgba(0, 0, 0, .87);--tin-body: rgba(0, 0, 0, .6);position:relative;padding:4px 4px 0}.tin-msg:before{content:\"\";position:absolute;top:0;left:0;right:0;height:3px;background:var(--tin-accent);border-radius:3px 3px 0 0}.tin-msg--error{--tin-accent: #c62828;--tin-accent-tint: rgba(198, 40, 40, .1)}.tin-msg--warning{--tin-accent: #ef6c00;--tin-accent-tint: rgba(239, 108, 0, .12)}.tin-msg--info{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1)}.tin-msg--success{--tin-accent: #2e7d32;--tin-accent-tint: rgba(46, 125, 50, .12)}.tin-msg--confirm{--tin-accent: #5e35b1;--tin-accent-tint: rgba(94, 53, 177, .1)}.tin-msg__head{display:flex;align-items:center;gap:14px;padding:20px 20px 8px}.tin-msg__icon{flex:0 0 auto;width:40px;height:40px;border-radius:50%;background:var(--tin-accent-tint);display:inline-flex;align-items:center;justify-content:center}.tin-msg__icon .mat-icon,.tin-msg__icon mat-icon{color:var(--tin-accent);font-size:22px;width:22px;height:22px;line-height:22px}.tin-msg__title{margin:0!important;padding:0!important;font-size:18px!important;font-weight:600!important;line-height:1.3!important;letter-spacing:-.1px;color:var(--tin-title)}.tin-msg__body{padding:0 20px 4px 74px!important;max-height:60vh}.tin-msg__p{margin:0 0 12px;font-size:13.5px;line-height:1.6;color:var(--tin-body)}.tin-msg__p--lead{font-size:14.5px;font-weight:500;color:var(--tin-title)}.tin-msg__p:last-child{margin-bottom:0}.tin-msg__ref{margin:14px 0 0;padding-top:10px;border-top:1px solid rgba(0,0,0,.08);font-size:12px;line-height:1.5;color:var(--tin-muted, rgba(0, 0, 0, .55))}.tin-msg__ref-code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;letter-spacing:.06em;color:var(--tin-title, rgba(0, 0, 0, .8));white-space:nowrap;-webkit-user-select:all;user-select:all}.tin-msg__actions{display:flex;justify-content:flex-end;gap:8px;padding:12px 20px 16px!important;margin:0!important;min-height:0!important}.tin-msg__btn-primary{background:var(--tin-accent)!important;color:#fff!important;min-width:88px;font-weight:500;letter-spacing:.2px}.tin-msg__btn-quiet{color:var(--tin-body)!important;font-weight:500}.tin-msg__icon{animation:tin-msg-pop .22s cubic-bezier(.2,.7,.3,1) both}@keyframes tin-msg-pop{0%{transform:scale(.82);opacity:0}to{transform:scale(1);opacity:1}}@media (prefers-reduced-motion: reduce){.tin-msg__icon{animation:none}}@media (max-width: 480px){.tin-msg__head{padding:16px 16px 8px;gap:12px}.tin-msg__body{padding:0 16px 4px!important}.tin-msg__actions{padding:12px 16px 14px!important}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i4.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i4.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }] }); }
|
|
5419
5439
|
}
|
|
5420
5440
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: messageDialog, decorators: [{
|
|
5421
5441
|
type: Component,
|
|
5422
|
-
args: [{ selector: 'lib-app-message', standalone: false, template: "<!-- Changed: two presentations behind one component. `modern` (appConfig.dialogStyle === 'modern') renders the\n structured layout; anything else renders the long-standing look, so an app that never opts in still gets a\n familiar dialog. Changed: classic is a SUPPORTED escape hatch, not frozen legacy \u2014 it is expected to render\n the same information as modern, and its three rendering bugs were repaired on 2026-08-06. Button ids\n (btnYes / btnNo / btnOK) are identical in BOTH branches \u2014 existing E2E specs select on them and must not\n care which style is active. -->\n\n<ng-container *ngIf=\"modern; else classic\">\n\n <!-- Rebuilt from a hardcoded per-type heading plus one undifferentiated paragraph into a structured layout \u2014\n icon chip, real title, lead paragraph, supporting detail. The type drives colour via a single class on\n the root, so adding a type is one entry in the component rather than three template branches. -->\n <div class=\"tin-msg\" [ngClass]=\"'tin-msg--' + messageType\">\n\n <div class=\"tin-msg__head\">\n <!-- The icon carries the type at a glance. It sits in a soft tinted disc rather than on a saturated\n banner: the intent is to inform, not to alarm someone who has usually done nothing wrong. -->\n <div class=\"tin-msg__icon\">\n <mat-icon>{{ iconName }}</mat-icon>\n </div>\n <h2 class=\"tin-msg__title\">{{ title }}</h2>\n </div>\n\n <mat-dialog-content class=\"tin-msg__body\">\n <!-- First paragraph is the lead (what happened); the rest is supporting detail (what to do next).\n Splitting on blank lines is what gives the eye somewhere to land \u2014 the old single run of text is\n why nobody read past the first line. -->\n <p *ngFor=\"let p of paragraphs; let first = first\" class=\"tin-msg__p\" [class.tin-msg__p--lead]=\"first\">{{ p }}</p>\n\n <!-- The backend's correlation code. Deliberately the quietest thing in the dialog \u2014 the user does not\n need to act on it, they only need to be able to read it out if they call support, at which point it\n turns a \"something broke this morning\" report into one log row. Hidden entirely when absent. -->\n <p class=\"tin-msg__ref\" *ngIf=\"reference\">If you contact your administrator, quote reference <span class=\"tin-msg__ref-code\">{{ reference }}</span>.</p>\n </mat-dialog-content>\n\n <mat-dialog-actions class=\"tin-msg__actions\">\n <!-- Changed: labels reverted to \"No\" / \"Yes\" (owner's call, 2026-08-06). The redesign had shipped them as\n \"Cancel\" / \"Yes, continue\"; these are the most-clicked buttons in the app and the original wording is\n what everybody here reads. Text only \u2014 the ids btnNo / btnYes are untouched and identical to the\n classic branch, so every E2E selector (#btnYes) and every caller is unaffected. -->\n <ng-container *ngIf=\"isConfirm; else modernAcknowledge\">\n <button id=\"btnNo\" mat-button class=\"tin-msg__btn-quiet\" (click)=\"response('no')\">No</button>\n <button id=\"btnYes\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('yes')\" cdkFocusInitial>Yes</button>\n </ng-container>\n <ng-template #modernAcknowledge>\n <button id=\"btnOK\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('ok')\" cdkFocusInitial>OK</button>\n </ng-template>\n </mat-dialog-actions>\n\n </div>\n\n</ng-container>\n\n\n<!-- Classic: the original LOOK, kept deliberately as the escape hatch (owner's call, 2026-08-06 \u2014 retire was\n recommended and declined). It is the original markup plus FIXES, never style changes, because reverting a\n bug alongside a look would be a regression. It renders the same INFORMATION as the modern branch, in its\n own visual language:\n 1. the caller's subject is used as the heading when one was supplied. The old template hardcoded the\n heading per type and rendered the subject for 'info' only, which is why a friendly title like\n \"No connection\" showed up as the bare word \"Error\".\n 2. 'warning' and 'success' are handled. They did not exist when this markup was written, so they would\n otherwise fall through every branch and render an empty dialog.\n 3. 'error' now has an icon; the reference line is no longer trapped in the error-only branch; and the\n body renders real paragraphs instead of one collapsed run of text.\n \u26A0\uFE0F Anything added to the modern branch above has to be considered here too \u2014 that omission is exactly how\n the three bugs in 3. got here. -->\n\n<ng-template #classic>\n <div class=\"tin-msg-classic\">\n\n <h2>\n <div class=\"tin-between\">\n <mat-label>{{ classicHeading }}</mat-label>\n <!-- Added: the missing 'error' icon. Every other type had one; error \u2014 the type that most needs a cue \u2014\n rendered its heading beside empty flex space. Colour matches the modern branch's error accent\n (#c62828) so the two presentations agree on what \"error\" looks like. -->\n <mat-icon *ngIf=\"messageType=='error'\" style=\"color: #c62828;\">error</mat-icon>\n <mat-icon *ngIf=\"messageType=='confirm'\">question_mark</mat-icon>\n <mat-icon *ngIf=\"messageType=='info'\" style=\"color: steelblue;\">info</mat-icon>\n <mat-icon *ngIf=\"messageType=='warning'\" style=\"color: #ef6c00;\">warning</mat-icon>\n <mat-icon *ngIf=\"messageType=='success'\" style=\"color: #2e7d32;\">check_circle</mat-icon>\n </div>\n </h2>\n\n <mat-dialog-content>\n\n <!-- Changed: was `{{ _messageDetails }}` rendered raw, so the blank-line-separated paragraphs the messages\n are actually written in (api-error.service.ts) collapsed into one undifferentiated run of text. That\n collapse is the original complaint that started WS-7 and it was still live here. Now renders the same\n `paragraphs` split the modern branch uses \u2014 same information, classic's own plain markup. -->\n <div *ngIf=\"messageType=='error'\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <div *ngIf=\"messageType!='error'\" class=\"alert alert-secondary\" role=\"alert\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <!-- Changed: lifted OUT of the error-only branch above. The reference is a correlation code, not an\n error-specific field \u2014 a warning or info that carried one used to drop it silently. -->\n <div class=\"tin-msg-classic__ref\" *ngIf=\"reference\">Reference: {{ reference }}</div>\n\n </mat-dialog-content>\n\n <mat-dialog-actions>\n\n <button id=\"btnYes\" mat-stroked-button style=\"color: green;\" *ngIf=\"isConfirm\" (click)=\"response('yes')\" cdkFocusInitial>Yes</button>\n\n <button id=\"btnNo\" mat-stroked-button style=\"color: red;\" *ngIf=\"isConfirm\" (click)=\"response('no')\">No</button>\n\n <button id=\"btnOK\" mat-stroked-button *ngIf=\"!isConfirm\" color=\"primary\" (click)=\"response('ok')\" cdkFocusInitial>OK</button>\n\n </mat-dialog-actions>\n\n </div>\n</ng-template>\n", styles: [":host{display:block}.tin-msg-classic h2{margin:0!important;padding:16px 16px 0!important;font-size:20px!important;font-weight:500!important;line-height:normal!important}.tin-msg-classic .tin-between{display:flex;justify-content:space-between;align-items:center;margin:0!important;padding:0!important}.tin-msg-classic__p{margin:0 0 10px}.tin-msg-classic__p:last-child{margin-bottom:0}.tin-msg-classic__ref{margin-top:10px;font-size:12px;color:#0000008c;letter-spacing:.04em}.tin-msg{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1);--tin-title: rgba(0, 0, 0, .87);--tin-body: rgba(0, 0, 0, .6);position:relative;padding:4px 4px 0}.tin-msg:before{content:\"\";position:absolute;top:0;left:0;right:0;height:3px;background:var(--tin-accent);border-radius:3px 3px 0 0}.tin-msg--error{--tin-accent: #c62828;--tin-accent-tint: rgba(198, 40, 40, .1)}.tin-msg--warning{--tin-accent: #ef6c00;--tin-accent-tint: rgba(239, 108, 0, .12)}.tin-msg--info{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1)}.tin-msg--success{--tin-accent: #2e7d32;--tin-accent-tint: rgba(46, 125, 50, .12)}.tin-msg--confirm{--tin-accent: #5e35b1;--tin-accent-tint: rgba(94, 53, 177, .1)}.tin-msg__head{display:flex;align-items:center;gap:14px;padding:20px 20px 8px}.tin-msg__icon{flex:0 0 auto;width:40px;height:40px;border-radius:50%;background:var(--tin-accent-tint);display:inline-flex;align-items:center;justify-content:center}.tin-msg__icon .mat-icon,.tin-msg__icon mat-icon{color:var(--tin-accent);font-size:22px;width:22px;height:22px;line-height:22px}.tin-msg__title{margin:0!important;padding:0!important;font-size:18px!important;font-weight:600!important;line-height:1.3!important;letter-spacing:-.1px;color:var(--tin-title)}.tin-msg__body{padding:0 20px 4px 74px!important;max-height:60vh}.tin-msg__p{margin:0 0 12px;font-size:13.5px;line-height:1.6;color:var(--tin-body)}.tin-msg__p--lead{font-size:14.5px;font-weight:500;color:var(--tin-title)}.tin-msg__p:last-child{margin-bottom:0}.tin-msg__ref{margin:14px 0 0;padding-top:10px;border-top:1px solid rgba(0,0,0,.08);font-size:12px;line-height:1.5;color:var(--tin-muted, rgba(0, 0, 0, .55))}.tin-msg__ref-code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;letter-spacing:.06em;color:var(--tin-title, rgba(0, 0, 0, .8));white-space:nowrap;-webkit-user-select:all;user-select:all}.tin-msg__actions{display:flex;justify-content:flex-end;gap:8px;padding:12px 20px 16px!important;margin:0!important;min-height:0!important}.tin-msg__btn-primary{background:var(--tin-accent)!important;color:#fff!important;min-width:88px;font-weight:500;letter-spacing:.2px}.tin-msg__btn-quiet{color:var(--tin-body)!important;font-weight:500}.tin-msg__icon{animation:tin-msg-pop .22s cubic-bezier(.2,.7,.3,1) both}@keyframes tin-msg-pop{0%{transform:scale(.82);opacity:0}to{transform:scale(1);opacity:1}}@media (prefers-reduced-motion: reduce){.tin-msg__icon{animation:none}}@media (max-width: 480px){.tin-msg__head{padding:16px 16px 8px;gap:12px}.tin-msg__body{padding:0 16px 4px!important}.tin-msg__actions{padding:12px 16px 14px!important}}\n"] }]
|
|
5442
|
+
args: [{ selector: 'lib-app-message', standalone: false, template: "<!-- Changed: two presentations behind one component. `modern` (appConfig.dialogStyle === 'modern') renders the\n structured layout; anything else renders the long-standing look, so an app that never opts in still gets a\n familiar dialog. Changed: classic is a SUPPORTED escape hatch, not frozen legacy \u2014 it is expected to render\n the same information as modern, and its three rendering bugs were repaired on 2026-08-06. Button ids\n (btnYes / btnNo / btnOK) are identical in BOTH branches \u2014 existing E2E specs select on them and must not\n care which style is active. -->\n\n<ng-container *ngIf=\"modern; else classic\">\n\n <!-- Rebuilt from a hardcoded per-type heading plus one undifferentiated paragraph into a structured layout \u2014\n icon chip, real title, lead paragraph, supporting detail. The type drives colour via a single class on\n the root, so adding a type is one entry in the component rather than three template branches. -->\n <div class=\"tin-msg\" [ngClass]=\"'tin-msg--' + messageType\">\n\n <div class=\"tin-msg__head\">\n <!-- The icon carries the type at a glance. It sits in a soft tinted disc rather than on a saturated\n banner: the intent is to inform, not to alarm someone who has usually done nothing wrong. -->\n <div class=\"tin-msg__icon\">\n <mat-icon>{{ iconName }}</mat-icon>\n </div>\n <h2 class=\"tin-msg__title\">{{ title }}</h2>\n </div>\n\n <mat-dialog-content class=\"tin-msg__body\">\n <!-- First paragraph is the lead (what happened); the rest is supporting detail (what to do next).\n Splitting on blank lines is what gives the eye somewhere to land \u2014 the old single run of text is\n why nobody read past the first line. -->\n <p *ngFor=\"let p of paragraphs; let first = first\" class=\"tin-msg__p\" [class.tin-msg__p--lead]=\"first\">{{ p }}</p>\n\n <!-- The backend's correlation code. Deliberately the quietest thing in the dialog \u2014 the user does not\n need to act on it, they only need to be able to read it out if they call support, at which point it\n turns a \"something broke this morning\" report into one log row. Hidden entirely when absent. -->\n <p class=\"tin-msg__ref\" *ngIf=\"reference\">If you contact your administrator, quote reference <span class=\"tin-msg__ref-code\">{{ reference }}</span>.</p>\n </mat-dialog-content>\n\n <mat-dialog-actions class=\"tin-msg__actions\">\n <!-- Changed: labels reverted to \"No\" / \"Yes\" (owner's call, 2026-08-06). The redesign had shipped them as\n \"Cancel\" / \"Yes, continue\"; these are the most-clicked buttons in the app and the original wording is\n what everybody here reads. Text only \u2014 the ids btnNo / btnYes are untouched and identical to the\n classic branch, so every E2E selector (#btnYes) and every caller is unaffected.\n Changed: the literal text is now {{ confirmLabel }} / {{ cancelLabel }} / {{ okLabel }}, which STILL\n render \"Yes\" / \"No\" / \"OK\" unless the caller passes a label. A caller that knows it is confirming a\n destructive action can name it (\"Delete\") so it no longer reads identically to a harmless one. -->\n <ng-container *ngIf=\"isConfirm; else modernAcknowledge\">\n <button id=\"btnNo\" mat-button class=\"tin-msg__btn-quiet\" (click)=\"response('no')\">{{ cancelLabel }}</button>\n <button id=\"btnYes\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('yes')\" cdkFocusInitial>{{ confirmLabel }}</button>\n </ng-container>\n <ng-template #modernAcknowledge>\n <button id=\"btnOK\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('ok')\" cdkFocusInitial>{{ okLabel }}</button>\n </ng-template>\n </mat-dialog-actions>\n\n </div>\n\n</ng-container>\n\n\n<!-- Classic: the original LOOK, kept deliberately as the escape hatch (owner's call, 2026-08-06 \u2014 retire was\n recommended and declined). It is the original markup plus FIXES, never style changes, because reverting a\n bug alongside a look would be a regression. It renders the same INFORMATION as the modern branch, in its\n own visual language:\n 1. the caller's subject is used as the heading when one was supplied. The old template hardcoded the\n heading per type and rendered the subject for 'info' only, which is why a friendly title like\n \"No connection\" showed up as the bare word \"Error\".\n 2. 'warning' and 'success' are handled. They did not exist when this markup was written, so they would\n otherwise fall through every branch and render an empty dialog.\n 3. 'error' now has an icon; the reference line is no longer trapped in the error-only branch; and the\n body renders real paragraphs instead of one collapsed run of text.\n \u26A0\uFE0F Anything added to the modern branch above has to be considered here too \u2014 that omission is exactly how\n the three bugs in 3. got here. -->\n\n<ng-template #classic>\n <div class=\"tin-msg-classic\">\n\n <h2>\n <div class=\"tin-between\">\n <mat-label>{{ classicHeading }}</mat-label>\n <!-- Added: the missing 'error' icon. Every other type had one; error \u2014 the type that most needs a cue \u2014\n rendered its heading beside empty flex space. Colour matches the modern branch's error accent\n (#c62828) so the two presentations agree on what \"error\" looks like. -->\n <mat-icon *ngIf=\"messageType=='error'\" style=\"color: #c62828;\">error</mat-icon>\n <mat-icon *ngIf=\"messageType=='confirm'\">question_mark</mat-icon>\n <mat-icon *ngIf=\"messageType=='info'\" style=\"color: steelblue;\">info</mat-icon>\n <mat-icon *ngIf=\"messageType=='warning'\" style=\"color: #ef6c00;\">warning</mat-icon>\n <mat-icon *ngIf=\"messageType=='success'\" style=\"color: #2e7d32;\">check_circle</mat-icon>\n </div>\n </h2>\n\n <mat-dialog-content>\n\n <!-- Changed: was `{{ _messageDetails }}` rendered raw, so the blank-line-separated paragraphs the messages\n are actually written in (api-error.service.ts) collapsed into one undifferentiated run of text. That\n collapse is the original complaint that started WS-7 and it was still live here. Now renders the same\n `paragraphs` split the modern branch uses \u2014 same information, classic's own plain markup. -->\n <div *ngIf=\"messageType=='error'\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <div *ngIf=\"messageType!='error'\" class=\"alert alert-secondary\" role=\"alert\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <!-- Changed: lifted OUT of the error-only branch above. The reference is a correlation code, not an\n error-specific field \u2014 a warning or info that carried one used to drop it silently. -->\n <div class=\"tin-msg-classic__ref\" *ngIf=\"reference\">Reference: {{ reference }}</div>\n\n </mat-dialog-content>\n\n <mat-dialog-actions>\n\n <!-- Changed: same caller-supplied labels as the modern branch, defaulting identically to \"Yes\" / \"No\" /\n \"OK\". Classic is expected to render the same INFORMATION as modern in its own visual language, so a\n caller that names its action must be honoured here too \u2014 otherwise the next person to pass a label\n would silently get \"Yes\" on every untracked consumer still running classic. Colours and ids untouched. -->\n <button id=\"btnYes\" mat-stroked-button style=\"color: green;\" *ngIf=\"isConfirm\" (click)=\"response('yes')\" cdkFocusInitial>{{ confirmLabel }}</button>\n\n <button id=\"btnNo\" mat-stroked-button style=\"color: red;\" *ngIf=\"isConfirm\" (click)=\"response('no')\">{{ cancelLabel }}</button>\n\n <button id=\"btnOK\" mat-stroked-button *ngIf=\"!isConfirm\" color=\"primary\" (click)=\"response('ok')\" cdkFocusInitial>{{ okLabel }}</button>\n\n </mat-dialog-actions>\n\n </div>\n</ng-template>\n", styles: [":host{display:block}.tin-msg-classic h2{margin:0!important;padding:16px 16px 0!important;font-size:20px!important;font-weight:500!important;line-height:normal!important}.tin-msg-classic .tin-between{display:flex;justify-content:space-between;align-items:center;margin:0!important;padding:0!important}.tin-msg-classic__p{margin:0 0 10px}.tin-msg-classic__p:last-child{margin-bottom:0}.tin-msg-classic__ref{margin-top:10px;font-size:12px;color:#0000008c;letter-spacing:.04em}.tin-msg{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1);--tin-title: rgba(0, 0, 0, .87);--tin-body: rgba(0, 0, 0, .6);position:relative;padding:4px 4px 0}.tin-msg:before{content:\"\";position:absolute;top:0;left:0;right:0;height:3px;background:var(--tin-accent);border-radius:3px 3px 0 0}.tin-msg--error{--tin-accent: #c62828;--tin-accent-tint: rgba(198, 40, 40, .1)}.tin-msg--warning{--tin-accent: #ef6c00;--tin-accent-tint: rgba(239, 108, 0, .12)}.tin-msg--info{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1)}.tin-msg--success{--tin-accent: #2e7d32;--tin-accent-tint: rgba(46, 125, 50, .12)}.tin-msg--confirm{--tin-accent: #5e35b1;--tin-accent-tint: rgba(94, 53, 177, .1)}.tin-msg__head{display:flex;align-items:center;gap:14px;padding:20px 20px 8px}.tin-msg__icon{flex:0 0 auto;width:40px;height:40px;border-radius:50%;background:var(--tin-accent-tint);display:inline-flex;align-items:center;justify-content:center}.tin-msg__icon .mat-icon,.tin-msg__icon mat-icon{color:var(--tin-accent);font-size:22px;width:22px;height:22px;line-height:22px}.tin-msg__title{margin:0!important;padding:0!important;font-size:18px!important;font-weight:600!important;line-height:1.3!important;letter-spacing:-.1px;color:var(--tin-title)}.tin-msg__body{padding:0 20px 4px 74px!important;max-height:60vh}.tin-msg__p{margin:0 0 12px;font-size:13.5px;line-height:1.6;color:var(--tin-body)}.tin-msg__p--lead{font-size:14.5px;font-weight:500;color:var(--tin-title)}.tin-msg__p:last-child{margin-bottom:0}.tin-msg__ref{margin:14px 0 0;padding-top:10px;border-top:1px solid rgba(0,0,0,.08);font-size:12px;line-height:1.5;color:var(--tin-muted, rgba(0, 0, 0, .55))}.tin-msg__ref-code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;letter-spacing:.06em;color:var(--tin-title, rgba(0, 0, 0, .8));white-space:nowrap;-webkit-user-select:all;user-select:all}.tin-msg__actions{display:flex;justify-content:flex-end;gap:8px;padding:12px 20px 16px!important;margin:0!important;min-height:0!important}.tin-msg__btn-primary{background:var(--tin-accent)!important;color:#fff!important;min-width:88px;font-weight:500;letter-spacing:.2px}.tin-msg__btn-quiet{color:var(--tin-body)!important;font-weight:500}.tin-msg__icon{animation:tin-msg-pop .22s cubic-bezier(.2,.7,.3,1) both}@keyframes tin-msg-pop{0%{transform:scale(.82);opacity:0}to{transform:scale(1);opacity:1}}@media (prefers-reduced-motion: reduce){.tin-msg__icon{animation:none}}@media (max-width: 480px){.tin-msg__head{padding:16px 16px 8px;gap:12px}.tin-msg__body{padding:0 16px 4px!important}.tin-msg__actions{padding:12px 16px 14px!important}}\n"] }]
|
|
5423
5443
|
}], ctorParameters: () => [{ type: i4.MatDialogRef }, { type: undefined, decorators: [{
|
|
5424
5444
|
type: Inject,
|
|
5425
5445
|
args: [MAT_DIALOG_DATA]
|
|
@@ -5492,13 +5512,20 @@ class MessageService {
|
|
|
5492
5512
|
data: { type, subject, details },
|
|
5493
5513
|
});
|
|
5494
5514
|
}
|
|
5495
|
-
|
|
5515
|
+
// Changed: optional `confirmLabel` / `cancelLabel`, appended last so all 23 existing call sites are unaffected
|
|
5516
|
+
// and keep rendering "Yes" / "No". Supply confirmLabel to name the action being confirmed — confirm('Delete this
|
|
5517
|
+
// shelter?', 'Delete') — so a destructive prompt stops reading identically to a harmless one. Only the caller
|
|
5518
|
+
// knows the intent; the dialog is handed a sentence.
|
|
5519
|
+
// ⚠️ Do NOT change the defaults to make every confirm read better: piglet-spa (38) and shift-spa (13) select
|
|
5520
|
+
// these buttons by their literal text via getByRole('button', { name: 'Yes' }), and the owner reverted a
|
|
5521
|
+
// hardcoded relabel on 2026-08-06. Opt in per call site.
|
|
5522
|
+
confirm(msg, confirmLabel, cancelLabel) {
|
|
5496
5523
|
let type = "confirm";
|
|
5497
5524
|
let subject = "";
|
|
5498
5525
|
let details = msg;
|
|
5499
5526
|
const dialogRef = this.dialog.open(messageDialog, {
|
|
5500
5527
|
width: "400px",
|
|
5501
|
-
data: { type, subject, details },
|
|
5528
|
+
data: { type, subject, details, confirmLabel, cancelLabel },
|
|
5502
5529
|
});
|
|
5503
5530
|
return dialogRef.afterClosed().pipe(mergeMap((result) => {
|
|
5504
5531
|
return of(result);
|
|
@@ -6365,7 +6392,7 @@ class InventoryService {
|
|
|
6365
6392
|
minColumns: ['requisitionNumber', 'requesterName', 'requestDate'],
|
|
6366
6393
|
columns: [
|
|
6367
6394
|
{ name: 'requisitionNumber', type: 'text', alias: 'Requisition #' },
|
|
6368
|
-
{ name: 'requesterName', type: '
|
|
6395
|
+
{ name: 'requesterName', type: 'monogram', alias: 'Requested By' }, // Changed: monogram — the row still carries the full name, so filter/sort are untouched
|
|
6369
6396
|
{ name: 'departmentName', type: 'text', alias: 'Department' },
|
|
6370
6397
|
{ name: 'requestDate', type: 'date', alias: 'Request Date' },
|
|
6371
6398
|
{ name: 'issuedDate', type: 'date', alias: 'Issued Date' },
|
|
@@ -6931,31 +6958,39 @@ class AccountingService {
|
|
|
6931
6958
|
formConfig: this.invoiceItemsFormConfig // Changed: Reference extracted form config
|
|
6932
6959
|
};
|
|
6933
6960
|
// Invoice action buttons
|
|
6934
|
-
|
|
6961
|
+
// Changed: the hand-rolled cap53 gate is now declared, not written. Every custom action is floored at Edit on
|
|
6962
|
+
// the controller's own capability (BaseController.CustomActionAccess), and InvoicesController raises
|
|
6963
|
+
// return/discard/write-off/provision to Full (InvoicesController.cs:540-546). capAccountingInvoices IS cap53,
|
|
6964
|
+
// which is BOTH InvoicesController.CapabilityName and InvoicePaymentsController's — so these read the same
|
|
6965
|
+
// column the server reads. `visible` is back to being about the ROW; the capability is about the ROLE.
|
|
6966
|
+
// Changed: `record-payment` opens a create dialog posting to invoicepayments?action=create, and BaseController
|
|
6967
|
+
// demands RoleAccess.Create for "create" (BaseController.cs:751) — NOT the Edit floor. It was wholly ungated:
|
|
6968
|
+
// a cap53 Edit holder was offered it and refused by the server.
|
|
6969
|
+
this.invoiceRecordPaymentButton = { name: 'record-payment', display: 'Record Payment', dialog: true, icon: { name: 'payment', color: 'blue' }, capability: this.dataService.capAccountingInvoices, requiredAccess: RoleAccess.Create,
|
|
6935
6970
|
detailsConfig: this.invoicePaymentsCreateDetailsDialogConfig,
|
|
6936
6971
|
visible: x => (x.status == InvoiceStatus.Submitted || x.status == InvoiceStatus.Paying) && x.outstandingAmount > 0 // Changed: Allow on both Submitted and Paying
|
|
6937
6972
|
}; // Changed: Use detailsConfig to open payment form dialog
|
|
6938
|
-
this.invoiceDiscardButton = { name: 'discard', inDialog: true, display: 'Discard', icon: { name: 'close', color: 'red' },
|
|
6973
|
+
this.invoiceDiscardButton = { name: 'discard', inDialog: true, display: 'Discard', icon: { name: 'close', color: 'red' }, capability: this.dataService.capAccountingInvoices, requiredAccess: RoleAccess.Full,
|
|
6939
6974
|
action: { url: 'invoices?action=discard', method: 'post', successMessage: 'Discarded' },
|
|
6940
6975
|
confirm: { message: 'Invoice will be marked as cancelled?' },
|
|
6941
|
-
visible: x => x.status == InvoiceStatus.Draft // Changed:
|
|
6976
|
+
visible: x => x.status == InvoiceStatus.Draft // Changed: `discard` is one of the four actions InvoicesController raises to Full — now declared above
|
|
6942
6977
|
};
|
|
6943
6978
|
// Changed: Return button — reverts submitted invoice back to draft, reverses accounting transactions
|
|
6944
|
-
this.invoiceReturnButton = { name: 'return', inDialog: true, display: 'Return', icon: { name: 'undo', color: 'orange' },
|
|
6979
|
+
this.invoiceReturnButton = { name: 'return', inDialog: true, display: 'Return', icon: { name: 'undo', color: 'orange' }, capability: this.dataService.capAccountingInvoices, requiredAccess: RoleAccess.Full,
|
|
6945
6980
|
action: { url: 'invoices?action=return', method: 'post', successMessage: 'Returned to Draft' },
|
|
6946
6981
|
confirm: { message: 'Return invoice to draft? This will reverse the receivable and VAT transactions.' },
|
|
6947
|
-
visible: x => x.status == InvoiceStatus.Submitted
|
|
6982
|
+
visible: x => x.status == InvoiceStatus.Submitted // Changed: `return` requires Full at the server — now declared above
|
|
6948
6983
|
};
|
|
6949
6984
|
// Changed: Write-off button — expenses outstanding balance as bad debt and closes the invoice (terminal WrittenOff status)
|
|
6950
|
-
this.invoiceWriteOffButton = { name: 'write-off', inDialog: true, display: 'Write Off', icon: { name: 'money_off', color: 'red' },
|
|
6985
|
+
this.invoiceWriteOffButton = { name: 'write-off', inDialog: true, display: 'Write Off', icon: { name: 'money_off', color: 'red' }, capability: this.dataService.capAccountingInvoices, requiredAccess: RoleAccess.Full,
|
|
6951
6986
|
action: { url: 'invoices?action=write-off', method: 'post' },
|
|
6952
6987
|
confirm: { message: 'Outstanding balance will be expensed as bad debt and the invoice closed. This posts a GL journal.' },
|
|
6953
|
-
visible: x => x.status == InvoiceStatus.Submitted || x.status == InvoiceStatus.Paying
|
|
6988
|
+
visible: x => (x.status == InvoiceStatus.Submitted || x.status == InvoiceStatus.Paying) // Changed: `write-off` requires Full at the server — now declared above
|
|
6954
6989
|
};
|
|
6955
|
-
this.invoiceSubmitButton = { name: 'submit', inDialog: true, display: 'Submit', icon: { name: 'send', },
|
|
6990
|
+
this.invoiceSubmitButton = { name: 'submit', inDialog: true, display: 'Submit', icon: { name: 'send', }, capability: this.dataService.capAccountingInvoices, requiredAccess: RoleAccess.Edit,
|
|
6956
6991
|
action: { url: 'invoices?action=submit', method: 'post', successMessage: 'Submitted' },
|
|
6957
6992
|
confirm: { message: 'Submit invoice? This will record revenue and create an accounts receivable entry.' },
|
|
6958
|
-
visible: x => x.status == InvoiceStatus.Draft,
|
|
6993
|
+
visible: x => x.status == InvoiceStatus.Draft, // Changed: `submit` keeps the Edit floor — the comment on InvoicesController.CustomActionAccess says so explicitly
|
|
6959
6994
|
disabled: x => x.totalAmount == 0
|
|
6960
6995
|
};
|
|
6961
6996
|
this.invoiceEditButton = { name: 'edit', display: 'Save', dialog: true, action: { url: 'invoices?action=edit', method: 'post', },
|
|
@@ -10754,10 +10789,28 @@ class ButtonService {
|
|
|
10754
10789
|
getVisibleButtons(buttons, row, tableConfig) {
|
|
10755
10790
|
return buttons.filter(button => this.testVisible(button, row, tableConfig));
|
|
10756
10791
|
}
|
|
10792
|
+
// Added: the opt-in capability gate, on its own. Mirrors the server's real rule — a NAMED capability at a
|
|
10793
|
+
// NAMED level — because that is what BaseController checks (CustomActionAccess: Edit on the controller's own
|
|
10794
|
+
// CapabilityName). Reads the same role object the configs already read by hand: dataService.myRole is fed
|
|
10795
|
+
// from authService.myRoleObserv (datalib.service.ts:569), which is currentRoleSource.asObservable() — one
|
|
10796
|
+
// source, not a second way to read a role. A button that names no capability returns true immediately, so
|
|
10797
|
+
// every existing button is untouched.
|
|
10798
|
+
hasCapability(button) {
|
|
10799
|
+
if (!button?.capability)
|
|
10800
|
+
return true;
|
|
10801
|
+
const capName = typeof button.capability === 'string' ? button.capability : button.capability.name; // resolved HERE, not at config-build time — datalib assigns CapItem.name after the configs are constructed
|
|
10802
|
+
if (!capName)
|
|
10803
|
+
return true; // an unresolved capability must not gate anything — a silent deny is worse than the status quo
|
|
10804
|
+
const level = this.authService.currentRoleSource.value?.[capName] ?? RoleAccess.None;
|
|
10805
|
+
return level >= (button.requiredAccess ?? RoleAccess.Edit); // Edit is BaseController.CustomActionAccess's floor
|
|
10806
|
+
}
|
|
10757
10807
|
testVisible(button, row, tableConfig) {
|
|
10758
10808
|
if (!Core.isItemVisible(button, row)) { // Changed: unified visible/hidden (boolean | condition), hidden wins
|
|
10759
10809
|
return false;
|
|
10760
10810
|
}
|
|
10811
|
+
if (!this.hasCapability(button)) { // Added: opt-in capability gate — no-op unless the config names one
|
|
10812
|
+
return false;
|
|
10813
|
+
}
|
|
10761
10814
|
const currentRole = this.authService.currentRoleSource.value;
|
|
10762
10815
|
// Check if button has detailsConfig with form restrictions
|
|
10763
10816
|
if (button.detailsConfig?.formConfig?.security) {
|
|
@@ -11773,28 +11826,60 @@ class ConfigService {
|
|
|
11773
11826
|
get value() {
|
|
11774
11827
|
return this.config.value;
|
|
11775
11828
|
}
|
|
11829
|
+
static { this.maxFailedAttempts = 3; }
|
|
11776
11830
|
constructor(dataService) {
|
|
11777
11831
|
this.dataService = dataService;
|
|
11778
11832
|
this.config = new BehaviorSubject({});
|
|
11779
11833
|
this.config$ = this.config.asObservable();
|
|
11780
11834
|
this.loaded = false;
|
|
11835
|
+
// Added: one attempt at a time. Two callers racing on login used to be harmless only because the latch below
|
|
11836
|
+
// swallowed the second; with the latch corrected they would both hit the wire, so dedupe explicitly instead.
|
|
11837
|
+
this.inFlight = false;
|
|
11838
|
+
// Added: the bound. Retrying is CALLER-DRIVEN — there is no timer and no resubscribe here, so a retry only
|
|
11839
|
+
// ever happens because something asked again. This cap stops a chatty caller (a component that loads on every
|
|
11840
|
+
// navigation) from turning a genuinely down API into one request per navigation, forever.
|
|
11841
|
+
this.failedAttempts = 0;
|
|
11781
11842
|
}
|
|
11782
11843
|
// Called on login/bootstrap — the backend creates the row on first read, so this never comes back empty
|
|
11783
11844
|
load(force = false) {
|
|
11845
|
+
// Changed: `loaded` was set HERE, before the request, and every failure was swallowed — so the FIRST attempt
|
|
11846
|
+
// won the session outright. shift-spa fired one on the login screen (AuthService seeds currentRoleSource with
|
|
11847
|
+
// a truthy `new Role`, and a BehaviorSubject replays its seed synchronously), it 401'd with an empty bearer
|
|
11848
|
+
// token, and every later authenticated caller returned early forever. The app then ran on an empty config —
|
|
11849
|
+
// every flag false, ~70 call sites — while the database said otherwise. Only a page reload masked it, which
|
|
11850
|
+
// is why it survived: silent-restore replays the 401'd request, a fresh interactive login does not.
|
|
11851
|
+
if (force)
|
|
11852
|
+
this.failedAttempts = 0; // an explicit refresh is a deliberate act (the App Configuration page just saved), so it is allowed past the cap
|
|
11784
11853
|
if (this.loaded && !force)
|
|
11785
11854
|
return;
|
|
11786
|
-
this.
|
|
11855
|
+
if (this.inFlight && !force)
|
|
11856
|
+
return;
|
|
11857
|
+
if (this.failedAttempts >= ConfigService.maxFailedAttempts && !force)
|
|
11858
|
+
return;
|
|
11859
|
+
this.inFlight = true;
|
|
11787
11860
|
this.dataService.CallApi({ url: 'configuration/get', skipCache: true }).subscribe({
|
|
11788
11861
|
next: (apiResponse) => {
|
|
11862
|
+
this.inFlight = false;
|
|
11789
11863
|
// DELIBERATELY SILENT (WS-6 Phase 4 triage). This runs on login/bootstrap, before the user has asked
|
|
11790
|
-
// for anything, and
|
|
11791
|
-
//
|
|
11792
|
-
//
|
|
11793
|
-
//
|
|
11794
|
-
|
|
11864
|
+
// for anything, and greeting someone with an error dialog the instant they sign in, for an app that then
|
|
11865
|
+
// works, is a worse lie than saying nothing. The error callback below absorbs the transport case likewise.
|
|
11866
|
+
// Changed: the old comment claimed everything downstream FAILS OPEN. It does not. Config flags are read as
|
|
11867
|
+
// `!!config.enableX` and the database defaults are TRUE, so an empty config reads FALSE and HIDES things —
|
|
11868
|
+
// fail-CLOSED. Staying silent is still right; latching the failure was not. Retry stays possible instead.
|
|
11869
|
+
if (apiResponse.success) {
|
|
11870
|
+
this.loaded = true; // Changed: only a response that actually carried a config counts as loaded
|
|
11871
|
+
this.failedAttempts = 0;
|
|
11795
11872
|
this.config.next(apiResponse.data || {});
|
|
11873
|
+
}
|
|
11874
|
+
else
|
|
11875
|
+
this.failedAttempts++; // Added: a refusal (401/403 shaped as success:false) is a failure, not a loaded config
|
|
11796
11876
|
},
|
|
11797
|
-
error: () => {
|
|
11877
|
+
error: () => {
|
|
11878
|
+
// an app without a ConfigurationController simply keeps the empty object — and after maxFailedAttempts
|
|
11879
|
+
// stops asking, so the 404 costs three requests a session rather than one, and never more
|
|
11880
|
+
this.inFlight = false;
|
|
11881
|
+
this.failedAttempts++;
|
|
11882
|
+
}
|
|
11798
11883
|
});
|
|
11799
11884
|
}
|
|
11800
11885
|
refresh() {
|
|
@@ -12143,7 +12228,9 @@ class SetupGuideComponent {
|
|
|
12143
12228
|
const act = this.getAction(step);
|
|
12144
12229
|
if (act.button) {
|
|
12145
12230
|
this.dialogService.openDefaultDetailsDialog({ ...act.button }, {}).subscribe(result => {
|
|
12146
|
-
if (result
|
|
12231
|
+
if (!result)
|
|
12232
|
+
return; // Added: Escape/backdrop dismissal closes with undefined. This one never threw (it already read through `?.`) but it fell on through to refresh() on every dismissal — a dismissal changes nothing, so it must not recount either.
|
|
12233
|
+
if (result.action === 'inputChange')
|
|
12147
12234
|
return; // Changed: an input change is not a close — nothing to recount or follow up
|
|
12148
12235
|
this.setupService.refresh(); // Recount after the dialog closes so progress advances immediately
|
|
12149
12236
|
this.openSuccessButton(act.button, result); // Added: complete CLAUDE.md's Two-Step Create Pattern from this page too
|
|
@@ -12428,11 +12515,11 @@ class SetupGuideComponent {
|
|
|
12428
12515
|
});
|
|
12429
12516
|
}
|
|
12430
12517
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SetupGuideComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
12431
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SetupGuideComponent, isStandalone: false, selector: "spa-setup-guide", ngImport: i0, template: "<div class=\"setup-page\" *ngIf=\"status\">\n\n <!-- Hero: overall readiness + celebration state at 100% -->\n <mat-card class=\"setup-hero\" [class.celebrate]=\"status.percent === 100\">\n <div class=\"hero-content\" *ngIf=\"status.percent < 100\">\n <div class=\"hero-text\">\n <h1>{{ title }}</h1>\n <p>Complete these steps to get your system ready for day-to-day operation.</p>\n <span class=\"hero-counter\">{{ status.completed }} of {{ status.total }} steps completed</span>\n </div>\n <div class=\"hero-progress\">\n <span class=\"hero-percent\">{{ status.percent }}%</span>\n <mat-progress-bar mode=\"determinate\" [value]=\"status.percent\"></mat-progress-bar>\n </div>\n </div>\n <div class=\"hero-content celebration\" *ngIf=\"status.percent === 100\">\n <mat-icon class=\"celebrate-icon\">celebration</mat-icon>\n <div class=\"hero-text\">\n <h1>You're all set!</h1>\n <p>All setup steps are complete \u2014 your system is ready to operate.</p>\n </div>\n </div>\n </mat-card>\n\n <!-- Module picker (v2): choose what the business uses; optional modules toggle steps + menus -->\n <mat-card class=\"setup-modules\" *ngIf=\"modules.length > 0\">\n <div class=\"group-header\">\n <h2>Your modules</h2>\n <span class=\"group-counter\">Tap a module to turn it on or off \u2014 you can change this anytime.</span>\n </div>\n <!-- Changed (v3): grouped \u2014 the picker now carries the whole library catalog, so cards sit under their\n group heading (the app's own modules first). A backend without groups yields one unnamed group,\n which renders as the original flat grid. -->\n <div class=\"module-section\" *ngFor=\"let mgroup of moduleGroups\">\n <div class=\"module-section-head\">\n <span class=\"module-section-name\">{{ mgroup.name }}</span>\n <span class=\"module-section-count\">{{ mgroup.enabled }} of {{ mgroup.total }} on</span>\n </div>\n <div class=\"module-grid\">\n <div class=\"module-card\" *ngFor=\"let mod of mgroup.modules\"\n [class.enabled]=\"mod.enabled\" [class.core]=\"mod.core\" [class.busy]=\"togglingKey === mod.key\"\n (click)=\"toggleModule(mod)\">\n <div class=\"module-head\">\n <mat-icon class=\"module-icon\">{{ mod.icon || 'extension' }}</mat-icon>\n <mat-icon class=\"module-state\" [class.on]=\"mod.enabled\">{{ mod.enabled ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n </div>\n <div class=\"module-title\">{{ mod.title }}</div>\n <div class=\"module-description\">{{ mod.description }}</div>\n <span class=\"module-chip\" *ngIf=\"mod.core\">Always on</span>\n </div>\n </div>\n </div>\n <div class=\"module-actions\" *ngIf=\"!modulesConfirmed\">\n <button mat-flat-button color=\"primary\" [disabled]=\"togglingKey !== ''\" (click)=\"confirmModules()\">Confirm selection</button>\n <span class=\"module-hint\">Happy with this selection? Confirm it to complete the step below.</span>\n </div>\n </mat-card>\n\n <!-- Category groups, top-down \u2014 Changed: flat spa-checklist replaces the raised mat-accordion -->\n <mat-card class=\"setup-group\" *ngFor=\"let group of groups\">\n <div class=\"group-header\">\n <h2>{{ group.name }}</h2>\n <span class=\"group-counter\">{{ group.completed }} of {{ group.total }}</span>\n </div>\n <spa-checklist [config]=\"group.config\" [itemTemplate]=\"stepExtrasTpl\"></spa-checklist>\n </mat-card>\n\n <!-- Demo data (opt-in via setupConfig.demoData) \u2014 moved here off the per-app configuration pages -->\n <mat-card class=\"setup-group setup-demo\" *ngIf=\"showDemoData\">\n <div class=\"group-header\">\n <h2>Demo data</h2>\n <span class=\"group-counter\">Explore with example records, then clear them out</span>\n </div>\n <p class=\"demo-description\">Seeding fills the system with example records so you can try it out before capturing anything real. Removing deletes only those example records.</p>\n <div class=\"demo-actions\">\n <button mat-flat-button color=\"primary\" [disabled]=\"demoBusy\" (click)=\"seedDemoData()\"><mat-icon>add_circle</mat-icon> Seed demo data</button>\n <button mat-stroked-button color=\"warn\" [disabled]=\"demoBusy\" (click)=\"removeDemoData()\"><mat-icon>delete</mat-icon> Remove demo data</button>\n </div>\n </mat-card>\n\n</div>\n\n<!-- Projected into the expanded checklist body: predefined roles picker on the roles step (v2) -->\n<ng-template #stepExtrasTpl let-item>\n <div class=\"role-templates\" *ngIf=\"hasRoleTemplates(item.data) && roleTemplates.length > 0\">\n <div class=\"role-template\" *ngFor=\"let tpl of roleTemplates\" [class.created]=\"tpl.exists\" [class.selected]=\"!tpl.exists && selectedTemplates[tpl.key]\" (click)=\"toggleTemplate(tpl)\">\n <div class=\"role-head\">\n <mat-icon class=\"role-state\" [class.on]=\"tpl.exists || selectedTemplates[tpl.key]\">{{ (tpl.exists || selectedTemplates[tpl.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"role-name\">{{ tpl.name }}</span>\n <span class=\"role-created\" *ngIf=\"tpl.exists\"><mat-icon>check</mat-icon>Created</span>\n </div>\n <div class=\"role-description\">{{ tpl.description }}</div>\n </div>\n </div>\n\n <!-- Added (presets): the preset picker. Same row as the role picker above \u2014 same state icon, same 8px stack,\n same colours \u2014 plus ONE sentence and ONE control.\n Changed (presets P4): rendered through ONE shared template for both the notification step and the sign-off\n step, parameterised by the picker object. The two sit one above the other on the same page, so a second\n near-identical block would drift and every difference would read as a mistake. -->\n <ng-container *ngIf=\"pickerFor(item.data) as picker\">\n <ng-container *ngTemplateOutlet=\"presetPickerTpl; context: { $implicit: picker }\"></ng-container>\n </ng-container>\n</ng-template>\n\n<!-- The one picker body. `picker` carries the domain: its rows, its roles, its wording, its apply call. -->\n<ng-template #presetPickerTpl let-picker>\n <div class=\"preset-picker\" *ngIf=\"picker.loaded && picker.presets.length > 0\">\n\n <!-- No selectable role \u2192 we ask for one instead of offering ticks that would build an inert rule. The wording\n is the picker's own, because \"no roles\" and \"roles nobody is in\" are different situations. -->\n <div class=\"preset-empty\" *ngIf=\"picker.roles.length === 0\">{{ picker.emptyText }}</div>\n\n <ng-container *ngIf=\"picker.roles.length > 0\">\n <div class=\"preset-section\" *ngFor=\"let pgroup of picker.groups\">\n <div class=\"preset-section-head\">\n <span class=\"preset-section-name\">{{ pgroup.name }}</span>\n <span class=\"preset-section-count\">{{ pgroup.on }} of {{ pgroup.total }}</span>\n </div>\n <div class=\"preset-row\" *ngFor=\"let row of pgroup.rows\" role=\"checkbox\" tabindex=\"0\"\n [class.configured]=\"row.preset.exists\" [class.selected]=\"!row.preset.exists && picker.selected[row.preset.key]\"\n [attr.aria-checked]=\"row.preset.exists || !!picker.selected[row.preset.key]\" [attr.aria-disabled]=\"row.preset.exists\"\n (click)=\"togglePreset(picker, row.preset)\"\n (keydown.enter)=\"togglePreset(picker, row.preset)\"\n (keydown.space)=\"togglePreset(picker, row.preset); $event.preventDefault()\">\n <div class=\"preset-head\">\n <mat-icon class=\"preset-state\" [class.on]=\"row.preset.exists || picker.selected[row.preset.key]\">{{ (row.preset.exists || picker.selected[row.preset.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"preset-name\">{{ row.preset.name }}</span>\n <span class=\"preset-configured\" *ngIf=\"row.preset.exists\"><mat-icon>check</mat-icon>Configured</span>\n </div>\n <div class=\"preset-description\">{{ row.preset.description }}</div>\n <!-- The effect sentence comes from the server whole; `before` and `after` are its two halves either side\n of the {role} placeholder, so reading the line IS reading the configuration. The sign-off sentences\n carry their own deferred-notification caveat, which is why there is no extra note here. -->\n <div class=\"preset-effect\" *ngIf=\"!row.preset.exists\">{{ row.before }}<select class=\"preset-role\" [attr.aria-label]=\"picker.roleAria + row.preset.name\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n (change)=\"setPresetRole(picker, row.preset, $any($event.target).value)\">\n <option *ngFor=\"let role of picker.roles\" [value]=\"role.value\" [selected]=\"role.value === picker.role[row.preset.key]\">{{ role.name }}</option>\n </select>{{ row.after }}</div>\n <div class=\"preset-effect preset-done\" *ngIf=\"row.preset.exists\">{{ picker.doneText }}</div>\n </div>\n </div>\n\n <!-- One page-level channel choice, notifications only. In-app is the only channel that always works; SMS is a\n stub and is never offered; an approval request is always in-app, so the sign-off picker has no such line. -->\n <div class=\"preset-email\" *ngIf=\"picker.showEmail\" role=\"checkbox\" tabindex=\"0\" [attr.aria-checked]=\"includeEmail\"\n (click)=\"toggleIncludeEmail()\"\n (keydown.enter)=\"toggleIncludeEmail()\"\n (keydown.space)=\"toggleIncludeEmail(); $event.preventDefault()\">\n <mat-icon class=\"preset-state\" [class.on]=\"includeEmail\">{{ includeEmail ? 'check_box' : 'check_box_outline_blank' }}</mat-icon>\n <span class=\"preset-email-label\">Also send these by email</span>\n <span class=\"preset-email-hint\">In-app always. Email needs your mail settings.</span>\n </div>\n </ng-container>\n </div>\n</ng-template>\n\n<!-- Graceful empty state (feature disabled or status unavailable) -->\n<div class=\"setup-empty\" *ngIf=\"!status\">\n <mat-icon>rocket_launch</mat-icon>\n <p>Setup status is not available yet.</p>\n</div>\n", styles: [".setup-page{max-width:860px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.setup-hero{padding:24px}.hero-content{display:flex;align-items:center;justify-content:space-between;gap:24px;flex-wrap:wrap}.hero-text h1{margin:0 0 4px;font-size:24px}.hero-text p{margin:0 0 8px;color:#0009}.hero-counter{font-size:13px;color:#0009}.hero-progress{flex:1;min-width:220px;max-width:340px}.hero-percent{display:block;font-size:28px;font-weight:600;color:#2e7d32;margin-bottom:6px;text-align:right}.hero-progress mat-progress-bar{height:10px;border-radius:5px}.setup-hero.celebrate{background:linear-gradient(135deg,#e8f5e9,#f1f8e9)}.celebration{justify-content:flex-start}.celebrate-icon{font-size:48px;width:48px;height:48px;color:#2e7d32;animation:celebrate-pop .6s ease-out}@keyframes celebrate-pop{0%{transform:scale(.3);opacity:0}70%{transform:scale(1.15)}to{transform:scale(1);opacity:1}}.setup-group{padding:16px}.group-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.group-header h2{margin:0;font-size:17px}.group-counter{font-size:12px;color:#0000008c}.setup-modules{padding:16px}.module-section+.module-section{margin-top:18px}.module-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.module-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.module-section-count{font-size:11px;color:#00000073;white-space:nowrap}.module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;margin-top:8px}.module-card{border:1px solid rgba(0,0,0,.12);border-radius:10px;padding:12px;cursor:pointer;transition:border-color .15s,background .15s,opacity .15s;opacity:.72;position:relative}.module-card:hover{border-color:#90a4ae}.module-card.enabled{border-color:#4caf50;background:#f6fbf6;opacity:1}.module-card.core{cursor:default}.module-card.busy{pointer-events:none;opacity:.5}.module-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.module-icon{color:#546e7a}.module-card.enabled .module-icon{color:#2e7d32}.module-state{color:#b0bec5}.module-state.on{color:#4caf50}.module-title{font-weight:600;font-size:14px;margin-bottom:4px}.module-description{font-size:12px;color:#0009;min-height:30px}.module-chip{display:inline-block;margin-top:8px;background:#eceff1;color:#546e7a;border-radius:12px;padding:2px 10px;font-size:11px}.module-actions{display:flex;align-items:center;gap:8px 12px;margin-top:14px;flex-wrap:wrap}.module-actions button{flex-shrink:0}.module-hint{font-size:12px;color:#0000008c}.role-templates{display:flex;flex-direction:column;gap:8px;margin:0 0 12px}.role-template{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.role-template:hover{border-color:#90a4ae}.role-template.selected{border-color:#4caf50;background:#f6fbf6}.role-template.created{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.role-head{display:flex;align-items:center;gap:8px}.role-state{color:#b0bec5;flex-shrink:0}.role-state.on{color:#4caf50}.role-name{font-weight:500;font-size:13px}.role-created{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32}.role-created mat-icon{font-size:16px;width:16px;height:16px}.role-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-picker{display:flex;flex-direction:column;gap:14px;margin:0 0 12px}.preset-empty{font-size:12px;line-height:18px;color:#0009;max-width:62ch}.preset-section{display:flex;flex-direction:column;gap:8px}.preset-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.preset-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.preset-section-count{font-size:11px;color:#00000073;white-space:nowrap}.preset-row{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.preset-row:hover{border-color:#90a4ae}.preset-row:focus-visible{outline:2px solid #4caf50;outline-offset:2px}.preset-row.selected{border-color:#4caf50;background:#f6fbf6}.preset-row.configured{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.preset-head{display:flex;align-items:center;gap:8px}.preset-state{color:#b0bec5;flex-shrink:0}.preset-state.on{color:#4caf50}.preset-name{font-weight:500;font-size:13px}.preset-configured{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32;white-space:nowrap}.preset-configured mat-icon{font-size:16px;width:16px;height:16px}.preset-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-effect{font-size:12px;line-height:22px;color:#000000c7;margin:4px 0 0 32px}.preset-done{color:#2e7d32}.preset-role{font-family:inherit;font-size:12px;font-weight:500;color:#2e7d32;background:#fff;border:1px solid #c8e6c9;border-radius:6px;padding:2px 4px;margin:0 2px;max-width:100%;cursor:pointer}.preset-role:hover{border-color:#4caf50}.preset-role:focus-visible{outline:2px solid #4caf50;outline-offset:1px}.preset-email{display:flex;align-items:center;gap:8px;flex-wrap:wrap;cursor:pointer;padding:2px 0}.preset-email:focus-visible{outline:2px solid #4caf50;outline-offset:2px;border-radius:6px}.preset-email-label{font-size:13px;font-weight:500}.preset-email-hint{font-size:12px;color:#0000008c}.setup-empty{text-align:center;padding:48px 16px;color:#00000080}.setup-empty mat-icon{font-size:40px;width:40px;height:40px}.demo-description{margin:8px 0 14px;font-size:13px;color:#0009}.demo-actions{display:flex;gap:12px;flex-wrap:wrap}@media (max-width: 700px){.module-hint{flex:0 0 100%}.preset-role{display:block;width:100%;margin:4px 0 0;padding:6px 8px}.preset-email-hint{flex:0 0 100%}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i2$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: i6$1.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: ChecklistComponent, selector: "spa-checklist", inputs: ["config", "itemTemplate"] }] }); }
|
|
12518
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SetupGuideComponent, isStandalone: false, selector: "spa-setup-guide", ngImport: i0, template: "<div class=\"setup-page\" *ngIf=\"status\">\n\n <!-- Hero: overall readiness + celebration state at 100% -->\n <mat-card class=\"setup-hero\" [class.celebrate]=\"status.percent === 100\">\n <div class=\"hero-content\" *ngIf=\"status.percent < 100\">\n <div class=\"hero-text\">\n <h1>{{ title }}</h1>\n <p>Complete these steps to get your system ready for day-to-day operation.</p>\n <span class=\"hero-counter\">{{ status.completed }} of {{ status.total }} steps completed</span>\n </div>\n <div class=\"hero-progress\">\n <span class=\"hero-percent\">{{ status.percent }}%</span>\n <mat-progress-bar mode=\"determinate\" [value]=\"status.percent\"></mat-progress-bar>\n </div>\n </div>\n <div class=\"hero-content celebration\" *ngIf=\"status.percent === 100\">\n <mat-icon class=\"celebrate-icon\">celebration</mat-icon>\n <div class=\"hero-text\">\n <h1>You're all set!</h1>\n <p>All setup steps are complete \u2014 your system is ready to operate.</p>\n </div>\n </div>\n </mat-card>\n\n <!-- Module picker (v2): choose what the business uses; optional modules toggle steps + menus -->\n <mat-card class=\"setup-modules\" *ngIf=\"modules.length > 0\">\n <div class=\"group-header\">\n <h2>Your modules</h2>\n <span class=\"group-counter\">Tap a module to turn it on or off \u2014 you can change this anytime.</span>\n </div>\n <!-- Changed (v3): grouped \u2014 the picker now carries the whole library catalog, so cards sit under their\n group heading (the app's own modules first). A backend without groups yields one unnamed group,\n which renders as the original flat grid. -->\n <div class=\"module-section\" *ngFor=\"let mgroup of moduleGroups\">\n <div class=\"module-section-head\">\n <span class=\"module-section-name\">{{ mgroup.name }}</span>\n <span class=\"module-section-count\">{{ mgroup.enabled }} of {{ mgroup.total }} on</span>\n </div>\n <div class=\"module-grid\">\n <div class=\"module-card\" *ngFor=\"let mod of mgroup.modules\"\n [class.enabled]=\"mod.enabled\" [class.core]=\"mod.core\" [class.busy]=\"togglingKey === mod.key\"\n (click)=\"toggleModule(mod)\">\n <div class=\"module-head\">\n <mat-icon class=\"module-icon\">{{ mod.icon || 'extension' }}</mat-icon>\n <mat-icon class=\"module-state\" [class.on]=\"mod.enabled\">{{ mod.enabled ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n </div>\n <div class=\"module-title\">{{ mod.title }}</div>\n <div class=\"module-description\">{{ mod.description }}</div>\n <span class=\"module-chip\" *ngIf=\"mod.core\">Always on</span>\n </div>\n </div>\n </div>\n <div class=\"module-actions\" *ngIf=\"!modulesConfirmed\">\n <button mat-flat-button color=\"primary\" [disabled]=\"togglingKey !== ''\" (click)=\"confirmModules()\">Confirm selection</button>\n <span class=\"module-hint\">Happy with this selection? Confirm it to complete the step below.</span>\n </div>\n </mat-card>\n\n <!-- Category groups, top-down \u2014 Changed: flat spa-checklist replaces the raised mat-accordion -->\n <mat-card class=\"setup-group\" *ngFor=\"let group of groups\">\n <div class=\"group-header\">\n <h2>{{ group.name }}</h2>\n <span class=\"group-counter\">{{ group.completed }} of {{ group.total }}</span>\n </div>\n <spa-checklist [config]=\"group.config\" [itemTemplate]=\"stepExtrasTpl\"></spa-checklist>\n </mat-card>\n\n <!-- Demo data (opt-in via setupConfig.demoData) \u2014 moved here off the per-app configuration pages -->\n <mat-card class=\"setup-group setup-demo\" *ngIf=\"showDemoData\">\n <div class=\"group-header\">\n <h2>Demo data</h2>\n <span class=\"group-counter\">Explore with example records, then clear them out</span>\n </div>\n <p class=\"demo-description\">Seeding fills the system with example records so you can try it out before capturing anything real. Removing deletes only those example records.</p>\n <div class=\"demo-actions\">\n <button mat-flat-button color=\"primary\" [disabled]=\"demoBusy\" (click)=\"seedDemoData()\"><mat-icon>add_circle</mat-icon> Seed demo data</button>\n <button mat-stroked-button color=\"warn\" [disabled]=\"demoBusy\" (click)=\"removeDemoData()\"><mat-icon>delete</mat-icon> Remove demo data</button>\n </div>\n </mat-card>\n\n</div>\n\n<!-- Projected into the expanded checklist body: predefined roles picker on the roles step (v2) -->\n<ng-template #stepExtrasTpl let-item>\n <div class=\"role-templates\" *ngIf=\"hasRoleTemplates(item.data) && roleTemplates.length > 0\">\n <div class=\"role-template\" *ngFor=\"let tpl of roleTemplates\" [class.created]=\"tpl.exists\" [class.selected]=\"!tpl.exists && selectedTemplates[tpl.key]\" (click)=\"toggleTemplate(tpl)\">\n <div class=\"role-head\">\n <mat-icon class=\"role-state\" [class.on]=\"tpl.exists || selectedTemplates[tpl.key]\">{{ (tpl.exists || selectedTemplates[tpl.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"role-name\">{{ tpl.name }}</span>\n <span class=\"role-created\" *ngIf=\"tpl.exists\"><mat-icon>check</mat-icon>Created</span>\n </div>\n <div class=\"role-description\">{{ tpl.description }}</div>\n </div>\n </div>\n\n <!-- Added (presets): the preset picker. Same row as the role picker above \u2014 same state icon, same 8px stack,\n same colours \u2014 plus ONE sentence and ONE control.\n Changed (presets P4): rendered through ONE shared template for both the notification step and the sign-off\n step, parameterised by the picker object. The two sit one above the other on the same page, so a second\n near-identical block would drift and every difference would read as a mistake. -->\n <ng-container *ngIf=\"pickerFor(item.data) as picker\">\n <ng-container *ngTemplateOutlet=\"presetPickerTpl; context: { $implicit: picker }\"></ng-container>\n </ng-container>\n</ng-template>\n\n<!-- The one picker body. `picker` carries the domain: its rows, its roles, its wording, its apply call. -->\n<ng-template #presetPickerTpl let-picker>\n <div class=\"preset-picker\" *ngIf=\"picker.loaded && picker.presets.length > 0\">\n\n <!-- No selectable role \u2192 we ask for one instead of offering ticks that would build an inert rule. The wording\n is the picker's own, because \"no roles\" and \"roles nobody is in\" are different situations. -->\n <div class=\"preset-empty\" *ngIf=\"picker.roles.length === 0\">{{ picker.emptyText }}</div>\n\n <ng-container *ngIf=\"picker.roles.length > 0\">\n <div class=\"preset-section\" *ngFor=\"let pgroup of picker.groups\">\n <div class=\"preset-section-head\">\n <span class=\"preset-section-name\">{{ pgroup.name }}</span>\n <span class=\"preset-section-count\">{{ pgroup.on }} of {{ pgroup.total }}</span>\n </div>\n <div class=\"preset-row\" *ngFor=\"let row of pgroup.rows\" role=\"checkbox\" tabindex=\"0\"\n [class.configured]=\"row.preset.exists\" [class.selected]=\"!row.preset.exists && picker.selected[row.preset.key]\"\n [attr.aria-checked]=\"row.preset.exists || !!picker.selected[row.preset.key]\" [attr.aria-disabled]=\"row.preset.exists\"\n (click)=\"togglePreset(picker, row.preset)\"\n (keydown.enter)=\"togglePreset(picker, row.preset)\"\n (keydown.space)=\"togglePreset(picker, row.preset); $event.preventDefault()\">\n <div class=\"preset-head\">\n <mat-icon class=\"preset-state\" [class.on]=\"row.preset.exists || picker.selected[row.preset.key]\">{{ (row.preset.exists || picker.selected[row.preset.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"preset-name\">{{ row.preset.name }}</span>\n <span class=\"preset-configured\" *ngIf=\"row.preset.exists\"><mat-icon>check</mat-icon>Configured</span>\n </div>\n <div class=\"preset-description\">{{ row.preset.description }}</div>\n <!-- The effect sentence comes from the server whole; `before` and `after` are its two halves either side\n of the {role} placeholder, so reading the line IS reading the configuration. The sign-off sentences\n carry their own deferred-notification caveat, which is why there is no extra note here. -->\n <div class=\"preset-effect\" *ngIf=\"!row.preset.exists\">{{ row.before }}<select class=\"preset-role\" [attr.aria-label]=\"picker.roleAria + row.preset.name\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n (change)=\"setPresetRole(picker, row.preset, $any($event.target).value)\">\n <option *ngFor=\"let role of picker.roles\" [value]=\"role.value\" [selected]=\"role.value === picker.role[row.preset.key]\">{{ role.name }}</option>\n </select>{{ row.after }}</div>\n <div class=\"preset-effect preset-done\" *ngIf=\"row.preset.exists\">{{ picker.doneText }}</div>\n </div>\n </div>\n\n <!-- One page-level channel choice, notifications only. In-app is the only channel that always works; SMS is a\n stub and is never offered; an approval request is always in-app, so the sign-off picker has no such line. -->\n <div class=\"preset-email\" *ngIf=\"picker.showEmail\" role=\"checkbox\" tabindex=\"0\" [attr.aria-checked]=\"includeEmail\"\n (click)=\"toggleIncludeEmail()\"\n (keydown.enter)=\"toggleIncludeEmail()\"\n (keydown.space)=\"toggleIncludeEmail(); $event.preventDefault()\">\n <mat-icon class=\"preset-state\" [class.on]=\"includeEmail\">{{ includeEmail ? 'check_box' : 'check_box_outline_blank' }}</mat-icon>\n <span class=\"preset-email-label\">Also send these by email</span>\n <span class=\"preset-email-hint\">In-app always. Email needs your mail settings.</span>\n </div>\n </ng-container>\n </div>\n</ng-template>\n\n<!-- Graceful empty state (feature disabled or status unavailable) -->\n<div class=\"setup-empty\" *ngIf=\"!status\">\n <mat-icon>rocket_launch</mat-icon>\n <p>Setup status is not available yet.</p>\n</div>\n", styles: [".setup-page{max-width:860px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.setup-hero{padding:24px}.hero-content{display:flex;align-items:center;justify-content:space-between;gap:24px;flex-wrap:wrap}.hero-text h1{margin:0 0 4px;font-size:24px}.hero-text p{margin:0 0 8px;color:#0009}.hero-counter{font-size:13px;color:#0009}.hero-progress{flex:1;min-width:220px;max-width:340px}.hero-percent{display:block;font-size:28px;font-weight:600;color:#2e7d32;margin-bottom:6px;text-align:right}.hero-progress mat-progress-bar{height:10px;border-radius:5px}.setup-hero.celebrate{background:linear-gradient(135deg,#e8f5e9,#f1f8e9)}.celebration{justify-content:flex-start}.celebrate-icon{font-size:48px;width:48px;height:48px;color:#2e7d32;animation:celebrate-pop .6s ease-out}@keyframes celebrate-pop{0%{transform:scale(.3);opacity:0}70%{transform:scale(1.15)}to{transform:scale(1);opacity:1}}.setup-group{padding:16px}.group-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.group-header h2{margin:0;font-size:17px}.group-counter{font-size:12px;color:#0000008c}.setup-modules{padding:16px}.module-section+.module-section{margin-top:18px}.module-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.module-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.module-section-count{font-size:11px;color:#00000073;white-space:nowrap}.module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;margin-top:8px}.module-card{border:1px solid rgba(0,0,0,.12);border-radius:10px;padding:12px;cursor:pointer;transition:border-color .15s,background .15s,opacity .15s;opacity:.72;position:relative}.module-card:hover{border-color:#90a4ae}.module-card.enabled{border-color:#4caf50;background:#f6fbf6;opacity:1}.module-card.core{cursor:default}.module-card.busy{pointer-events:none;opacity:.5}.module-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.module-icon{color:#546e7a}.module-card.enabled .module-icon{color:#2e7d32}.module-state{color:#b0bec5}.module-state.on{color:#4caf50}.module-title{font-weight:600;font-size:14px;margin-bottom:4px}.module-description{font-size:12px;color:#0009;min-height:30px}.module-chip{display:inline-block;margin-top:8px;background:#eceff1;color:#546e7a;border-radius:12px;padding:2px 10px;font-size:11px}.module-actions{display:flex;align-items:center;gap:8px 12px;margin-top:14px;flex-wrap:wrap}.module-actions button{flex-shrink:0}.module-hint{font-size:12px;color:#0000008c}.role-templates{display:flex;flex-direction:column;gap:8px;margin:0 0 12px}.role-template{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.role-template:hover{border-color:#90a4ae}.role-template.selected{border-color:#4caf50;background:#f6fbf6}.role-template.created{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.role-head{display:flex;align-items:center;gap:8px}.role-state{color:#b0bec5;flex-shrink:0}.role-state.on{color:#4caf50}.role-name{font-weight:500;font-size:13px}.role-created{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32}.role-created mat-icon{font-size:16px;width:16px;height:16px}.role-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-picker{display:flex;flex-direction:column;gap:14px;margin:0 0 12px}.preset-empty{font-size:12px;line-height:18px;color:#0009;max-width:62ch}.preset-section{display:flex;flex-direction:column;gap:8px}.preset-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.preset-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.preset-section-count{font-size:11px;color:#00000073;white-space:nowrap}.preset-row{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.preset-row:hover{border-color:#90a4ae}.preset-row:focus-visible{outline:2px solid #4caf50;outline-offset:2px}.preset-row.selected{border-color:#4caf50;background:#f6fbf6}.preset-row.configured{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.preset-head{display:flex;align-items:center;gap:8px}.preset-state{color:#b0bec5;flex-shrink:0}.preset-state.on{color:#4caf50}.preset-name{font-weight:500;font-size:13px}.preset-configured{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32;white-space:nowrap}.preset-configured mat-icon{font-size:16px;width:16px;height:16px}.preset-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-effect{font-size:12px;line-height:22px;color:#000000c7;margin:4px 0 0 32px}.preset-done{color:#2e7d32}.preset-role{font-family:inherit;font-size:12px;font-weight:500;color:#2e7d32;background:#fff;border:1px solid #c8e6c9;border-radius:6px;padding:2px 4px;margin:0 2px;max-width:100%;cursor:pointer}.preset-role:hover{border-color:#4caf50}.preset-role:focus-visible{outline:2px solid #4caf50;outline-offset:1px}.preset-email{display:flex;align-items:center;gap:8px;flex-wrap:wrap;cursor:pointer;padding:2px 0}.preset-email:focus-visible{outline:2px solid #4caf50;outline-offset:2px;border-radius:6px}.preset-email-label{font-size:13px;font-weight:500}.preset-email-hint{font-size:12px;color:#0000008c}.setup-empty{text-align:center;padding:48px 16px;color:#00000080}.setup-empty mat-icon{font-size:40px;width:40px;height:40px}.demo-description{margin:8px 0 14px;font-size:13px;color:#0009}.demo-actions{display:flex;gap:12px;flex-wrap:wrap}@media (max-width: 700px){.setup-page{padding:10px 0;gap:12px}.setup-hero{padding:12px}.setup-modules,.setup-group{padding:10px}.hero-content{gap:10px}.hero-text h1{font-size:20px}.setup-hero:not(.celebrate) .hero-text p{display:none}.hero-counter{font-size:12px}.hero-progress{min-width:0;max-width:none}.hero-percent{font-size:22px;margin-bottom:4px}.hero-progress mat-progress-bar{height:8px;border-radius:4px}.celebrate-icon{font-size:36px;width:36px;height:36px}.group-header{margin-bottom:6px;gap:2px 8px}.group-header h2{font-size:15px}.group-counter{font-size:11px}.setup-modules>.group-header,.setup-demo>.group-header{flex-wrap:wrap}.setup-modules>.group-header .group-counter,.setup-demo>.group-header .group-counter{flex:1 0 100%}.module-section+.module-section{margin-top:12px}.module-section-head{padding-bottom:4px}.module-grid{gap:8px;margin-top:6px}.module-card{padding:10px}.module-head{margin-bottom:4px}.module-title{font-size:13.5px;margin-bottom:2px}.module-description{min-height:0}.module-chip{margin-top:6px}.module-actions{margin-top:10px}.role-templates,.preset-picker{gap:6px;margin-bottom:8px}.role-template,.preset-row{padding:8px 10px}.role-description,.preset-description,.preset-effect{margin-left:0}.preset-section{gap:6px}.preset-section-head{padding-bottom:4px}.demo-description{margin:6px 0 10px;font-size:12.5px}.demo-actions{gap:8px}.setup-empty{padding:28px 12px}.module-hint{flex:0 0 100%}.preset-role{display:block;width:100%;margin:4px 0 0;padding:6px 8px}.preset-email-hint{flex:0 0 100%}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i2$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: i6$1.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: ChecklistComponent, selector: "spa-checklist", inputs: ["config", "itemTemplate"] }] }); }
|
|
12432
12519
|
}
|
|
12433
12520
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SetupGuideComponent, decorators: [{
|
|
12434
12521
|
type: Component,
|
|
12435
|
-
args: [{ selector: 'spa-setup-guide', standalone: false, template: "<div class=\"setup-page\" *ngIf=\"status\">\n\n <!-- Hero: overall readiness + celebration state at 100% -->\n <mat-card class=\"setup-hero\" [class.celebrate]=\"status.percent === 100\">\n <div class=\"hero-content\" *ngIf=\"status.percent < 100\">\n <div class=\"hero-text\">\n <h1>{{ title }}</h1>\n <p>Complete these steps to get your system ready for day-to-day operation.</p>\n <span class=\"hero-counter\">{{ status.completed }} of {{ status.total }} steps completed</span>\n </div>\n <div class=\"hero-progress\">\n <span class=\"hero-percent\">{{ status.percent }}%</span>\n <mat-progress-bar mode=\"determinate\" [value]=\"status.percent\"></mat-progress-bar>\n </div>\n </div>\n <div class=\"hero-content celebration\" *ngIf=\"status.percent === 100\">\n <mat-icon class=\"celebrate-icon\">celebration</mat-icon>\n <div class=\"hero-text\">\n <h1>You're all set!</h1>\n <p>All setup steps are complete \u2014 your system is ready to operate.</p>\n </div>\n </div>\n </mat-card>\n\n <!-- Module picker (v2): choose what the business uses; optional modules toggle steps + menus -->\n <mat-card class=\"setup-modules\" *ngIf=\"modules.length > 0\">\n <div class=\"group-header\">\n <h2>Your modules</h2>\n <span class=\"group-counter\">Tap a module to turn it on or off \u2014 you can change this anytime.</span>\n </div>\n <!-- Changed (v3): grouped \u2014 the picker now carries the whole library catalog, so cards sit under their\n group heading (the app's own modules first). A backend without groups yields one unnamed group,\n which renders as the original flat grid. -->\n <div class=\"module-section\" *ngFor=\"let mgroup of moduleGroups\">\n <div class=\"module-section-head\">\n <span class=\"module-section-name\">{{ mgroup.name }}</span>\n <span class=\"module-section-count\">{{ mgroup.enabled }} of {{ mgroup.total }} on</span>\n </div>\n <div class=\"module-grid\">\n <div class=\"module-card\" *ngFor=\"let mod of mgroup.modules\"\n [class.enabled]=\"mod.enabled\" [class.core]=\"mod.core\" [class.busy]=\"togglingKey === mod.key\"\n (click)=\"toggleModule(mod)\">\n <div class=\"module-head\">\n <mat-icon class=\"module-icon\">{{ mod.icon || 'extension' }}</mat-icon>\n <mat-icon class=\"module-state\" [class.on]=\"mod.enabled\">{{ mod.enabled ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n </div>\n <div class=\"module-title\">{{ mod.title }}</div>\n <div class=\"module-description\">{{ mod.description }}</div>\n <span class=\"module-chip\" *ngIf=\"mod.core\">Always on</span>\n </div>\n </div>\n </div>\n <div class=\"module-actions\" *ngIf=\"!modulesConfirmed\">\n <button mat-flat-button color=\"primary\" [disabled]=\"togglingKey !== ''\" (click)=\"confirmModules()\">Confirm selection</button>\n <span class=\"module-hint\">Happy with this selection? Confirm it to complete the step below.</span>\n </div>\n </mat-card>\n\n <!-- Category groups, top-down \u2014 Changed: flat spa-checklist replaces the raised mat-accordion -->\n <mat-card class=\"setup-group\" *ngFor=\"let group of groups\">\n <div class=\"group-header\">\n <h2>{{ group.name }}</h2>\n <span class=\"group-counter\">{{ group.completed }} of {{ group.total }}</span>\n </div>\n <spa-checklist [config]=\"group.config\" [itemTemplate]=\"stepExtrasTpl\"></spa-checklist>\n </mat-card>\n\n <!-- Demo data (opt-in via setupConfig.demoData) \u2014 moved here off the per-app configuration pages -->\n <mat-card class=\"setup-group setup-demo\" *ngIf=\"showDemoData\">\n <div class=\"group-header\">\n <h2>Demo data</h2>\n <span class=\"group-counter\">Explore with example records, then clear them out</span>\n </div>\n <p class=\"demo-description\">Seeding fills the system with example records so you can try it out before capturing anything real. Removing deletes only those example records.</p>\n <div class=\"demo-actions\">\n <button mat-flat-button color=\"primary\" [disabled]=\"demoBusy\" (click)=\"seedDemoData()\"><mat-icon>add_circle</mat-icon> Seed demo data</button>\n <button mat-stroked-button color=\"warn\" [disabled]=\"demoBusy\" (click)=\"removeDemoData()\"><mat-icon>delete</mat-icon> Remove demo data</button>\n </div>\n </mat-card>\n\n</div>\n\n<!-- Projected into the expanded checklist body: predefined roles picker on the roles step (v2) -->\n<ng-template #stepExtrasTpl let-item>\n <div class=\"role-templates\" *ngIf=\"hasRoleTemplates(item.data) && roleTemplates.length > 0\">\n <div class=\"role-template\" *ngFor=\"let tpl of roleTemplates\" [class.created]=\"tpl.exists\" [class.selected]=\"!tpl.exists && selectedTemplates[tpl.key]\" (click)=\"toggleTemplate(tpl)\">\n <div class=\"role-head\">\n <mat-icon class=\"role-state\" [class.on]=\"tpl.exists || selectedTemplates[tpl.key]\">{{ (tpl.exists || selectedTemplates[tpl.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"role-name\">{{ tpl.name }}</span>\n <span class=\"role-created\" *ngIf=\"tpl.exists\"><mat-icon>check</mat-icon>Created</span>\n </div>\n <div class=\"role-description\">{{ tpl.description }}</div>\n </div>\n </div>\n\n <!-- Added (presets): the preset picker. Same row as the role picker above \u2014 same state icon, same 8px stack,\n same colours \u2014 plus ONE sentence and ONE control.\n Changed (presets P4): rendered through ONE shared template for both the notification step and the sign-off\n step, parameterised by the picker object. The two sit one above the other on the same page, so a second\n near-identical block would drift and every difference would read as a mistake. -->\n <ng-container *ngIf=\"pickerFor(item.data) as picker\">\n <ng-container *ngTemplateOutlet=\"presetPickerTpl; context: { $implicit: picker }\"></ng-container>\n </ng-container>\n</ng-template>\n\n<!-- The one picker body. `picker` carries the domain: its rows, its roles, its wording, its apply call. -->\n<ng-template #presetPickerTpl let-picker>\n <div class=\"preset-picker\" *ngIf=\"picker.loaded && picker.presets.length > 0\">\n\n <!-- No selectable role \u2192 we ask for one instead of offering ticks that would build an inert rule. The wording\n is the picker's own, because \"no roles\" and \"roles nobody is in\" are different situations. -->\n <div class=\"preset-empty\" *ngIf=\"picker.roles.length === 0\">{{ picker.emptyText }}</div>\n\n <ng-container *ngIf=\"picker.roles.length > 0\">\n <div class=\"preset-section\" *ngFor=\"let pgroup of picker.groups\">\n <div class=\"preset-section-head\">\n <span class=\"preset-section-name\">{{ pgroup.name }}</span>\n <span class=\"preset-section-count\">{{ pgroup.on }} of {{ pgroup.total }}</span>\n </div>\n <div class=\"preset-row\" *ngFor=\"let row of pgroup.rows\" role=\"checkbox\" tabindex=\"0\"\n [class.configured]=\"row.preset.exists\" [class.selected]=\"!row.preset.exists && picker.selected[row.preset.key]\"\n [attr.aria-checked]=\"row.preset.exists || !!picker.selected[row.preset.key]\" [attr.aria-disabled]=\"row.preset.exists\"\n (click)=\"togglePreset(picker, row.preset)\"\n (keydown.enter)=\"togglePreset(picker, row.preset)\"\n (keydown.space)=\"togglePreset(picker, row.preset); $event.preventDefault()\">\n <div class=\"preset-head\">\n <mat-icon class=\"preset-state\" [class.on]=\"row.preset.exists || picker.selected[row.preset.key]\">{{ (row.preset.exists || picker.selected[row.preset.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"preset-name\">{{ row.preset.name }}</span>\n <span class=\"preset-configured\" *ngIf=\"row.preset.exists\"><mat-icon>check</mat-icon>Configured</span>\n </div>\n <div class=\"preset-description\">{{ row.preset.description }}</div>\n <!-- The effect sentence comes from the server whole; `before` and `after` are its two halves either side\n of the {role} placeholder, so reading the line IS reading the configuration. The sign-off sentences\n carry their own deferred-notification caveat, which is why there is no extra note here. -->\n <div class=\"preset-effect\" *ngIf=\"!row.preset.exists\">{{ row.before }}<select class=\"preset-role\" [attr.aria-label]=\"picker.roleAria + row.preset.name\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n (change)=\"setPresetRole(picker, row.preset, $any($event.target).value)\">\n <option *ngFor=\"let role of picker.roles\" [value]=\"role.value\" [selected]=\"role.value === picker.role[row.preset.key]\">{{ role.name }}</option>\n </select>{{ row.after }}</div>\n <div class=\"preset-effect preset-done\" *ngIf=\"row.preset.exists\">{{ picker.doneText }}</div>\n </div>\n </div>\n\n <!-- One page-level channel choice, notifications only. In-app is the only channel that always works; SMS is a\n stub and is never offered; an approval request is always in-app, so the sign-off picker has no such line. -->\n <div class=\"preset-email\" *ngIf=\"picker.showEmail\" role=\"checkbox\" tabindex=\"0\" [attr.aria-checked]=\"includeEmail\"\n (click)=\"toggleIncludeEmail()\"\n (keydown.enter)=\"toggleIncludeEmail()\"\n (keydown.space)=\"toggleIncludeEmail(); $event.preventDefault()\">\n <mat-icon class=\"preset-state\" [class.on]=\"includeEmail\">{{ includeEmail ? 'check_box' : 'check_box_outline_blank' }}</mat-icon>\n <span class=\"preset-email-label\">Also send these by email</span>\n <span class=\"preset-email-hint\">In-app always. Email needs your mail settings.</span>\n </div>\n </ng-container>\n </div>\n</ng-template>\n\n<!-- Graceful empty state (feature disabled or status unavailable) -->\n<div class=\"setup-empty\" *ngIf=\"!status\">\n <mat-icon>rocket_launch</mat-icon>\n <p>Setup status is not available yet.</p>\n</div>\n", styles: [".setup-page{max-width:860px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.setup-hero{padding:24px}.hero-content{display:flex;align-items:center;justify-content:space-between;gap:24px;flex-wrap:wrap}.hero-text h1{margin:0 0 4px;font-size:24px}.hero-text p{margin:0 0 8px;color:#0009}.hero-counter{font-size:13px;color:#0009}.hero-progress{flex:1;min-width:220px;max-width:340px}.hero-percent{display:block;font-size:28px;font-weight:600;color:#2e7d32;margin-bottom:6px;text-align:right}.hero-progress mat-progress-bar{height:10px;border-radius:5px}.setup-hero.celebrate{background:linear-gradient(135deg,#e8f5e9,#f1f8e9)}.celebration{justify-content:flex-start}.celebrate-icon{font-size:48px;width:48px;height:48px;color:#2e7d32;animation:celebrate-pop .6s ease-out}@keyframes celebrate-pop{0%{transform:scale(.3);opacity:0}70%{transform:scale(1.15)}to{transform:scale(1);opacity:1}}.setup-group{padding:16px}.group-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.group-header h2{margin:0;font-size:17px}.group-counter{font-size:12px;color:#0000008c}.setup-modules{padding:16px}.module-section+.module-section{margin-top:18px}.module-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.module-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.module-section-count{font-size:11px;color:#00000073;white-space:nowrap}.module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;margin-top:8px}.module-card{border:1px solid rgba(0,0,0,.12);border-radius:10px;padding:12px;cursor:pointer;transition:border-color .15s,background .15s,opacity .15s;opacity:.72;position:relative}.module-card:hover{border-color:#90a4ae}.module-card.enabled{border-color:#4caf50;background:#f6fbf6;opacity:1}.module-card.core{cursor:default}.module-card.busy{pointer-events:none;opacity:.5}.module-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.module-icon{color:#546e7a}.module-card.enabled .module-icon{color:#2e7d32}.module-state{color:#b0bec5}.module-state.on{color:#4caf50}.module-title{font-weight:600;font-size:14px;margin-bottom:4px}.module-description{font-size:12px;color:#0009;min-height:30px}.module-chip{display:inline-block;margin-top:8px;background:#eceff1;color:#546e7a;border-radius:12px;padding:2px 10px;font-size:11px}.module-actions{display:flex;align-items:center;gap:8px 12px;margin-top:14px;flex-wrap:wrap}.module-actions button{flex-shrink:0}.module-hint{font-size:12px;color:#0000008c}.role-templates{display:flex;flex-direction:column;gap:8px;margin:0 0 12px}.role-template{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.role-template:hover{border-color:#90a4ae}.role-template.selected{border-color:#4caf50;background:#f6fbf6}.role-template.created{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.role-head{display:flex;align-items:center;gap:8px}.role-state{color:#b0bec5;flex-shrink:0}.role-state.on{color:#4caf50}.role-name{font-weight:500;font-size:13px}.role-created{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32}.role-created mat-icon{font-size:16px;width:16px;height:16px}.role-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-picker{display:flex;flex-direction:column;gap:14px;margin:0 0 12px}.preset-empty{font-size:12px;line-height:18px;color:#0009;max-width:62ch}.preset-section{display:flex;flex-direction:column;gap:8px}.preset-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.preset-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.preset-section-count{font-size:11px;color:#00000073;white-space:nowrap}.preset-row{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.preset-row:hover{border-color:#90a4ae}.preset-row:focus-visible{outline:2px solid #4caf50;outline-offset:2px}.preset-row.selected{border-color:#4caf50;background:#f6fbf6}.preset-row.configured{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.preset-head{display:flex;align-items:center;gap:8px}.preset-state{color:#b0bec5;flex-shrink:0}.preset-state.on{color:#4caf50}.preset-name{font-weight:500;font-size:13px}.preset-configured{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32;white-space:nowrap}.preset-configured mat-icon{font-size:16px;width:16px;height:16px}.preset-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-effect{font-size:12px;line-height:22px;color:#000000c7;margin:4px 0 0 32px}.preset-done{color:#2e7d32}.preset-role{font-family:inherit;font-size:12px;font-weight:500;color:#2e7d32;background:#fff;border:1px solid #c8e6c9;border-radius:6px;padding:2px 4px;margin:0 2px;max-width:100%;cursor:pointer}.preset-role:hover{border-color:#4caf50}.preset-role:focus-visible{outline:2px solid #4caf50;outline-offset:1px}.preset-email{display:flex;align-items:center;gap:8px;flex-wrap:wrap;cursor:pointer;padding:2px 0}.preset-email:focus-visible{outline:2px solid #4caf50;outline-offset:2px;border-radius:6px}.preset-email-label{font-size:13px;font-weight:500}.preset-email-hint{font-size:12px;color:#0000008c}.setup-empty{text-align:center;padding:48px 16px;color:#00000080}.setup-empty mat-icon{font-size:40px;width:40px;height:40px}.demo-description{margin:8px 0 14px;font-size:13px;color:#0009}.demo-actions{display:flex;gap:12px;flex-wrap:wrap}@media (max-width: 700px){.module-hint{flex:0 0 100%}.preset-role{display:block;width:100%;margin:4px 0 0;padding:6px 8px}.preset-email-hint{flex:0 0 100%}}\n"] }]
|
|
12522
|
+
args: [{ selector: 'spa-setup-guide', standalone: false, template: "<div class=\"setup-page\" *ngIf=\"status\">\n\n <!-- Hero: overall readiness + celebration state at 100% -->\n <mat-card class=\"setup-hero\" [class.celebrate]=\"status.percent === 100\">\n <div class=\"hero-content\" *ngIf=\"status.percent < 100\">\n <div class=\"hero-text\">\n <h1>{{ title }}</h1>\n <p>Complete these steps to get your system ready for day-to-day operation.</p>\n <span class=\"hero-counter\">{{ status.completed }} of {{ status.total }} steps completed</span>\n </div>\n <div class=\"hero-progress\">\n <span class=\"hero-percent\">{{ status.percent }}%</span>\n <mat-progress-bar mode=\"determinate\" [value]=\"status.percent\"></mat-progress-bar>\n </div>\n </div>\n <div class=\"hero-content celebration\" *ngIf=\"status.percent === 100\">\n <mat-icon class=\"celebrate-icon\">celebration</mat-icon>\n <div class=\"hero-text\">\n <h1>You're all set!</h1>\n <p>All setup steps are complete \u2014 your system is ready to operate.</p>\n </div>\n </div>\n </mat-card>\n\n <!-- Module picker (v2): choose what the business uses; optional modules toggle steps + menus -->\n <mat-card class=\"setup-modules\" *ngIf=\"modules.length > 0\">\n <div class=\"group-header\">\n <h2>Your modules</h2>\n <span class=\"group-counter\">Tap a module to turn it on or off \u2014 you can change this anytime.</span>\n </div>\n <!-- Changed (v3): grouped \u2014 the picker now carries the whole library catalog, so cards sit under their\n group heading (the app's own modules first). A backend without groups yields one unnamed group,\n which renders as the original flat grid. -->\n <div class=\"module-section\" *ngFor=\"let mgroup of moduleGroups\">\n <div class=\"module-section-head\">\n <span class=\"module-section-name\">{{ mgroup.name }}</span>\n <span class=\"module-section-count\">{{ mgroup.enabled }} of {{ mgroup.total }} on</span>\n </div>\n <div class=\"module-grid\">\n <div class=\"module-card\" *ngFor=\"let mod of mgroup.modules\"\n [class.enabled]=\"mod.enabled\" [class.core]=\"mod.core\" [class.busy]=\"togglingKey === mod.key\"\n (click)=\"toggleModule(mod)\">\n <div class=\"module-head\">\n <mat-icon class=\"module-icon\">{{ mod.icon || 'extension' }}</mat-icon>\n <mat-icon class=\"module-state\" [class.on]=\"mod.enabled\">{{ mod.enabled ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n </div>\n <div class=\"module-title\">{{ mod.title }}</div>\n <div class=\"module-description\">{{ mod.description }}</div>\n <span class=\"module-chip\" *ngIf=\"mod.core\">Always on</span>\n </div>\n </div>\n </div>\n <div class=\"module-actions\" *ngIf=\"!modulesConfirmed\">\n <button mat-flat-button color=\"primary\" [disabled]=\"togglingKey !== ''\" (click)=\"confirmModules()\">Confirm selection</button>\n <span class=\"module-hint\">Happy with this selection? Confirm it to complete the step below.</span>\n </div>\n </mat-card>\n\n <!-- Category groups, top-down \u2014 Changed: flat spa-checklist replaces the raised mat-accordion -->\n <mat-card class=\"setup-group\" *ngFor=\"let group of groups\">\n <div class=\"group-header\">\n <h2>{{ group.name }}</h2>\n <span class=\"group-counter\">{{ group.completed }} of {{ group.total }}</span>\n </div>\n <spa-checklist [config]=\"group.config\" [itemTemplate]=\"stepExtrasTpl\"></spa-checklist>\n </mat-card>\n\n <!-- Demo data (opt-in via setupConfig.demoData) \u2014 moved here off the per-app configuration pages -->\n <mat-card class=\"setup-group setup-demo\" *ngIf=\"showDemoData\">\n <div class=\"group-header\">\n <h2>Demo data</h2>\n <span class=\"group-counter\">Explore with example records, then clear them out</span>\n </div>\n <p class=\"demo-description\">Seeding fills the system with example records so you can try it out before capturing anything real. Removing deletes only those example records.</p>\n <div class=\"demo-actions\">\n <button mat-flat-button color=\"primary\" [disabled]=\"demoBusy\" (click)=\"seedDemoData()\"><mat-icon>add_circle</mat-icon> Seed demo data</button>\n <button mat-stroked-button color=\"warn\" [disabled]=\"demoBusy\" (click)=\"removeDemoData()\"><mat-icon>delete</mat-icon> Remove demo data</button>\n </div>\n </mat-card>\n\n</div>\n\n<!-- Projected into the expanded checklist body: predefined roles picker on the roles step (v2) -->\n<ng-template #stepExtrasTpl let-item>\n <div class=\"role-templates\" *ngIf=\"hasRoleTemplates(item.data) && roleTemplates.length > 0\">\n <div class=\"role-template\" *ngFor=\"let tpl of roleTemplates\" [class.created]=\"tpl.exists\" [class.selected]=\"!tpl.exists && selectedTemplates[tpl.key]\" (click)=\"toggleTemplate(tpl)\">\n <div class=\"role-head\">\n <mat-icon class=\"role-state\" [class.on]=\"tpl.exists || selectedTemplates[tpl.key]\">{{ (tpl.exists || selectedTemplates[tpl.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"role-name\">{{ tpl.name }}</span>\n <span class=\"role-created\" *ngIf=\"tpl.exists\"><mat-icon>check</mat-icon>Created</span>\n </div>\n <div class=\"role-description\">{{ tpl.description }}</div>\n </div>\n </div>\n\n <!-- Added (presets): the preset picker. Same row as the role picker above \u2014 same state icon, same 8px stack,\n same colours \u2014 plus ONE sentence and ONE control.\n Changed (presets P4): rendered through ONE shared template for both the notification step and the sign-off\n step, parameterised by the picker object. The two sit one above the other on the same page, so a second\n near-identical block would drift and every difference would read as a mistake. -->\n <ng-container *ngIf=\"pickerFor(item.data) as picker\">\n <ng-container *ngTemplateOutlet=\"presetPickerTpl; context: { $implicit: picker }\"></ng-container>\n </ng-container>\n</ng-template>\n\n<!-- The one picker body. `picker` carries the domain: its rows, its roles, its wording, its apply call. -->\n<ng-template #presetPickerTpl let-picker>\n <div class=\"preset-picker\" *ngIf=\"picker.loaded && picker.presets.length > 0\">\n\n <!-- No selectable role \u2192 we ask for one instead of offering ticks that would build an inert rule. The wording\n is the picker's own, because \"no roles\" and \"roles nobody is in\" are different situations. -->\n <div class=\"preset-empty\" *ngIf=\"picker.roles.length === 0\">{{ picker.emptyText }}</div>\n\n <ng-container *ngIf=\"picker.roles.length > 0\">\n <div class=\"preset-section\" *ngFor=\"let pgroup of picker.groups\">\n <div class=\"preset-section-head\">\n <span class=\"preset-section-name\">{{ pgroup.name }}</span>\n <span class=\"preset-section-count\">{{ pgroup.on }} of {{ pgroup.total }}</span>\n </div>\n <div class=\"preset-row\" *ngFor=\"let row of pgroup.rows\" role=\"checkbox\" tabindex=\"0\"\n [class.configured]=\"row.preset.exists\" [class.selected]=\"!row.preset.exists && picker.selected[row.preset.key]\"\n [attr.aria-checked]=\"row.preset.exists || !!picker.selected[row.preset.key]\" [attr.aria-disabled]=\"row.preset.exists\"\n (click)=\"togglePreset(picker, row.preset)\"\n (keydown.enter)=\"togglePreset(picker, row.preset)\"\n (keydown.space)=\"togglePreset(picker, row.preset); $event.preventDefault()\">\n <div class=\"preset-head\">\n <mat-icon class=\"preset-state\" [class.on]=\"row.preset.exists || picker.selected[row.preset.key]\">{{ (row.preset.exists || picker.selected[row.preset.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"preset-name\">{{ row.preset.name }}</span>\n <span class=\"preset-configured\" *ngIf=\"row.preset.exists\"><mat-icon>check</mat-icon>Configured</span>\n </div>\n <div class=\"preset-description\">{{ row.preset.description }}</div>\n <!-- The effect sentence comes from the server whole; `before` and `after` are its two halves either side\n of the {role} placeholder, so reading the line IS reading the configuration. The sign-off sentences\n carry their own deferred-notification caveat, which is why there is no extra note here. -->\n <div class=\"preset-effect\" *ngIf=\"!row.preset.exists\">{{ row.before }}<select class=\"preset-role\" [attr.aria-label]=\"picker.roleAria + row.preset.name\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n (change)=\"setPresetRole(picker, row.preset, $any($event.target).value)\">\n <option *ngFor=\"let role of picker.roles\" [value]=\"role.value\" [selected]=\"role.value === picker.role[row.preset.key]\">{{ role.name }}</option>\n </select>{{ row.after }}</div>\n <div class=\"preset-effect preset-done\" *ngIf=\"row.preset.exists\">{{ picker.doneText }}</div>\n </div>\n </div>\n\n <!-- One page-level channel choice, notifications only. In-app is the only channel that always works; SMS is a\n stub and is never offered; an approval request is always in-app, so the sign-off picker has no such line. -->\n <div class=\"preset-email\" *ngIf=\"picker.showEmail\" role=\"checkbox\" tabindex=\"0\" [attr.aria-checked]=\"includeEmail\"\n (click)=\"toggleIncludeEmail()\"\n (keydown.enter)=\"toggleIncludeEmail()\"\n (keydown.space)=\"toggleIncludeEmail(); $event.preventDefault()\">\n <mat-icon class=\"preset-state\" [class.on]=\"includeEmail\">{{ includeEmail ? 'check_box' : 'check_box_outline_blank' }}</mat-icon>\n <span class=\"preset-email-label\">Also send these by email</span>\n <span class=\"preset-email-hint\">In-app always. Email needs your mail settings.</span>\n </div>\n </ng-container>\n </div>\n</ng-template>\n\n<!-- Graceful empty state (feature disabled or status unavailable) -->\n<div class=\"setup-empty\" *ngIf=\"!status\">\n <mat-icon>rocket_launch</mat-icon>\n <p>Setup status is not available yet.</p>\n</div>\n", styles: [".setup-page{max-width:860px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.setup-hero{padding:24px}.hero-content{display:flex;align-items:center;justify-content:space-between;gap:24px;flex-wrap:wrap}.hero-text h1{margin:0 0 4px;font-size:24px}.hero-text p{margin:0 0 8px;color:#0009}.hero-counter{font-size:13px;color:#0009}.hero-progress{flex:1;min-width:220px;max-width:340px}.hero-percent{display:block;font-size:28px;font-weight:600;color:#2e7d32;margin-bottom:6px;text-align:right}.hero-progress mat-progress-bar{height:10px;border-radius:5px}.setup-hero.celebrate{background:linear-gradient(135deg,#e8f5e9,#f1f8e9)}.celebration{justify-content:flex-start}.celebrate-icon{font-size:48px;width:48px;height:48px;color:#2e7d32;animation:celebrate-pop .6s ease-out}@keyframes celebrate-pop{0%{transform:scale(.3);opacity:0}70%{transform:scale(1.15)}to{transform:scale(1);opacity:1}}.setup-group{padding:16px}.group-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.group-header h2{margin:0;font-size:17px}.group-counter{font-size:12px;color:#0000008c}.setup-modules{padding:16px}.module-section+.module-section{margin-top:18px}.module-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.module-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.module-section-count{font-size:11px;color:#00000073;white-space:nowrap}.module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;margin-top:8px}.module-card{border:1px solid rgba(0,0,0,.12);border-radius:10px;padding:12px;cursor:pointer;transition:border-color .15s,background .15s,opacity .15s;opacity:.72;position:relative}.module-card:hover{border-color:#90a4ae}.module-card.enabled{border-color:#4caf50;background:#f6fbf6;opacity:1}.module-card.core{cursor:default}.module-card.busy{pointer-events:none;opacity:.5}.module-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.module-icon{color:#546e7a}.module-card.enabled .module-icon{color:#2e7d32}.module-state{color:#b0bec5}.module-state.on{color:#4caf50}.module-title{font-weight:600;font-size:14px;margin-bottom:4px}.module-description{font-size:12px;color:#0009;min-height:30px}.module-chip{display:inline-block;margin-top:8px;background:#eceff1;color:#546e7a;border-radius:12px;padding:2px 10px;font-size:11px}.module-actions{display:flex;align-items:center;gap:8px 12px;margin-top:14px;flex-wrap:wrap}.module-actions button{flex-shrink:0}.module-hint{font-size:12px;color:#0000008c}.role-templates{display:flex;flex-direction:column;gap:8px;margin:0 0 12px}.role-template{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.role-template:hover{border-color:#90a4ae}.role-template.selected{border-color:#4caf50;background:#f6fbf6}.role-template.created{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.role-head{display:flex;align-items:center;gap:8px}.role-state{color:#b0bec5;flex-shrink:0}.role-state.on{color:#4caf50}.role-name{font-weight:500;font-size:13px}.role-created{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32}.role-created mat-icon{font-size:16px;width:16px;height:16px}.role-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-picker{display:flex;flex-direction:column;gap:14px;margin:0 0 12px}.preset-empty{font-size:12px;line-height:18px;color:#0009;max-width:62ch}.preset-section{display:flex;flex-direction:column;gap:8px}.preset-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.preset-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.preset-section-count{font-size:11px;color:#00000073;white-space:nowrap}.preset-row{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.preset-row:hover{border-color:#90a4ae}.preset-row:focus-visible{outline:2px solid #4caf50;outline-offset:2px}.preset-row.selected{border-color:#4caf50;background:#f6fbf6}.preset-row.configured{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.preset-head{display:flex;align-items:center;gap:8px}.preset-state{color:#b0bec5;flex-shrink:0}.preset-state.on{color:#4caf50}.preset-name{font-weight:500;font-size:13px}.preset-configured{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32;white-space:nowrap}.preset-configured mat-icon{font-size:16px;width:16px;height:16px}.preset-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-effect{font-size:12px;line-height:22px;color:#000000c7;margin:4px 0 0 32px}.preset-done{color:#2e7d32}.preset-role{font-family:inherit;font-size:12px;font-weight:500;color:#2e7d32;background:#fff;border:1px solid #c8e6c9;border-radius:6px;padding:2px 4px;margin:0 2px;max-width:100%;cursor:pointer}.preset-role:hover{border-color:#4caf50}.preset-role:focus-visible{outline:2px solid #4caf50;outline-offset:1px}.preset-email{display:flex;align-items:center;gap:8px;flex-wrap:wrap;cursor:pointer;padding:2px 0}.preset-email:focus-visible{outline:2px solid #4caf50;outline-offset:2px;border-radius:6px}.preset-email-label{font-size:13px;font-weight:500}.preset-email-hint{font-size:12px;color:#0000008c}.setup-empty{text-align:center;padding:48px 16px;color:#00000080}.setup-empty mat-icon{font-size:40px;width:40px;height:40px}.demo-description{margin:8px 0 14px;font-size:13px;color:#0009}.demo-actions{display:flex;gap:12px;flex-wrap:wrap}@media (max-width: 700px){.setup-page{padding:10px 0;gap:12px}.setup-hero{padding:12px}.setup-modules,.setup-group{padding:10px}.hero-content{gap:10px}.hero-text h1{font-size:20px}.setup-hero:not(.celebrate) .hero-text p{display:none}.hero-counter{font-size:12px}.hero-progress{min-width:0;max-width:none}.hero-percent{font-size:22px;margin-bottom:4px}.hero-progress mat-progress-bar{height:8px;border-radius:4px}.celebrate-icon{font-size:36px;width:36px;height:36px}.group-header{margin-bottom:6px;gap:2px 8px}.group-header h2{font-size:15px}.group-counter{font-size:11px}.setup-modules>.group-header,.setup-demo>.group-header{flex-wrap:wrap}.setup-modules>.group-header .group-counter,.setup-demo>.group-header .group-counter{flex:1 0 100%}.module-section+.module-section{margin-top:12px}.module-section-head{padding-bottom:4px}.module-grid{gap:8px;margin-top:6px}.module-card{padding:10px}.module-head{margin-bottom:4px}.module-title{font-size:13.5px;margin-bottom:2px}.module-description{min-height:0}.module-chip{margin-top:6px}.module-actions{margin-top:10px}.role-templates,.preset-picker{gap:6px;margin-bottom:8px}.role-template,.preset-row{padding:8px 10px}.role-description,.preset-description,.preset-effect{margin-left:0}.preset-section{gap:6px}.preset-section-head{padding-bottom:4px}.demo-description{margin:6px 0 10px;font-size:12.5px}.demo-actions{gap:8px}.setup-empty{padding:28px 12px}.module-hint{flex:0 0 100%}.preset-role{display:block;width:100%;margin:4px 0 0;padding:6px 8px}.preset-email-hint{flex:0 0 100%}}\n"] }]
|
|
12436
12523
|
}] });
|
|
12437
12524
|
|
|
12438
12525
|
// Quiet Loading — perceived-progress engine (FSD D1).
|
|
@@ -13342,7 +13429,7 @@ class TextSingleComponent {
|
|
|
13342
13429
|
let button = this.detailsConfig.buttons.find(b => b.name === mode);
|
|
13343
13430
|
button.detailsConfig = this.detailsConfig;
|
|
13344
13431
|
this.dialogService.openDefaultDetailsDialog(button, dynamicData).subscribe(result => {
|
|
13345
|
-
if (result
|
|
13432
|
+
if (result?.message === 'success') { // Changed: a dismissed quick-add dialog closes with undefined — read through it rather than throwing
|
|
13346
13433
|
// Added (dropdown quick-add): auto-select the record the user just created; refresh's initFilter
|
|
13347
13434
|
// then resolves the display text once the new option is in the list
|
|
13348
13435
|
if (mode === 'create')
|
|
@@ -13542,6 +13629,9 @@ class SelectCommonComponent {
|
|
|
13542
13629
|
this.loadIndicator = new FieldLoadIndicator();
|
|
13543
13630
|
// Added (dropdown quick-add): sentinel value for the always-first "add new" option — never a real option value
|
|
13544
13631
|
this.ADD_NEW_OPTION = "__spa_add_new__";
|
|
13632
|
+
// Added: the deferred default must not fire into a torn-down field (a dialog closed while its options were
|
|
13633
|
+
// still settling would otherwise write onto data nobody is showing any more)
|
|
13634
|
+
this.destroyed = false;
|
|
13545
13635
|
this.width = "100%";
|
|
13546
13636
|
this.readonly = false;
|
|
13547
13637
|
this.required = true;
|
|
@@ -13579,6 +13669,24 @@ class SelectCommonComponent {
|
|
|
13579
13669
|
// Added (dropdown quick-add): keep the restore point in sync with parent-driven value changes
|
|
13580
13670
|
if (this.value !== this.ADD_NEW_OPTION)
|
|
13581
13671
|
this.previousValue = this.value;
|
|
13672
|
+
// Added: defaultFirstValue was inert on every select that names a masterField. setDefaultValue() has only
|
|
13673
|
+
// two entry points — ngOnInit (options are still empty then) and the field.optionsSubject subscription —
|
|
13674
|
+
// and form.component.ts only creates an optionsSubject for fields WITHOUT a masterField, so a child field
|
|
13675
|
+
// has no second entry point at all. Its options arrive later, as a plain [options] input mutation from
|
|
13676
|
+
// updateChildOptions(), which lands here in ngOnChanges and nowhere else. Reproduced on shift-spa's driver
|
|
13677
|
+
// create dialog: Department self-selected, Position stayed empty with exactly one option in its list.
|
|
13678
|
+
// Deliberately scoped to masterField selects: every other select already gets its default through one of
|
|
13679
|
+
// the two existing entry points, so nothing else's timing moves. setDefaultValue() still declines when a
|
|
13680
|
+
// value is already set, so this can only fill a blank, never overwrite a choice.
|
|
13681
|
+
// Deferred by one microtask on purpose: setDefaultValue() emits valueChange, and emitting from inside
|
|
13682
|
+
// ngOnChanges writes the parent's data[field.name] after the parent's bindings were already checked, which
|
|
13683
|
+
// is NG0100 — the same shape spa-select-multi's selectAll had. The microtask runs once the pass has
|
|
13684
|
+
// finished, so the new value is picked up by the next one instead of invalidating this one.
|
|
13685
|
+
if (this.masterField && this.options !== this.defaultedOptions) {
|
|
13686
|
+
this.defaultedOptions = this.options;
|
|
13687
|
+
Promise.resolve().then(() => { if (!this.destroyed)
|
|
13688
|
+
this.setDefaultValue(); });
|
|
13689
|
+
}
|
|
13582
13690
|
if (this.readonlyMode != "" && this.options.length > 0) {
|
|
13583
13691
|
this.displayValue = this.options.filter((m) => m[`${this.optionValue}`] == this.value)[0][`${this.optionDisplay}`];
|
|
13584
13692
|
}
|
|
@@ -13749,6 +13857,7 @@ class SelectCommonComponent {
|
|
|
13749
13857
|
});
|
|
13750
13858
|
}
|
|
13751
13859
|
ngOnDestroy() {
|
|
13860
|
+
this.destroyed = true; // Added: cancels a master-driven default still queued on the microtask queue
|
|
13752
13861
|
if (this.subscription) {
|
|
13753
13862
|
this.subscription.unsubscribe();
|
|
13754
13863
|
}
|
|
@@ -13858,8 +13967,21 @@ class TextMultiComponent {
|
|
|
13858
13967
|
this.loadIndicator = new FieldLoadIndicator();
|
|
13859
13968
|
this.hoverChange = new EventEmitter();
|
|
13860
13969
|
} // Changed: injected ApiErrorService so a failed option load is no longer silent
|
|
13970
|
+
// Added: identical normalisation to SelectMultiComponent.toValueList, deliberately kept the same shape and
|
|
13971
|
+
// name so the two multi-value components cannot drift. `value` is documented as a ';'-delimited string, but
|
|
13972
|
+
// the truthiness guard below used to hand anything truthy straight to .split() — an array (any form whose
|
|
13973
|
+
// backend DTO takes a List<int> rewrites the bound property in place before posting) or a bare scalar both
|
|
13974
|
+
// threw "split is not a function". select-multi hit exactly that in piglet; nothing feeds text-multi an array
|
|
13975
|
+
// today, so this is hardening ahead of the first caller that does, not a repair.
|
|
13976
|
+
toValueList(raw) {
|
|
13977
|
+
if (Array.isArray(raw))
|
|
13978
|
+
return raw.filter(v => v !== null && v !== undefined && String(v).trim() !== '').map(v => String(v).trim()); // Added: the List<int> shape
|
|
13979
|
+
if (!raw)
|
|
13980
|
+
return []; // Added: preserves the previous falsy-means-nothing-entered rule for null/undefined/''
|
|
13981
|
+
return String(raw).split(';').filter(v => v.trim() !== '').map(v => v.trim()); // Changed: String() first, so a bare scalar no longer throws either
|
|
13982
|
+
}
|
|
13861
13983
|
ngOnInit() {
|
|
13862
|
-
this.values = this.value
|
|
13984
|
+
this.values = this.toValueList(this.value); // Changed: was this.value.split(';') directly on an @Input of unknown shape
|
|
13863
13985
|
this.setupAutoComplete();
|
|
13864
13986
|
this.getData(this.loadAction);
|
|
13865
13987
|
}
|
|
@@ -14057,6 +14179,7 @@ class SelectMultiComponent {
|
|
|
14057
14179
|
this.selectAll = false; // Changed: auto-select all options when no value is set
|
|
14058
14180
|
this.control = new FormControl([]);
|
|
14059
14181
|
this.selectedValues = [];
|
|
14182
|
+
this.destroyed = false; // Added: a field torn down before the microtask runs must not emit into a dead parent
|
|
14060
14183
|
this.isHovered = false;
|
|
14061
14184
|
// Added: drives the suffix refresh icon — held visible and spinning while the loadAction is in flight
|
|
14062
14185
|
this.loadIndicator = new FieldLoadIndicator();
|
|
@@ -14086,13 +14209,23 @@ class SelectMultiComponent {
|
|
|
14086
14209
|
}
|
|
14087
14210
|
this.initializeValues();
|
|
14088
14211
|
}
|
|
14212
|
+
// Added: `value` is documented as a ';'-delimited string, but real callers legitimately hold an array —
|
|
14213
|
+
// any form whose backend DTO takes a List<int> rewrites the bound property in place before posting
|
|
14214
|
+
// (piglet has four: wean, sell, move, treat). That array flowed straight back down [(value)] and
|
|
14215
|
+
// this.value.split(';') threw "split is not a function" on every subsequent change-detection pass.
|
|
14216
|
+
// Normalise on the way in rather than type-guarding at one call site, and keep emitting the ';' string
|
|
14217
|
+
// on the way out so no existing consumer's parsing changes.
|
|
14218
|
+
toValueList(raw) {
|
|
14219
|
+
if (Array.isArray(raw))
|
|
14220
|
+
return raw.filter(v => v !== null && v !== undefined && String(v).trim() !== '').map(v => String(v).trim()); // Added: the List<int> shape
|
|
14221
|
+
if (!raw)
|
|
14222
|
+
return []; // Added: preserves the previous falsy-means-nothing-selected rule for null/undefined/''
|
|
14223
|
+
return String(raw).split(';').filter(v => v.trim() !== '').map(v => v.trim()); // Changed: String() first, so a bare scalar (a single id) no longer throws either
|
|
14224
|
+
}
|
|
14089
14225
|
initializeValues() {
|
|
14090
|
-
|
|
14091
|
-
|
|
14092
|
-
|
|
14093
|
-
.map(v => this.options.find(opt => opt[this.optionValue]?.toString() === v)
|
|
14094
|
-
? v : null)
|
|
14095
|
-
.filter(v => v !== null);
|
|
14226
|
+
const requested = this.toValueList(this.value); // Changed: was this.value.split(';') directly on an @Input of unknown shape
|
|
14227
|
+
if (requested.length > 0) { // Changed: gate on the parsed request, not on truthiness of a raw value that may be an array
|
|
14228
|
+
const values = requested.filter(v => this.options?.some(opt => opt[this.optionValue]?.toString() === v)); // Changed: same "drop values not in options" rule, expressed without the map-to-null round trip
|
|
14096
14229
|
this.selectedValues = values;
|
|
14097
14230
|
this.control.setValue(values);
|
|
14098
14231
|
}
|
|
@@ -14101,13 +14234,24 @@ class SelectMultiComponent {
|
|
|
14101
14234
|
const allValues = this.options.map(opt => opt[this.optionValue]?.toString());
|
|
14102
14235
|
this.selectedValues = allValues;
|
|
14103
14236
|
this.control.setValue(allValues);
|
|
14104
|
-
this.updateValue()
|
|
14237
|
+
this.publishAutoSelection(); // Changed: was this.updateValue() — see publishAutoSelection for why the emit cannot happen here
|
|
14105
14238
|
}
|
|
14106
14239
|
else {
|
|
14107
14240
|
this.selectedValues = [];
|
|
14108
14241
|
this.control.setValue([]);
|
|
14109
14242
|
}
|
|
14110
14243
|
}
|
|
14244
|
+
// Added: initializeValues runs from ngOnChanges/ngOnInit — inside the parent's change-detection pass.
|
|
14245
|
+
// Emitting valueChange there writes the parent's bound property after its bindings were already checked,
|
|
14246
|
+
// which is exactly what NG0100 ExpressionChangedAfterItHasBeenCheckedError reports. It fired on every
|
|
14247
|
+
// selectAll open, independently of the array crash above. Deferring by one microtask moves the parent
|
|
14248
|
+
// write into the next turn. Same idiom as day-book.component.ts's showLoading.
|
|
14249
|
+
// Only the AUTOMATIC selection defers — selectionChange() still emits synchronously, because a user
|
|
14250
|
+
// click is not inside a checked pass and callers rely on the value being current when they read it.
|
|
14251
|
+
publishAutoSelection() {
|
|
14252
|
+
Promise.resolve().then(() => { if (!this.destroyed)
|
|
14253
|
+
this.updateValue(); });
|
|
14254
|
+
}
|
|
14111
14255
|
selectionChange(event) {
|
|
14112
14256
|
this.selectedValues = event.value || [];
|
|
14113
14257
|
this.updateValue();
|
|
@@ -14169,6 +14313,7 @@ class SelectMultiComponent {
|
|
|
14169
14313
|
});
|
|
14170
14314
|
}
|
|
14171
14315
|
ngOnDestroy() {
|
|
14316
|
+
this.destroyed = true; // Added: suppresses a deferred selectAll emit that lands after teardown
|
|
14172
14317
|
this.loadIndicator.destroy(); // Added: clear any pending delay/hold timer on a field torn down mid-read
|
|
14173
14318
|
}
|
|
14174
14319
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SelectMultiComponent, deps: [{ token: MessageService }, { token: DataServiceLib }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
@@ -15171,7 +15316,7 @@ class TableHeaderComponent {
|
|
|
15171
15316
|
this.customClick.emit("upload");
|
|
15172
15317
|
this.dialogService.openDefaultDialog(this.uploadDetailsDialog).afterClosed.subscribe(result => {
|
|
15173
15318
|
// console.log(result)
|
|
15174
|
-
if (result
|
|
15319
|
+
if (result?.message == 'success') { // Changed: a dismissed upload dialog closes with undefined — read through it rather than throwing
|
|
15175
15320
|
this.refreshClick.emit();
|
|
15176
15321
|
this.messageService.toast("Upload successful");
|
|
15177
15322
|
}
|
|
@@ -15314,11 +15459,11 @@ class CheckComponent {
|
|
|
15314
15459
|
this.infoClick.emit();
|
|
15315
15460
|
}
|
|
15316
15461
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CheckComponent, deps: [{ token: MessageService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15317
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: CheckComponent, isStandalone: false, selector: "spa-check", inputs: { readonly: "readonly", display: "display", value: "value", infoMessage: "infoMessage" }, outputs: { valueChange: "valueChange", click: "click", check: "check", uncheck: "uncheck", infoClick: "infoClick" }, ngImport: i0, template: "\n\n<mat-checkbox color=\"primary\" [(ngModel)]=\"value\" (change)=\"changed()\" (click)=\"clicked()\" [disabled]=\"readonly\">{{display}}</mat-checkbox>\n\n<!-- <div class=\"suffix-icons\">\n\n <mat-checkbox color=\"primary\" [(ngModel)]=\"value\" (change)=\"changed()\" (click)=\"clicked()\" [disabled]=\"readonly\">{{display}}</mat-checkbox>\n\n <mat-icon *ngIf=\"infoMessage\" (click)=\"onInfoClick($event)\" matTooltip=\"Info\" matTooltipPosition=\"above\" style=\"color: steelblue;font-size: 15px;margin-left: 5px;margin-top: 8px;\">info</mat-icon>\n\n</div> -->\n\n", styles: [".suffix-icons{display:flex;align-items:center}\n"], dependencies: [{ kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i4$3.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }] }); }
|
|
15462
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: CheckComponent, isStandalone: false, selector: "spa-check", inputs: { readonly: "readonly", display: "display", value: "value", infoMessage: "infoMessage", hint: "hint" }, outputs: { valueChange: "valueChange", click: "click", check: "check", uncheck: "uncheck", infoClick: "infoClick" }, ngImport: i0, template: "\n\n<!-- Changed: checkbox + hint are wrapped so the host presents ONE flex item, not two.\n tin-styles.css's FORM-1/FORM-3 rule makes `spa-form spa-check` a flex row to centre it on the field\n control band; with the hint as a second child it became a sibling COLUMN beside the label instead of a\n line beneath it. Wrapping restores the single-item assumption that rule was written against, so the\n band alignment is preserved untouched and the hint stacks where check.component.css already aims it. -->\n<div class=\"check-body\">\n <mat-checkbox color=\"primary\" [(ngModel)]=\"value\" (change)=\"changed()\" (click)=\"clicked()\" [disabled]=\"readonly\">{{display}}</mat-checkbox>\n\n <!-- Added: the hint line. Sits under the label rather than beside it so a full sentence can be read without\n stretching the grid column, and is indented to the label's own left edge, not the box's. -->\n <div class=\"check-hint\" *ngIf=\"hint\">{{hint}}</div>\n</div>\n\n<!-- <div class=\"suffix-icons\">\n\n <mat-checkbox color=\"primary\" [(ngModel)]=\"value\" (change)=\"changed()\" (click)=\"clicked()\" [disabled]=\"readonly\">{{display}}</mat-checkbox>\n\n <mat-icon *ngIf=\"infoMessage\" (click)=\"onInfoClick($event)\" matTooltip=\"Info\" matTooltipPosition=\"above\" style=\"color: steelblue;font-size: 15px;margin-left: 5px;margin-top: 8px;\">info</mat-icon>\n\n</div> -->\n\n", styles: [".suffix-icons{display:flex;align-items:center}.check-body{display:inline-flex;flex-direction:column;align-items:flex-start}.check-hint{font-size:12px;line-height:1.35;color:#0000008c;margin:1px 0 0 calc(var(--mat-checkbox-state-layer-size, var(--mdc-checkbox-state-layer-size, 40px)) + 4px)}\n"], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i4$3.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }] }); }
|
|
15318
15463
|
}
|
|
15319
15464
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CheckComponent, decorators: [{
|
|
15320
15465
|
type: Component,
|
|
15321
|
-
args: [{ selector: 'spa-check', standalone: false, template: "\n\n<mat-checkbox color=\"primary\" [(ngModel)]=\"value\" (change)=\"changed()\" (click)=\"clicked()\" [disabled]=\"readonly\">{{display}}</mat-checkbox>\n\n<!-- <div class=\"suffix-icons\">\n\n <mat-checkbox color=\"primary\" [(ngModel)]=\"value\" (change)=\"changed()\" (click)=\"clicked()\" [disabled]=\"readonly\">{{display}}</mat-checkbox>\n\n <mat-icon *ngIf=\"infoMessage\" (click)=\"onInfoClick($event)\" matTooltip=\"Info\" matTooltipPosition=\"above\" style=\"color: steelblue;font-size: 15px;margin-left: 5px;margin-top: 8px;\">info</mat-icon>\n\n</div> -->\n\n", styles: [".suffix-icons{display:flex;align-items:center}\n"] }]
|
|
15466
|
+
args: [{ selector: 'spa-check', standalone: false, template: "\n\n<!-- Changed: checkbox + hint are wrapped so the host presents ONE flex item, not two.\n tin-styles.css's FORM-1/FORM-3 rule makes `spa-form spa-check` a flex row to centre it on the field\n control band; with the hint as a second child it became a sibling COLUMN beside the label instead of a\n line beneath it. Wrapping restores the single-item assumption that rule was written against, so the\n band alignment is preserved untouched and the hint stacks where check.component.css already aims it. -->\n<div class=\"check-body\">\n <mat-checkbox color=\"primary\" [(ngModel)]=\"value\" (change)=\"changed()\" (click)=\"clicked()\" [disabled]=\"readonly\">{{display}}</mat-checkbox>\n\n <!-- Added: the hint line. Sits under the label rather than beside it so a full sentence can be read without\n stretching the grid column, and is indented to the label's own left edge, not the box's. -->\n <div class=\"check-hint\" *ngIf=\"hint\">{{hint}}</div>\n</div>\n\n<!-- <div class=\"suffix-icons\">\n\n <mat-checkbox color=\"primary\" [(ngModel)]=\"value\" (change)=\"changed()\" (click)=\"clicked()\" [disabled]=\"readonly\">{{display}}</mat-checkbox>\n\n <mat-icon *ngIf=\"infoMessage\" (click)=\"onInfoClick($event)\" matTooltip=\"Info\" matTooltipPosition=\"above\" style=\"color: steelblue;font-size: 15px;margin-left: 5px;margin-top: 8px;\">info</mat-icon>\n\n</div> -->\n\n", styles: [".suffix-icons{display:flex;align-items:center}.check-body{display:inline-flex;flex-direction:column;align-items:flex-start}.check-hint{font-size:12px;line-height:1.35;color:#0000008c;margin:1px 0 0 calc(var(--mat-checkbox-state-layer-size, var(--mdc-checkbox-state-layer-size, 40px)) + 4px)}\n"] }]
|
|
15322
15467
|
}], ctorParameters: () => [{ type: MessageService }], propDecorators: { readonly: [{
|
|
15323
15468
|
type: Input
|
|
15324
15469
|
}], display: [{
|
|
@@ -15337,6 +15482,94 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
15337
15482
|
type: Input
|
|
15338
15483
|
}], infoClick: [{
|
|
15339
15484
|
type: Output
|
|
15485
|
+
}], hint: [{
|
|
15486
|
+
type: Input
|
|
15487
|
+
}] } });
|
|
15488
|
+
|
|
15489
|
+
// TinCore Constants.SystemUser = "System"; its Users row is FirstName "System" / LastName "User",
|
|
15490
|
+
// so a system-stamped row reaches the client as either of these. Kept tight and evidence-based —
|
|
15491
|
+
// speculative tokens ('job', 'seed') are NOT matched because nothing in this codebase produces them.
|
|
15492
|
+
const SYSTEM_TOKENS = ['system', 'system user'];
|
|
15493
|
+
// Ten hues carry people. Slate is NOT in the rotation — it is reserved for "not a person", so a
|
|
15494
|
+
// neutral circle can never be mistaken for someone whose colour merely happens to be grey.
|
|
15495
|
+
const MONOGRAM_TONES = ['indigo', 'blue', 'cyan', 'teal', 'green', 'moss', 'ochre', 'clay', 'rose', 'plum'];
|
|
15496
|
+
// Initials from a display name. Rules, in order:
|
|
15497
|
+
// - an email uses the local part, split on . _ + - john.smith@x.com -> JS, admin@x.com -> AD
|
|
15498
|
+
// - two or more words take FIRST + LAST, so middle names are ignored Mary Jane Watson -> MW
|
|
15499
|
+
// - one word takes its first two characters admin -> AD, Prince -> PR
|
|
15500
|
+
// - a single character stands alone X -> X
|
|
15501
|
+
function monogramInitials(display) {
|
|
15502
|
+
const s = (display ?? '').replace(/\s+/g, ' ').trim();
|
|
15503
|
+
if (!s)
|
|
15504
|
+
return '';
|
|
15505
|
+
if (s.includes('@')) {
|
|
15506
|
+
const local = s.split('@')[0];
|
|
15507
|
+
const parts = local.split(/[._+-]+/).filter(p => p.length);
|
|
15508
|
+
if (parts.length > 1)
|
|
15509
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
15510
|
+
return local.slice(0, 2).toUpperCase();
|
|
15511
|
+
}
|
|
15512
|
+
const words = s.split(' ').filter(w => w.length);
|
|
15513
|
+
if (words.length > 1)
|
|
15514
|
+
return (words[0][0] + words[words.length - 1][0]).toUpperCase();
|
|
15515
|
+
return words[0].slice(0, 2).toUpperCase();
|
|
15516
|
+
}
|
|
15517
|
+
// Classifies the actor. BaseBasic.CreatedByName is $"{CreatedByUser?.FirstName} {CreatedByUser?.LastName}",
|
|
15518
|
+
// which yields a bare " " when the user is missing — that trims to empty and lands here as 'unknown',
|
|
15519
|
+
// which is exactly why the trim above is load-bearing rather than cosmetic.
|
|
15520
|
+
function monogramKind(display, key) {
|
|
15521
|
+
const d = (display ?? '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
15522
|
+
const k = (key ?? '').trim().toLowerCase();
|
|
15523
|
+
if (SYSTEM_TOKENS.includes(k) || SYSTEM_TOKENS.includes(d))
|
|
15524
|
+
return 'system';
|
|
15525
|
+
if (!d && !k)
|
|
15526
|
+
return 'unknown';
|
|
15527
|
+
return 'person';
|
|
15528
|
+
}
|
|
15529
|
+
// FNV-1a over the stable key. Deterministic and well spread for short strings; Math.imul keeps the
|
|
15530
|
+
// multiply in 32-bit so the result is identical across engines.
|
|
15531
|
+
function monogramPaletteIndex(key, buckets) {
|
|
15532
|
+
const s = (key ?? '').trim().toLowerCase();
|
|
15533
|
+
let h = 0x811c9dc5;
|
|
15534
|
+
for (let i = 0; i < s.length; i++) {
|
|
15535
|
+
h ^= s.charCodeAt(i);
|
|
15536
|
+
h = Math.imul(h, 0x01000193);
|
|
15537
|
+
}
|
|
15538
|
+
return (h >>> 0) % buckets;
|
|
15539
|
+
}
|
|
15540
|
+
class MonogramComponent {
|
|
15541
|
+
constructor() {
|
|
15542
|
+
this.tapped = new EventEmitter(); // touch path — there is no hover on a phone
|
|
15543
|
+
this.initials = '';
|
|
15544
|
+
this.kind = 'unknown';
|
|
15545
|
+
this.tone = 'slate';
|
|
15546
|
+
this.label = 'Unknown';
|
|
15547
|
+
}
|
|
15548
|
+
ngOnChanges() {
|
|
15549
|
+
const display = (this.value ?? '').replace(/\s+/g, ' ').trim();
|
|
15550
|
+
this.kind = monogramKind(this.value, this.key);
|
|
15551
|
+
// Colour seeds off the stable key when there is one; the normalized display name is the fallback,
|
|
15552
|
+
// which is stable in practice because every audit name is formatted by one base class.
|
|
15553
|
+
const seed = (this.key ?? '').trim() || display;
|
|
15554
|
+
this.initials = monogramInitials(display || this.key);
|
|
15555
|
+
this.tone = this.kind === 'person' ? MONOGRAM_TONES[monogramPaletteIndex(seed, MONOGRAM_TONES.length)] : 'slate';
|
|
15556
|
+
this.label = this.kind === 'system' ? 'System' : (display || 'Unknown');
|
|
15557
|
+
}
|
|
15558
|
+
onTap() {
|
|
15559
|
+
this.tapped.emit(this.label); // tap/Enter surfaces the full name where hover cannot
|
|
15560
|
+
}
|
|
15561
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: MonogramComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15562
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: MonogramComponent, isStandalone: false, selector: "spa-monogram", inputs: { value: "value", key: "key" }, outputs: { tapped: "tapped" }, usesOnChanges: true, ngImport: i0, template: "<span class=\"tin-mono\" [ngClass]=\"'tin-mono--' + tone\" role=\"img\" [attr.aria-label]=\"label\" [matTooltip]=\"label\" matTooltipPosition=\"above\" tabindex=\"0\" (click)=\"onTap()\" (keydown.enter)=\"onTap()\" (keydown.space)=\"onTap()\">\n <mat-icon *ngIf=\"kind === 'system'\" class=\"tin-mono__icon\">settings</mat-icon>\n <ng-container *ngIf=\"kind !== 'system'\">{{ initials || '\u2013' }}</ng-container>\n</span>\n", styles: [".tin-mono{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;font-size:10.5px;font-weight:600;letter-spacing:.3px;line-height:1;font-variant-numeric:tabular-nums;-webkit-user-select:none;user-select:none;cursor:pointer;flex-shrink:0;vertical-align:middle;background:var(--tin-mono-bg);color:var(--tin-mono-fg);box-shadow:inset 0 0 0 1px #0000000b;transition:box-shadow .12s ease,transform .12s ease}.tin-mono:hover{box-shadow:inset 0 0 0 1px #00000017,0 1px 3px #00000024}.tin-mono:active{transform:scale(.94)}.tin-mono:focus-visible{outline:2px solid var(--tin-mono-fg);outline-offset:2px}.tin-mono__icon{font-size:15px;width:15px;height:15px;opacity:.8}.tin-mono--slate{--tin-mono-bg: #E4E7EA;--tin-mono-fg: #3F4B57}.tin-mono--indigo{--tin-mono-bg: #E4E6F7;--tin-mono-fg: #343F94}.tin-mono--blue{--tin-mono-bg: #DBE8F8;--tin-mono-fg: #1B4F8A}.tin-mono--cyan{--tin-mono-bg: #D6EAF0;--tin-mono-fg: #17545F}.tin-mono--teal{--tin-mono-bg: #D5EBE3;--tin-mono-fg: #1A5847}.tin-mono--green{--tin-mono-bg: #DDEBD6;--tin-mono-fg: #33602A}.tin-mono--moss{--tin-mono-bg: #E6E8CF;--tin-mono-fg: #55591B}.tin-mono--ochre{--tin-mono-bg: #F3E6CD;--tin-mono-fg: #6B5115}.tin-mono--clay{--tin-mono-bg: #F6E2D6;--tin-mono-fg: #7A4522}.tin-mono--rose{--tin-mono-bg: #F7E0E4;--tin-mono-fg: #8A3A4C}.tin-mono--plum{--tin-mono-bg: #EEE0F2;--tin-mono-fg: #6B3A82}:host-context([data-theme=\"dark\"]) .tin-mono--slate,:host-context(.tin-dark) .tin-mono--slate{--tin-mono-bg: #333B44;--tin-mono-fg: #C5CDD6}:host-context([data-theme=\"dark\"]) .tin-mono--indigo,:host-context(.tin-dark) .tin-mono--indigo{--tin-mono-bg: #2E3468;--tin-mono-fg: #C5CBF0}:host-context([data-theme=\"dark\"]) .tin-mono--blue,:host-context(.tin-dark) .tin-mono--blue{--tin-mono-bg: #22415F;--tin-mono-fg: #B9D5F2}:host-context([data-theme=\"dark\"]) .tin-mono--cyan,:host-context(.tin-dark) .tin-mono--cyan{--tin-mono-bg: #1C424A;--tin-mono-fg: #AED8E2}:host-context([data-theme=\"dark\"]) .tin-mono--teal,:host-context(.tin-dark) .tin-mono--teal{--tin-mono-bg: #1D453A;--tin-mono-fg: #ABDCC9}:host-context([data-theme=\"dark\"]) .tin-mono--green,:host-context(.tin-dark) .tin-mono--green{--tin-mono-bg: #2A4423;--tin-mono-fg: #C0DCB4}:host-context([data-theme=\"dark\"]) .tin-mono--moss,:host-context(.tin-dark) .tin-mono--moss{--tin-mono-bg: #3F4218;--tin-mono-fg: #D2D6A6}:host-context([data-theme=\"dark\"]) .tin-mono--ochre,:host-context(.tin-dark) .tin-mono--ochre{--tin-mono-bg: #4C3C13;--tin-mono-fg: #E4CD9C}:host-context([data-theme=\"dark\"]) .tin-mono--clay,:host-context(.tin-dark) .tin-mono--clay{--tin-mono-bg: #55351E;--tin-mono-fg: #EEC3A6}:host-context([data-theme=\"dark\"]) .tin-mono--rose,:host-context(.tin-dark) .tin-mono--rose{--tin-mono-bg: #5C2E38;--tin-mono-fg: #F0BFC8}:host-context([data-theme=\"dark\"]) .tin-mono--plum,:host-context(.tin-dark) .tin-mono--plum{--tin-mono-bg: #4A2E58;--tin-mono-fg: #DCBFE8}:host-context([data-theme=\"dark\"]) .tin-mono,:host-context(.tin-dark) .tin-mono{box-shadow:inset 0 0 0 1px #ffffff0f}@media (prefers-reduced-motion: reduce){.tin-mono{transition:none}.tin-mono:active{transform:none}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15563
|
+
}
|
|
15564
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: MonogramComponent, decorators: [{
|
|
15565
|
+
type: Component,
|
|
15566
|
+
args: [{ selector: 'spa-monogram', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<span class=\"tin-mono\" [ngClass]=\"'tin-mono--' + tone\" role=\"img\" [attr.aria-label]=\"label\" [matTooltip]=\"label\" matTooltipPosition=\"above\" tabindex=\"0\" (click)=\"onTap()\" (keydown.enter)=\"onTap()\" (keydown.space)=\"onTap()\">\n <mat-icon *ngIf=\"kind === 'system'\" class=\"tin-mono__icon\">settings</mat-icon>\n <ng-container *ngIf=\"kind !== 'system'\">{{ initials || '\u2013' }}</ng-container>\n</span>\n", styles: [".tin-mono{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;font-size:10.5px;font-weight:600;letter-spacing:.3px;line-height:1;font-variant-numeric:tabular-nums;-webkit-user-select:none;user-select:none;cursor:pointer;flex-shrink:0;vertical-align:middle;background:var(--tin-mono-bg);color:var(--tin-mono-fg);box-shadow:inset 0 0 0 1px #0000000b;transition:box-shadow .12s ease,transform .12s ease}.tin-mono:hover{box-shadow:inset 0 0 0 1px #00000017,0 1px 3px #00000024}.tin-mono:active{transform:scale(.94)}.tin-mono:focus-visible{outline:2px solid var(--tin-mono-fg);outline-offset:2px}.tin-mono__icon{font-size:15px;width:15px;height:15px;opacity:.8}.tin-mono--slate{--tin-mono-bg: #E4E7EA;--tin-mono-fg: #3F4B57}.tin-mono--indigo{--tin-mono-bg: #E4E6F7;--tin-mono-fg: #343F94}.tin-mono--blue{--tin-mono-bg: #DBE8F8;--tin-mono-fg: #1B4F8A}.tin-mono--cyan{--tin-mono-bg: #D6EAF0;--tin-mono-fg: #17545F}.tin-mono--teal{--tin-mono-bg: #D5EBE3;--tin-mono-fg: #1A5847}.tin-mono--green{--tin-mono-bg: #DDEBD6;--tin-mono-fg: #33602A}.tin-mono--moss{--tin-mono-bg: #E6E8CF;--tin-mono-fg: #55591B}.tin-mono--ochre{--tin-mono-bg: #F3E6CD;--tin-mono-fg: #6B5115}.tin-mono--clay{--tin-mono-bg: #F6E2D6;--tin-mono-fg: #7A4522}.tin-mono--rose{--tin-mono-bg: #F7E0E4;--tin-mono-fg: #8A3A4C}.tin-mono--plum{--tin-mono-bg: #EEE0F2;--tin-mono-fg: #6B3A82}:host-context([data-theme=\"dark\"]) .tin-mono--slate,:host-context(.tin-dark) .tin-mono--slate{--tin-mono-bg: #333B44;--tin-mono-fg: #C5CDD6}:host-context([data-theme=\"dark\"]) .tin-mono--indigo,:host-context(.tin-dark) .tin-mono--indigo{--tin-mono-bg: #2E3468;--tin-mono-fg: #C5CBF0}:host-context([data-theme=\"dark\"]) .tin-mono--blue,:host-context(.tin-dark) .tin-mono--blue{--tin-mono-bg: #22415F;--tin-mono-fg: #B9D5F2}:host-context([data-theme=\"dark\"]) .tin-mono--cyan,:host-context(.tin-dark) .tin-mono--cyan{--tin-mono-bg: #1C424A;--tin-mono-fg: #AED8E2}:host-context([data-theme=\"dark\"]) .tin-mono--teal,:host-context(.tin-dark) .tin-mono--teal{--tin-mono-bg: #1D453A;--tin-mono-fg: #ABDCC9}:host-context([data-theme=\"dark\"]) .tin-mono--green,:host-context(.tin-dark) .tin-mono--green{--tin-mono-bg: #2A4423;--tin-mono-fg: #C0DCB4}:host-context([data-theme=\"dark\"]) .tin-mono--moss,:host-context(.tin-dark) .tin-mono--moss{--tin-mono-bg: #3F4218;--tin-mono-fg: #D2D6A6}:host-context([data-theme=\"dark\"]) .tin-mono--ochre,:host-context(.tin-dark) .tin-mono--ochre{--tin-mono-bg: #4C3C13;--tin-mono-fg: #E4CD9C}:host-context([data-theme=\"dark\"]) .tin-mono--clay,:host-context(.tin-dark) .tin-mono--clay{--tin-mono-bg: #55351E;--tin-mono-fg: #EEC3A6}:host-context([data-theme=\"dark\"]) .tin-mono--rose,:host-context(.tin-dark) .tin-mono--rose{--tin-mono-bg: #5C2E38;--tin-mono-fg: #F0BFC8}:host-context([data-theme=\"dark\"]) .tin-mono--plum,:host-context(.tin-dark) .tin-mono--plum{--tin-mono-bg: #4A2E58;--tin-mono-fg: #DCBFE8}:host-context([data-theme=\"dark\"]) .tin-mono,:host-context(.tin-dark) .tin-mono{box-shadow:inset 0 0 0 1px #ffffff0f}@media (prefers-reduced-motion: reduce){.tin-mono{transition:none}.tin-mono:active{transform:none}}\n"] }]
|
|
15567
|
+
}], propDecorators: { value: [{
|
|
15568
|
+
type: Input
|
|
15569
|
+
}], key: [{
|
|
15570
|
+
type: Input
|
|
15571
|
+
}], tapped: [{
|
|
15572
|
+
type: Output
|
|
15340
15573
|
}] } });
|
|
15341
15574
|
|
|
15342
15575
|
class TableRowComponent {
|
|
@@ -15427,11 +15660,11 @@ class TableRowComponent {
|
|
|
15427
15660
|
return false;
|
|
15428
15661
|
}
|
|
15429
15662
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableRowComponent, deps: [{ token: ButtonService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15430
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TableRowComponent, isStandalone: false, selector: "app-table-row", inputs: { column: "column", row: "row", config: "config", smallScreen: "smallScreen" }, outputs: { actionClick: "actionClick", columnClick: "columnClick", showBannerEvent: "showBannerEvent" }, usesOnChanges: true, ngImport: i0, template: "<ng-container [ngSwitch]=\"column.type\">\n <ng-container *ngSwitchCase=\"'checkbox'\">\n <spa-check [value]=\"row[column.name]\" [readonly]=\"true\"></spa-check>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'select'\">\n <spa-select-lite [options]=\"column.options\" [optionDisplay]=\"column.optionDisplay\" [optionValue]=\"column.optionValue\" [(value)]=\"row[column.name]\" width=\"90%\"></spa-select-lite>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'chip'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'background-color': vm.color || '#eceff1', 'color': 'rgba(0, 0, 0, 0.87)', 'border': 'none'}\">{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'icon'\">\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date'\">\n {{row[column.name] | date : 'dd/MM/yyyy'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date-short'\">\n {{row[column.name] | date : 'd MMM'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetime'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetimesec'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm:ss'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'money'\">\n <label [ngStyle]=\"{'color': vm.color }\">{{row[column.name] | currency:'':''}}</label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'button'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'color': vm.color}\" >{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchDefault>\n <label [ngStyle]=\"{'color': vm.color }\">\n <ng-container *ngIf=\"column.type === 'number'\">\n {{row[column.name] | number:'1.0-2'}}\n </ng-container>\n <ng-container *ngIf=\"column.type !== 'number'\">\n {{vm.text}}\n </ng-container>\n </label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.truncated\" matTooltip='Show more' matTooltipPosition=\"above\" (click)=\"showBanner(row[column.name])\">more_horiz</mat-icon>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n</ng-container>\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px;vertical-align:middle}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#e5e5e5}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.mat-mdc-cell .mat-mdc-outlined-button,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button){height:auto!important;min-height:36px!important;white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important;line-height:1.4!important;text-align:left!important;padding:6px 16px!important}.mat-mdc-cell .mat-mdc-outlined-button .mdc-button__label,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button) .mdc-button__label{white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i1$2.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1$2.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1$2.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: SelectLiteComponent, selector: "spa-select-lite" }, { kind: "pipe", type: i1$2.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$2.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
15663
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TableRowComponent, isStandalone: false, selector: "app-table-row", inputs: { column: "column", row: "row", config: "config", smallScreen: "smallScreen" }, outputs: { actionClick: "actionClick", columnClick: "columnClick", showBannerEvent: "showBannerEvent" }, usesOnChanges: true, ngImport: i0, template: "<ng-container [ngSwitch]=\"column.type\">\n <ng-container *ngSwitchCase=\"'checkbox'\">\n <spa-check [value]=\"row[column.name]\" [readonly]=\"true\"></spa-check>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'select'\">\n <spa-select-lite [options]=\"column.options\" [optionDisplay]=\"column.optionDisplay\" [optionValue]=\"column.optionValue\" [(value)]=\"row[column.name]\" width=\"90%\"></spa-select-lite>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'chip'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'background-color': vm.color || '#eceff1', 'color': 'rgba(0, 0, 0, 0.87)', 'border': 'none'}\">{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'icon'\">\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date'\">\n {{row[column.name] | date : 'dd/MM/yyyy'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date-short'\">\n {{row[column.name] | date : 'd MMM'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetime'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetimesec'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm:ss'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'money'\">\n <label [ngStyle]=\"{'color': vm.color }\">{{row[column.name] | currency:'':''}}</label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <!-- Added: monogram \u2014 the row still holds the full name, so filter/sort keep working on the underlying value -->\n <ng-container *ngSwitchCase=\"'monogram'\">\n <spa-monogram [value]=\"row[column.name]\" [key]=\"column.keyField ? row[column.keyField] : null\" (tapped)=\"showBanner($event)\"></spa-monogram>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'button'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'color': vm.color}\" >{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchDefault>\n <label [ngStyle]=\"{'color': vm.color }\">\n <ng-container *ngIf=\"column.type === 'number'\">\n {{row[column.name] | number:'1.0-2'}}\n </ng-container>\n <ng-container *ngIf=\"column.type !== 'number'\">\n {{vm.text}}\n </ng-container>\n </label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.truncated\" matTooltip='Show more' matTooltipPosition=\"above\" (click)=\"showBanner(row[column.name])\">more_horiz</mat-icon>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n</ng-container>\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px;vertical-align:middle}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#e5e5e5}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.mat-mdc-cell .mat-mdc-outlined-button,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button){height:auto!important;min-height:36px!important;white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important;line-height:1.4!important;text-align:left!important;padding:6px 16px!important}.mat-mdc-cell .mat-mdc-outlined-button .mdc-button__label,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button) .mdc-button__label{white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i1$2.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1$2.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1$2.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage", "hint"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: MonogramComponent, selector: "spa-monogram", inputs: ["value", "key"], outputs: ["tapped"] }, { kind: "component", type: SelectLiteComponent, selector: "spa-select-lite" }, { kind: "pipe", type: i1$2.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$2.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
15431
15664
|
}
|
|
15432
15665
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableRowComponent, decorators: [{
|
|
15433
15666
|
type: Component,
|
|
15434
|
-
args: [{ selector: 'app-table-row', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<ng-container [ngSwitch]=\"column.type\">\n <ng-container *ngSwitchCase=\"'checkbox'\">\n <spa-check [value]=\"row[column.name]\" [readonly]=\"true\"></spa-check>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'select'\">\n <spa-select-lite [options]=\"column.options\" [optionDisplay]=\"column.optionDisplay\" [optionValue]=\"column.optionValue\" [(value)]=\"row[column.name]\" width=\"90%\"></spa-select-lite>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'chip'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'background-color': vm.color || '#eceff1', 'color': 'rgba(0, 0, 0, 0.87)', 'border': 'none'}\">{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'icon'\">\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date'\">\n {{row[column.name] | date : 'dd/MM/yyyy'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date-short'\">\n {{row[column.name] | date : 'd MMM'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetime'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetimesec'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm:ss'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'money'\">\n <label [ngStyle]=\"{'color': vm.color }\">{{row[column.name] | currency:'':''}}</label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'button'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'color': vm.color}\" >{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchDefault>\n <label [ngStyle]=\"{'color': vm.color }\">\n <ng-container *ngIf=\"column.type === 'number'\">\n {{row[column.name] | number:'1.0-2'}}\n </ng-container>\n <ng-container *ngIf=\"column.type !== 'number'\">\n {{vm.text}}\n </ng-container>\n </label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.truncated\" matTooltip='Show more' matTooltipPosition=\"above\" (click)=\"showBanner(row[column.name])\">more_horiz</mat-icon>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n</ng-container>\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px;vertical-align:middle}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#e5e5e5}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.mat-mdc-cell .mat-mdc-outlined-button,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button){height:auto!important;min-height:36px!important;white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important;line-height:1.4!important;text-align:left!important;padding:6px 16px!important}.mat-mdc-cell .mat-mdc-outlined-button .mdc-button__label,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button) .mdc-button__label{white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important}\n"] }]
|
|
15667
|
+
args: [{ selector: 'app-table-row', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<ng-container [ngSwitch]=\"column.type\">\n <ng-container *ngSwitchCase=\"'checkbox'\">\n <spa-check [value]=\"row[column.name]\" [readonly]=\"true\"></spa-check>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'select'\">\n <spa-select-lite [options]=\"column.options\" [optionDisplay]=\"column.optionDisplay\" [optionValue]=\"column.optionValue\" [(value)]=\"row[column.name]\" width=\"90%\"></spa-select-lite>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'chip'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'background-color': vm.color || '#eceff1', 'color': 'rgba(0, 0, 0, 0.87)', 'border': 'none'}\">{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'icon'\">\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date'\">\n {{row[column.name] | date : 'dd/MM/yyyy'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date-short'\">\n {{row[column.name] | date : 'd MMM'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetime'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetimesec'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm:ss'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'money'\">\n <label [ngStyle]=\"{'color': vm.color }\">{{row[column.name] | currency:'':''}}</label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <!-- Added: monogram \u2014 the row still holds the full name, so filter/sort keep working on the underlying value -->\n <ng-container *ngSwitchCase=\"'monogram'\">\n <spa-monogram [value]=\"row[column.name]\" [key]=\"column.keyField ? row[column.keyField] : null\" (tapped)=\"showBanner($event)\"></spa-monogram>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'button'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'color': vm.color}\" >{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchDefault>\n <label [ngStyle]=\"{'color': vm.color }\">\n <ng-container *ngIf=\"column.type === 'number'\">\n {{row[column.name] | number:'1.0-2'}}\n </ng-container>\n <ng-container *ngIf=\"column.type !== 'number'\">\n {{vm.text}}\n </ng-container>\n </label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.truncated\" matTooltip='Show more' matTooltipPosition=\"above\" (click)=\"showBanner(row[column.name])\">more_horiz</mat-icon>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n</ng-container>\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px;vertical-align:middle}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#e5e5e5}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.mat-mdc-cell .mat-mdc-outlined-button,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button){height:auto!important;min-height:36px!important;white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important;line-height:1.4!important;text-align:left!important;padding:6px 16px!important}.mat-mdc-cell .mat-mdc-outlined-button .mdc-button__label,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button) .mdc-button__label{white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important}\n"] }]
|
|
15435
15668
|
}], ctorParameters: () => [{ type: ButtonService }], propDecorators: { column: [{
|
|
15436
15669
|
type: Input
|
|
15437
15670
|
}], row: [{
|
|
@@ -15866,7 +16099,7 @@ class SelectComponent extends SelectCommonComponent {
|
|
|
15866
16099
|
let button = this.detailsConfig.buttons.find(b => b.name === mode);
|
|
15867
16100
|
button.detailsConfig = this.detailsConfig;
|
|
15868
16101
|
this.dialogService.openDefaultDetailsDialog(button, dynamicData).subscribe(result => {
|
|
15869
|
-
if (result
|
|
16102
|
+
if (result?.message === 'success') { // Changed: a dismissed quick-add dialog closes with undefined — read through it rather than throwing
|
|
15870
16103
|
// Added (dropdown quick-add): auto-select the record the user just created — the point of adding
|
|
15871
16104
|
// from a dropdown is to use the new record, so don't make them find it in the list afterwards
|
|
15872
16105
|
if (mode === 'create')
|
|
@@ -16594,7 +16827,7 @@ class InlineCellComponent {
|
|
|
16594
16827
|
<spa-text *ngSwitchDefault [display]="display" [(value)]="data[field.name]" (valueChange)="cellChanged()" [required]="field.required" [min]="field.min" [max]="field.max" [suffix]="field.suffix" [regex]="field.regex"></spa-text>
|
|
16595
16828
|
|
|
16596
16829
|
</ng-container>
|
|
16597
|
-
`, isInline: true, styles: [":host{display:block;min-width:90px;padding-top:6px}\n"], dependencies: [{ kind: "directive", type: i1$2.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1$2.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1$2.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "component", type: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextAreaComponent, selector: "spa-text-area", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "rows", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: DateComponent, selector: "spa-date", inputs: ["required", "min", "max", "readonly", "hint", "value", "display", "placeholder", "width", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: DatetimeComponent, selector: "spa-datetime", inputs: ["display", "value", "readonly", "width", "min", "max", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: MoneyComponent, selector: "spa-money", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "currency", "required", "min", "max", "infoMessage", "copyContent", "clearContent", "suffix"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: NumberComponent, selector: "spa-number", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "step", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: EmailComponent, selector: "spa-email", inputs: ["display", "value", "readonly", "required", "hint", "suffix", "infoMessage", "copyContent", "clearContent", "options", "optionValue"], outputs: ["valueChange"] }] }); }
|
|
16830
|
+
`, isInline: true, styles: [":host{display:block;min-width:90px;padding-top:6px}\n"], dependencies: [{ kind: "directive", type: i1$2.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1$2.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1$2.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "component", type: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextAreaComponent, selector: "spa-text-area", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "rows", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage", "hint"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: DateComponent, selector: "spa-date", inputs: ["required", "min", "max", "readonly", "hint", "value", "display", "placeholder", "width", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: DatetimeComponent, selector: "spa-datetime", inputs: ["display", "value", "readonly", "width", "min", "max", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: MoneyComponent, selector: "spa-money", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "currency", "required", "min", "max", "infoMessage", "copyContent", "clearContent", "suffix"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: NumberComponent, selector: "spa-number", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "step", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: EmailComponent, selector: "spa-email", inputs: ["display", "value", "readonly", "required", "hint", "suffix", "infoMessage", "copyContent", "clearContent", "options", "optionValue"], outputs: ["valueChange"] }] }); }
|
|
16598
16831
|
}
|
|
16599
16832
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: InlineCellComponent, decorators: [{
|
|
16600
16833
|
type: Component,
|
|
@@ -17055,7 +17288,7 @@ class GroupsComponent {
|
|
|
17055
17288
|
getVisibleButtons(item) {
|
|
17056
17289
|
let buttons = this.buttonCache.get(item);
|
|
17057
17290
|
if (!buttons) {
|
|
17058
|
-
buttons = this.displayedButtons ? this.displayedButtons.filter(button => Core.isItemVisible(button, item)) : []; // Changed: unified visible/hidden (boolean | condition)
|
|
17291
|
+
buttons = this.displayedButtons ? this.displayedButtons.filter(button => Core.isItemVisible(button, item) && this.buttonService.hasCapability(button)) : []; // Changed: unified visible/hidden (boolean | condition) + the opt-in capability, which this path bypassed by not going through testVisible
|
|
17059
17292
|
this.buttonCache.set(item, buttons);
|
|
17060
17293
|
}
|
|
17061
17294
|
return buttons;
|
|
@@ -17201,6 +17434,15 @@ class TableComponent {
|
|
|
17201
17434
|
this.filterActive = false;
|
|
17202
17435
|
this.filterText = ''; // Added: current paged-mode filter text
|
|
17203
17436
|
this.groupFilterText = ''; // Added: relayed to spa-groups so the standard header filter drives the grouped view
|
|
17437
|
+
// Added: the headline for the "filter matched nothing" empty state. Built ONCE per filter change rather than
|
|
17438
|
+
// by a template getter — a getter that concatenates a string allocates on every change-detection pass.
|
|
17439
|
+
this.filterEmptyTitle = 'No matches';
|
|
17440
|
+
this.filterEmptyTitlePartial = 'No matches in the rows loaded so far'; // Added: the partial-window variant of the headline above (see filterScopePartial)
|
|
17441
|
+
// Added: a load that FAILED is not a load that returned nothing, and until now the grid could not tell them
|
|
17442
|
+
// apart. The failure paths never assign dataSource, so it stayed undefined and the template's
|
|
17443
|
+
// `!dataSource && !loadingStage` branch rendered "Loading..." — permanently, under a dismissed error dialog.
|
|
17444
|
+
// Set by every failure handler, cleared whenever a read starts or succeeds.
|
|
17445
|
+
this.loadFailed = false;
|
|
17204
17446
|
this.fetchSize = 20;
|
|
17205
17447
|
this.pagedUrlBase = '';
|
|
17206
17448
|
this.pageFetchInFlight = false;
|
|
@@ -17253,16 +17495,29 @@ class TableComponent {
|
|
|
17253
17495
|
get sectionHidden() {
|
|
17254
17496
|
return !!(this.config?.sectionConfig?.hideWhenEmpty && this.dataSource?.length === 0);
|
|
17255
17497
|
}
|
|
17498
|
+
// Changed: the expand half is delegated to expandSection so the two entry points cannot drift apart
|
|
17256
17499
|
toggleSection() {
|
|
17257
17500
|
if (this.config?.sectionConfig?.collapsible === false)
|
|
17258
17501
|
return;
|
|
17259
|
-
this.sectionCollapsed
|
|
17502
|
+
if (this.sectionCollapsed) {
|
|
17503
|
+
this.expandSection();
|
|
17504
|
+
return;
|
|
17505
|
+
}
|
|
17506
|
+
this.sectionCollapsed = true;
|
|
17507
|
+
}
|
|
17508
|
+
// Added: the expand-ONLY entry point, for a caller that wants this section open rather than flipped — a Day
|
|
17509
|
+
// Book chip or stage tile means "show me this list", and routing that through toggleSection() closes the very
|
|
17510
|
+
// thing the user asked to see whenever the section happened to be open already. Deliberately NOT gated on
|
|
17511
|
+
// `collapsible === false`: a section the operator may not collapse by hand is one that should be open anyway.
|
|
17512
|
+
expandSection() {
|
|
17260
17513
|
if (!this.sectionCollapsed)
|
|
17261
|
-
|
|
17514
|
+
return;
|
|
17515
|
+
this.sectionCollapsed = false;
|
|
17516
|
+
setTimeout(() => this.setPaginator()); // re-attach — the paginator ViewChild did not exist while collapsed
|
|
17262
17517
|
}
|
|
17263
17518
|
buildSectionHeader() {
|
|
17264
17519
|
this.sectionChipList = (this.config?.sectionConfig?.chips || []).filter(c => Core.isItemVisible(c, this.dataSourceValue)); // predicates receive the rows
|
|
17265
|
-
this.sectionButtonList = (this.config?.sectionConfig?.buttons || []).filter(b => Core.isItemVisible(b, this.dataSourceValue));
|
|
17520
|
+
this.sectionButtonList = (this.config?.sectionConfig?.buttons || []).filter(b => Core.isItemVisible(b, this.dataSourceValue) && this.buttonService.hasCapability(b)); // Changed: section-header buttons bypass testVisible, so the opt-in capability is applied here too. No-op for a button that names none
|
|
17266
17521
|
}
|
|
17267
17522
|
sectionChips() { return this.sectionChipList; } // Changed: returns the materialized field
|
|
17268
17523
|
sectionButtons() { return this.sectionButtonList; } // Changed: returns the materialized field
|
|
@@ -17344,6 +17599,45 @@ class TableComponent {
|
|
|
17344
17599
|
return 35;
|
|
17345
17600
|
return [72, 58, 86, 64, 78, 52][index % 6];
|
|
17346
17601
|
}
|
|
17602
|
+
// Added: "nothing here yet" and "nothing matches what you typed" are two different states and they get two
|
|
17603
|
+
// different sentences. The "No Data" line beside this one tests dataSource — the LOADED WINDOW — which a
|
|
17604
|
+
// filter never shrinks: filtering happens downstream of it (MatTableDataSource.filteredData when unpaged,
|
|
17605
|
+
// filteredRows when paged). So a filter matching nothing left dataSource full, the line suppressed, and the
|
|
17606
|
+
// user staring at a header, a "0 of 0" paginator and no explanation. This getter is the missing case only —
|
|
17607
|
+
// it is FALSE whenever dataSource is empty, so the genuinely-empty state keeps the line and the words it
|
|
17608
|
+
// always had. Reads lengths and flags only; it allocates nothing, so it is safe to bind in the template.
|
|
17609
|
+
get filteredToNothing() {
|
|
17610
|
+
if (this.loadingStage === 'initial')
|
|
17611
|
+
return false; // the loading stage owns the space until it completes
|
|
17612
|
+
if (!(this.dataSource?.length > 0))
|
|
17613
|
+
return false; // genuinely empty — "No Data" answers for that state
|
|
17614
|
+
// Only the table view filters what it renders. Cards and capsules are bound to dataSource directly and show
|
|
17615
|
+
// every row regardless of the filter, and the grouped view filters on its own predicate inside spa-groups —
|
|
17616
|
+
// claiming "no matches" over a screen still full of cards would be a worse lie than saying nothing.
|
|
17617
|
+
if (this.config?.viewType && this.config.viewType !== 'table')
|
|
17618
|
+
return false;
|
|
17619
|
+
if (this.pagedMode)
|
|
17620
|
+
return this.filterActive && this.filteredRows?.length === 0;
|
|
17621
|
+
return !!this.tableDataSource?.filter && this.tableDataSource.filteredData?.length === 0;
|
|
17622
|
+
}
|
|
17623
|
+
// Added: THE predicate for "the filter did not see everything". In paged mode the client filter runs over
|
|
17624
|
+
// loadedRows — the accumulated window — and filterChanged issues no fetch, so with a partial window a filter
|
|
17625
|
+
// searched the loaded rows ONLY. This getter is deliberately the single definition of that fact: the hint
|
|
17626
|
+
// banner above the grid and the empty-state message below it both bind to it, so the two physically cannot
|
|
17627
|
+
// disagree about how much was searched. They previously stated it independently and did — the banner said
|
|
17628
|
+
// "only the 20 loaded rows of 240" while the message underneath offered "all 240 rows", implying the filter
|
|
17629
|
+
// had covered the lot. Reads lengths and flags only; allocates nothing, so it is safe to bind in a template.
|
|
17630
|
+
get filterScopePartial() {
|
|
17631
|
+
return this.pagedMode && this.filterActive && this.loadedRows.length < this.serverTotal;
|
|
17632
|
+
}
|
|
17633
|
+
// Added: what the user gets back by clearing the filter. Paged mode's dataSource is only the loaded window, so
|
|
17634
|
+
// the server total (the same figure its own paginator shows) is the honest number there. Only ever rendered in
|
|
17635
|
+
// the COMPLETE-scope state — where the window is partial the count would misdescribe what was searched, which
|
|
17636
|
+
// is exactly why that state gets its own sentence rather than this one.
|
|
17637
|
+
get filteredTotal() {
|
|
17638
|
+
return this.pagedMode ? this.serverTotal + this.overlayDelta : (this.dataSource?.length ?? 0);
|
|
17639
|
+
}
|
|
17640
|
+
get filteredRowNoun() { return this.filteredTotal === 1 ? 'row' : 'rows'; } // Added: keeps "all 1 rows" out of the message
|
|
17347
17641
|
get stageEntityName() {
|
|
17348
17642
|
// An author-supplied entityName is their wording and is used verbatim.
|
|
17349
17643
|
if (this.config?.entityName)
|
|
@@ -17474,6 +17768,25 @@ class TableComponent {
|
|
|
17474
17768
|
return null;
|
|
17475
17769
|
return field;
|
|
17476
17770
|
}
|
|
17771
|
+
// Added: every field the CONFIG declares inline-editable, whether or not it has a cell on screen
|
|
17772
|
+
configuredInlineFields() {
|
|
17773
|
+
return (this.config?.columns ?? []).map(column => this.getInlineField(column)).filter(field => !!field);
|
|
17774
|
+
}
|
|
17775
|
+
// Added: the inline-editable fields that are ACTUALLY RENDERED. <app-inline-cell> lives inside a per-column
|
|
17776
|
+
// matColumnDef (table.component.html:101), so a column missing from displayedColumns has no editor at all.
|
|
17777
|
+
// displayedColumns is not config.columns: setColumns REPLACES it with the minColumns subset below 600px
|
|
17778
|
+
// (table-config.service.ts:26-27) and also drops columns pruned by a visible/hidden predicate.
|
|
17779
|
+
renderedInlineFields() {
|
|
17780
|
+
return (this.config?.columns ?? []).filter(column => this.displayedColumns?.includes(column.name)).map(column => this.getInlineField(column)).filter(field => !!field);
|
|
17781
|
+
}
|
|
17782
|
+
// Added: inline edit is only honest when EVERY editable field has a cell the operator can see and change.
|
|
17783
|
+
// Previously seedInlineDefaults and submitInlineEdit both walked config.columns, so a field cut by minColumns
|
|
17784
|
+
// was still seeded with its defaultValue, still passed the required check, and was still posted — the row
|
|
17785
|
+
// submitted values nobody ever saw (piglet weaned litters at an optimistic count on a phone, 2026-08-10).
|
|
17786
|
+
canEditInline() {
|
|
17787
|
+
const configured = this.configuredInlineFields();
|
|
17788
|
+
return configured.length > 0 && this.renderedInlineFields().length === configured.length;
|
|
17789
|
+
}
|
|
17477
17790
|
startInlineEdit(row) {
|
|
17478
17791
|
this.editingRow = row;
|
|
17479
17792
|
this.editingModel = { ...row }; // shallow copy — scalars only change, cancel restores by discarding
|
|
@@ -17487,9 +17800,8 @@ class TableComponent {
|
|
|
17487
17800
|
seedInlineDefaults(row) {
|
|
17488
17801
|
if (!this.config.inlineEdit || !this.config.columns)
|
|
17489
17802
|
return;
|
|
17490
|
-
for (const
|
|
17491
|
-
|
|
17492
|
-
if (!field || field.defaultValue === undefined || field.defaultValue === null)
|
|
17803
|
+
for (const field of this.renderedInlineFields()) { // Changed: was config.columns — an unrendered field must never be silently seeded, because nobody can see or correct what it was seeded with
|
|
17804
|
+
if (field.defaultValue === undefined || field.defaultValue === null)
|
|
17493
17805
|
continue;
|
|
17494
17806
|
const current = this.editingModel[field.name];
|
|
17495
17807
|
if (current !== undefined && current !== null && current !== '')
|
|
@@ -17505,7 +17817,7 @@ class TableComponent {
|
|
|
17505
17817
|
if (!this.editingRow)
|
|
17506
17818
|
return;
|
|
17507
17819
|
// Validate only the fields that are editable inline, with the same rules the dialog form applies
|
|
17508
|
-
const editableFields = this.config.columns
|
|
17820
|
+
const editableFields = this.renderedInlineFields(); // Changed: was config.columns — validating a field with no cell on screen let a required check pass against a value the operator never supplied
|
|
17509
17821
|
const validationResult = Core.validateObject(editableFields, this.editingModel);
|
|
17510
17822
|
if (validationResult !== '') {
|
|
17511
17823
|
this.messageService.toast(validationResult);
|
|
@@ -17552,6 +17864,13 @@ class TableComponent {
|
|
|
17552
17864
|
this.displayedColumns = this.tableConfigService.setColumns(this.config, this.smallScreen);
|
|
17553
17865
|
this.displayedButtons = this.tableConfigService.getDisplayedButtons(this.config?.buttons, this.smallScreen, this.config);
|
|
17554
17866
|
this.actionsWidth = this.tableConfigService.getActionsWidth(this.displayedButtons, this.smallScreen, this.config);
|
|
17867
|
+
// Added: the same defect through the resize door. A row opened for edit on a wide screen keeps its seeded
|
|
17868
|
+
// editingModel when the window narrows past 600px, but its cells vanish with the columns — so the tick would
|
|
17869
|
+
// post values the operator can no longer see. Discard the edit instead; cancel has always been non-destructive.
|
|
17870
|
+
if (this.editingRow && !this.canEditInline()) {
|
|
17871
|
+
this.cancelInlineEdit();
|
|
17872
|
+
this.messageService.toast('Editing was closed — this screen is too narrow to show every editable field');
|
|
17873
|
+
}
|
|
17555
17874
|
if (this.config?.searchConfig) {
|
|
17556
17875
|
this.showFilterButton = false;
|
|
17557
17876
|
}
|
|
@@ -17574,6 +17893,7 @@ class TableComponent {
|
|
|
17574
17893
|
}
|
|
17575
17894
|
}
|
|
17576
17895
|
loadData(action, data) {
|
|
17896
|
+
this.loadFailed = false; // Added: a retry must clear the previous failure before it can be re-decided — set here, the ONE entry point both modes pass through
|
|
17577
17897
|
if (this.pagedMode) {
|
|
17578
17898
|
this.loadDataPaged(action, data);
|
|
17579
17899
|
return;
|
|
@@ -17611,10 +17931,36 @@ class TableComponent {
|
|
|
17611
17931
|
}
|
|
17612
17932
|
}
|
|
17613
17933
|
else {
|
|
17934
|
+
this.loadFailed = true; // Added: the dialog is dismissable and dedupes across tables — without this the grid is left saying "Loading..." with nothing still loading
|
|
17614
17935
|
this.apiErrorService.presentAppFailure(apiResponse, 'load', action.url); // Changed: a failed row load — dedupes with other loads so a broken backend yields one dialog, not one per table
|
|
17615
17936
|
}
|
|
17616
|
-
}
|
|
17617
|
-
|
|
17937
|
+
},
|
|
17938
|
+
// Added: the TRANSPORT failure — a dead API, a dropped connection, a CORS refusal. It never reaches the
|
|
17939
|
+
// branch above because there is no ApiResponse at all: LoaderInterceptor shows the dialog and rethrows,
|
|
17940
|
+
// and this subscribe had no error callback, so the grid was left rendering "No Data" — the one thing
|
|
17941
|
+
// that was definitely NOT true. Rethrown so the error still reaches Angular's global handler exactly as
|
|
17942
|
+
// it did before; this callback only records WHY the grid is empty.
|
|
17943
|
+
(err) => { this.loadFailed = true; throw err; });
|
|
17944
|
+
}
|
|
17945
|
+
}
|
|
17946
|
+
// Added (phone density, opt-in via config.hideSinglePagePaginator): true when the pager is navigating a
|
|
17947
|
+
// single page on a phone, and is therefore 56px of chrome that can reach nowhere it is not already. The
|
|
17948
|
+
// paginator is still RENDERED and only hidden in CSS — the same reason the empty-data case is a class and
|
|
17949
|
+
// not an *ngIf: #tablePaginator is a ViewChild that setPaginator() attaches to the MatTableDataSource, and
|
|
17950
|
+
// removing it from the DOM would break paging for every table that does have a second page.
|
|
17951
|
+
// Desktop returns false on the smallScreen guard alone, so nothing above 600px can be affected.
|
|
17952
|
+
get singlePagePaginatorHidden() {
|
|
17953
|
+
if (!this.config?.hideSinglePagePaginator || !this.smallScreen)
|
|
17954
|
+
return false;
|
|
17955
|
+
// On a phone hidePageSize is on, so the size cannot be changed from the UI and the paginator's own value
|
|
17956
|
+
// and pageSizes[0] agree; reading the live one first keeps this honest if it was changed before a resize.
|
|
17957
|
+
const size = this.pagedMode ? this.pageSize : (this.tablePaginator?.pageSize || this.config?.pageSizes?.[0] || 10);
|
|
17958
|
+
if (!size)
|
|
17959
|
+
return false;
|
|
17960
|
+
const total = this.pagedMode
|
|
17961
|
+
? (this.filterActive ? this.filteredRows.length : this.serverTotal + this.overlayDelta)
|
|
17962
|
+
: (this.dataSource?.length || 0);
|
|
17963
|
+
return total > 0 && total <= size; // zero rows is already handled by the existing empty-data rule
|
|
17618
17964
|
}
|
|
17619
17965
|
setPaginator() {
|
|
17620
17966
|
if (this.pagedMode)
|
|
@@ -17674,9 +18020,11 @@ class TableComponent {
|
|
|
17674
18020
|
}
|
|
17675
18021
|
}
|
|
17676
18022
|
else {
|
|
18023
|
+
this.loadFailed = true; // Added: same treatment as the unpaged path — a failed search load must not read as a load still running
|
|
17677
18024
|
this.apiErrorService.presentAppFailure(apiResponse, 'load', action.url); // Changed: paged-mode search load — same classify + dedupe treatment as the unpaged path above
|
|
17678
18025
|
}
|
|
17679
|
-
});
|
|
18026
|
+
}, (err) => { this.loadFailed = true; throw err; } // Added: transport failure — same reasoning as the unpaged path above
|
|
18027
|
+
);
|
|
17680
18028
|
return;
|
|
17681
18029
|
}
|
|
17682
18030
|
if (action.url !== this.pagedUrlBase) {
|
|
@@ -17708,6 +18056,7 @@ class TableComponent {
|
|
|
17708
18056
|
console.log(apiResponse);
|
|
17709
18057
|
}
|
|
17710
18058
|
if (!apiResponse.success) {
|
|
18059
|
+
this.loadFailed = true; // Added: only ever SEEN on a first chunk (the message is gated on !dataSource) — a failed page-forward leaves the rows already on screen alone
|
|
17711
18060
|
this.apiErrorService.presentAppFailure(apiResponse, 'load', pagedAction.url); // Changed: chunk fetch — scrolling a broken table fires these repeatedly, so deduping matters most here
|
|
17712
18061
|
return;
|
|
17713
18062
|
}
|
|
@@ -17735,7 +18084,7 @@ class TableComponent {
|
|
|
17735
18084
|
if (apiResponse.message != "success" && apiResponse.message != "") {
|
|
17736
18085
|
this.messageService.toast(apiResponse.message);
|
|
17737
18086
|
}
|
|
17738
|
-
}, () => { this.pageFetchInFlight = false; } // never leave the single-flight guard stuck on a failed request
|
|
18087
|
+
}, () => { this.pageFetchInFlight = false; this.loadFailed = true; } // never leave the single-flight guard stuck on a failed request; Added: a transport-level failure (no ApiResponse at all) is still a failed load
|
|
17739
18088
|
);
|
|
17740
18089
|
}
|
|
17741
18090
|
// Appends skip/take to a URL copy at fetch time
|
|
@@ -17815,6 +18164,17 @@ class TableComponent {
|
|
|
17815
18164
|
// so its text has to be relayed regardless of paged mode. Set before the paged-mode guard below — grouped
|
|
17816
18165
|
// views are not paged, and returning early here is what would leave the header filter doing nothing.
|
|
17817
18166
|
this.groupFilterText = (text ?? '').trim();
|
|
18167
|
+
// Added: build the filtered-empty headline here, where the text arrives for EVERY mode. spa-filter is the
|
|
18168
|
+
// only writer of MatTableDataSource.filter in the library and it emits on keyUp, clear and ngOnChanges
|
|
18169
|
+
// alike, so this field cannot drift from the filter the grid is actually applying. Long pastes are capped
|
|
18170
|
+
// so one field cannot push the message off a phone screen.
|
|
18171
|
+
const term = this.groupFilterText;
|
|
18172
|
+
const shown = term.length > 40 ? term.slice(0, 40) + '…' : term;
|
|
18173
|
+
this.filterEmptyTitle = term ? `No matches for “${shown}”` : 'No matches';
|
|
18174
|
+
// Added: the partial-window headline. Built here beside its sibling so both are computed once per filter
|
|
18175
|
+
// change rather than per change-detection pass. Neither string embeds a COUNT — counts are the banner's and
|
|
18176
|
+
// the paginator's to state — so neither can go stale if the loaded window grows while a filter is active.
|
|
18177
|
+
this.filterEmptyTitlePartial = term ? `No matches for “${shown}” in the rows loaded so far` : 'No matches in the rows loaded so far';
|
|
17818
18178
|
if (!this.pagedMode)
|
|
17819
18179
|
return;
|
|
17820
18180
|
this.filterText = (text ?? '').trim();
|
|
@@ -17830,7 +18190,9 @@ class TableComponent {
|
|
|
17830
18190
|
let config = new FormConfig;
|
|
17831
18191
|
config.fields = [];
|
|
17832
18192
|
this.config.columns.forEach(column => {
|
|
17833
|
-
|
|
18193
|
+
// Changed: monogram is a DISPLAY-only column type with no form editor — the value behind it is a plain
|
|
18194
|
+
// name string, so an auto-derived field falls back to text rather than leaking an unknown Field type.
|
|
18195
|
+
let field = { name: column.name, type: column.type === 'monogram' ? 'text' : column.type };
|
|
17834
18196
|
config.fields.push(field);
|
|
17835
18197
|
});
|
|
17836
18198
|
this.config.formConfig = config;
|
|
@@ -17978,8 +18340,11 @@ class TableComponent {
|
|
|
17978
18340
|
this.viewModel(actionData, actionButton);
|
|
17979
18341
|
}
|
|
17980
18342
|
else if (name === 'edit') {
|
|
17981
|
-
// Changed: inlineEdit tables open the row cells in place instead of the dialog
|
|
17982
|
-
|
|
18343
|
+
// Changed: inlineEdit tables open the row cells in place instead of the dialog — but ONLY when every
|
|
18344
|
+
// editable field actually has a cell on screen. Inline edit is an optimisation of the dialog, so when it
|
|
18345
|
+
// cannot show the whole editor it falls back to the dialog it was optimising: same row, same edit action,
|
|
18346
|
+
// every field rendered (a form has no minColumns). Nothing is submitted that the operator could not see.
|
|
18347
|
+
if (this.config.inlineEdit && this.canEditInline()) {
|
|
17983
18348
|
this.startInlineEdit(actionData);
|
|
17984
18349
|
return;
|
|
17985
18350
|
}
|
|
@@ -18067,6 +18432,8 @@ class TableComponent {
|
|
|
18067
18432
|
return;
|
|
18068
18433
|
}
|
|
18069
18434
|
this.dialogService.openDefaultDetailsDialog(btn, row, this.nestingLevel).subscribe(result => {
|
|
18435
|
+
if (!result)
|
|
18436
|
+
return; // Added: Escape or a backdrop click closes with undefined — a dismissal is a normal outcome, so fall straight out rather than throwing on result.action. No branch below is a default, so doing nothing is exactly right: no refresh, no emit, no onSuccessButton.
|
|
18070
18437
|
if (result.action === 'doAction') {
|
|
18071
18438
|
this.doAction(result.name, result.row);
|
|
18072
18439
|
this.actionClickedEmit(result.name, result.row);
|
|
@@ -18437,11 +18804,11 @@ class TableComponent {
|
|
|
18437
18804
|
this.setPaginator();
|
|
18438
18805
|
}
|
|
18439
18806
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableComponent, deps: [{ token: DataServiceLib }, { token: MessageService }, { token: i1$4.BreakpointObserver }, { token: i4.MatDialog }, { token: ButtonService }, { token: DialogService }, { token: TableConfigService }, { token: ConditionService }, { token: AuthService }, { token: SignalRService }, { token: OfflineService }, { token: ApiErrorService }, { token: TIN_SPA_RUNTIME_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
18440
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TableComponent, isStandalone: false, selector: "spa-table", inputs: { data: "data", tileData: "tileData", config: "config", localMode: "localMode", parentDetails: "parentDetails", reload: "reload", activeTab: "activeTab", inTab: "inTab", nestingLevel: "nestingLevel" }, outputs: { dataLoad: "dataLoad", totalChange: "totalChange", actionSuccess: "actionSuccess", refreshClick: "refreshClick", searchClick: "searchClick", createClick: "createClick", actionClick: "actionClick", inputChange: "inputChange", actionResponse: "actionResponse" }, viewQueries: [{ propertyName: "tablePaginator", first: true, predicate: ["tablePaginator"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "\n<ng-container *ngIf=\"hasFormAccess && !sectionHidden\"> <!-- Changed: sectionConfig.hideWhenEmpty hides the whole table -->\n\n <!-- Added: collapsible flat section header (sectionConfig) \u2014 1px border, no elevation, whole row toggles -->\n <div class=\"tbl-section-header\" *ngIf=\"config.sectionConfig\" (click)=\"toggleSection()\" [attr.aria-expanded]=\"!sectionCollapsed\" [class.tbl-section-static]=\"config.sectionConfig.collapsible === false\" role=\"button\" tabindex=\"0\" (keydown.enter)=\"toggleSection()\">\n <mat-icon class=\"tbl-section-icon\" *ngIf=\"config.sectionConfig.icon\">{{ config.sectionConfig.icon }}</mat-icon>\n <span class=\"tbl-section-title\">{{ config.sectionConfig.title }}</span>\n <span class=\"tbl-section-count\" *ngIf=\"config.sectionConfig.showCount !== false\">{{ dataSource?.length || 0 }}</span>\n <span class=\"tbl-section-chip\" *ngFor=\"let chip of sectionChips()\" [style.color]=\"chip.color\">{{ chip.text }}</span>\n <span class=\"tbl-section-spacer\"></span>\n <!-- Changed: the label is wrapped so a NARROW screen can drop it and leave an icon-only button. Only a\n button that HAS an icon loses its text \u2014 otherwise it would collapse to a blank square. -->\n <button mat-stroked-button color=\"primary\" *ngFor=\"let btn of sectionButtons()\" (click)=\"sectionButtonClicked(btn, $event)\" [matTooltip]=\"btn.display || btn.name\"><mat-icon *ngIf=\"btn.icon?.name\">{{ btn.icon.name }}</mat-icon><span class=\"tbl-section-btn-text\" [class.has-icon]=\"!!btn.icon?.name\">{{ btn.display || btn.name }}</span></button>\n <mat-icon class=\"tbl-section-chevron\" *ngIf=\"config.sectionConfig.collapsible !== false\">{{ sectionCollapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </div>\n\n <!-- Added: the section's \"why this list exists\" line. It sits UNDER the title (a reason only makes sense once\n the list has been named) and only while the section is open \u2014 a shut section already has its own summary\n line, and a paragraph over the top of that is noise. -->\n <p class=\"tbl-section-caption\" *ngIf=\"config.sectionConfig?.caption && !sectionCollapsed\">{{ config.sectionConfig.caption }}</p>\n\n <!-- Added: collapsed section affordance \u2014 mirrors the Day Book \"Show the N\" pattern -->\n <div class=\"tbl-section-more\" *ngIf=\"config.sectionConfig && sectionCollapsed && (dataSource?.length || 0) > 0\">\n <button type=\"button\" class=\"tbl-section-link\" (click)=\"toggleSection()\">Show the {{ dataSource.length }}</button>\n </div>\n\n <ng-container *ngIf=\"!config.sectionConfig || !sectionCollapsed\"> <!-- Added: section collapse hides the table body -->\n\n <!-- Search -->\n <spa-search\n *ngIf=\"config.searchConfig\" [config]=\"config.searchConfig\" [smallScreen]=\"smallScreen\" [tableDataSource]=\"tableDataSource\" style=\"margin-bottom: 20px;\" (searchClick)=\"searchClicked($event)\">\n </spa-search>\n\n <!-- Header -->\n <!-- Changed: [isRealTime] binds the RESOLVED value, not the raw config. It was `config.realTime`, so the live\n indicator only ever appeared on a table that set the flag ITSELF \u2014 a table relying on the app-wide\n `tableDefaults.realTime` was genuinely subscribed to SignalR (setupRealTimeSubscriptions and\n realTimeRefreshOrFallback both resolve through effRealTime) and simply never showed the dot. That is\n how \"Trips and Loads have no real time\" got reported: a status light disagreeing with the system it\n reports on. effRealTime also gets the inverse right \u2014 realTime:true under a global false still lights,\n and realTime:false under a global true stays dark. -->\n <app-table-header\n [config]=\"config\" [data]=\"dataSource\" [tableDataSource]=\"tableDataSource\" [tileConfig]=\"config.tileConfig\" [tileData]=\"tileData\" [tileReload]=\"tileReload\" [lastSearch]=\"lastSearch\" [smallScreen]=\"smallScreen\"\n [showFilterButton]=\"showFilterButton\" [isRealTime]=\"effRealTime\" [isConnected]=\"isSignalRConnected\" [refreshing]=\"loadingStage === 'refresh'\"\n (createClick)=\"newModel()\" (customClick)=\"customModel($event,null)\"\n (refreshClick)=\"refreshClicked()\" (tileClick)=\"tileClicked($event)\" (tileUnClick)=\"tileUnClicked($event)\" (filterChange)=\"filterChanged($event)\">\n </app-table-header>\n\n <!-- Added (Quiet Loading D4): refresh with data already on screen \u2014 a 2px line flush under the header and the\n spinning refresh icon are the ONLY signals. Rows stay live, clickable and un-dimmed while they swap. -->\n <div *ngIf=\"loadingStage === 'refresh'\" class=\"tin-load-line\" aria-hidden=\"true\">\n <span class=\"tin-load-line-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n\n <!-- Added: paged-mode filter hint \u2014 the client filter only covers rows loaded so far -->\n <div *ngIf=\"pagedMode && filterActive && loadedRows.length < serverTotal\" class=\"paged-filter-hint\">\n <mat-icon>info</mat-icon>\n <span>Filtering only the {{loadedRows.length}} loaded rows of {{serverTotal}}. {{ config.searchConfig ? 'Use Search for complete results.' : 'Refine with search for complete results.' }}</span>\n </div>\n\n\n <!-- Table -->\n <div *ngIf=\"!config.viewType || config?.viewType === 'table'\">\n\n <p *ngIf=\"!config\"><em>Configure Table</em></p>\n <p *ngIf=\"!dataSource && !loadingStage\"><em>Loading...</em></p> <!-- Changed (Quiet Loading): the bare text is replaced by the stage below while a quiet load is on screen -->\n\n <!-- Added (Quiet Loading D3): first load, nothing on screen yet. The progress module is the hero (eased bar +\n counting percentage + caption) and the ghost rows hold the exact space the real rows will fill, so the\n table does not jump when data lands. Only ever rendered when quiet loading is on. -->\n <div *ngIf=\"loadingStage === 'initial'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div class=\"tin-load-ghosts\">\n <div class=\"tin-load-ghost-row\" *ngFor=\"let r of ghostRows\">\n <div class=\"tin-load-ghost-cell\" *ngFor=\"let c of ghostColumns; let i = index\">\n <span class=\"tin-skel\" [style.width.%]=\"ghostWidth(i, c)\" [style.animation-delay.ms]=\"r * 120\"></span> <!-- staggered sweep: each row starts 120ms after the one above -->\n </div>\n </div>\n </div>\n\n </div>\n\n <div *ngIf=\"dataSource && loadingStage !== 'initial' && (!smallScreen || (smallScreen && dataSource?.length > 0))\" [class.tin-load-in]=\"effQuietLoading\"> <!-- Changed (Quiet Loading): the empty header-only table is suppressed while the initial stage stands in for it, and the real rows fade in where the ghosts were (D3) -->\n\n <table mat-table [dataSource]=\"tableDataSource\" [trackBy]=\"trackByRow\" [ngClass]=\"elevation\" [class.tin-no-col-headers]=\"config.hideColumnHeaders\"> <!-- Changed: optional column-header suppression -->\n\n <ng-container *ngFor=\"let column of config.columns\" [matColumnDef]=\"column.name\">\n <th mat-header-cell *matHeaderCellDef >{{ column.alias ?? column.name | camelToWords }}</th>\n <td mat-cell *matCellDef=\"let row;\" class=\"right-padding\" >\n\n <!-- Added: inline edit \u2014 editable cells swap to their form-field editor while the row is in edit mode -->\n <app-inline-cell *ngIf=\"isRowEditing(row) && getInlineField(column); else displayCell\" [field]=\"getInlineField(column)\" [data]=\"editingModel\"></app-inline-cell>\n\n <!-- Rows -->\n <ng-template #displayCell>\n <app-table-row [column]=\"column\" [row]=\"row\" [config]=\"config\" [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked(column.name, row)\" (columnClick)=\"columnClicked(column, row)\" (showBannerEvent)=\"showBanner($event)\">\n </app-table-row>\n </ng-template>\n\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"action\">\n <th mat-header-cell *matHeaderCellDef> Action </th>\n <td mat-cell *matCellDef=\"let row\" [ngStyle]=\"{width:false ? '20px' : actionsWidth}\">\n <div class=\"action-buttons-container\">\n\n <!-- Added: inline edit \u2014 while a row edits in place, its actions collapse to submit/cancel -->\n <ng-container *ngIf=\"isRowEditing(row); else rowActions\">\n <button mat-icon-button matTooltip=\"Save\" matTooltipPosition=\"above\" (click)=\"submitInlineEdit()\"><mat-icon class=\"inline-save\">check</mat-icon></button> <!-- Changed: dropped color=\"primary\" \u2014 the icon now carries a green save cue -->\n <button mat-icon-button matTooltip=\"Cancel\" matTooltipPosition=\"above\" (click)=\"cancelInlineEdit()\"><mat-icon class=\"inline-cancel\">close</mat-icon></button> <!-- Changed: red cancel cue -->\n </ng-container>\n\n <!-- Actions -->\n <ng-template #rowActions>\n <app-table-action\n [displayedButtons]=\"displayedButtons\" [config]=\"config\" [smallScreen]=\"smallScreen\" [row]=\"row\" (actionClick)=\"actionClicked($event.name, $event.row)\">\n </app-table-action>\n </ng-template>\n\n </div>\n </td>\n </ng-container>\n\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\" [ngClass]=\"{'make-gray': (config.greyOut && config.greyOut(row)) || row.pendingApproval, 'row-editing': isRowEditing(row)}\"></tr> <!-- Changed: row-editing flags the row that is open for inline edit -->\n </table>\n\n </div>\n\n <!-- Changed: Removed *ngIf condition to keep paginator always in DOM and maintain ViewChild reference -->\n <!-- Changed: Added CSS class binding to hide when no data instead of conditional rendering -->\n <!-- Changed: Legacy paginator only renders in non-paged mode (pagedMode is constant per instance, set before first render) -->\n <!-- Changed: hidePageSize on a phone. The \"Items per page\" label and its select cost a whole row of a\n narrow screen for a control almost nobody touches there \u2014 the default is what gets used. The range\n (\"1 \u2013 10 of 54\") and the arrows stay, which is the part that is actually navigated. Bound to\n smallScreen, the component's existing breakpoint (max-width 600px, live via BreakpointObserver), so\n the paginator agrees with how this table already decides what \"mobile\" means rather than\n introducing a third breakpoint. -->\n <mat-paginator *ngIf=\"!pagedMode\"\n #tablePaginator\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [hidePageSize]=\"smallScreen\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial'}\"\n showFirstLastButtons>\n </mat-paginator>\n\n <!-- Added: manual paginator for server-side paged mode \u2014 fully state-bound, never attached to MatTableDataSource. Always visible: when filtering it pages the in-memory filtered subset (length = filtered count); otherwise the server window (length = true total). No first/last jump (would force fetching the whole gap). -->\n <mat-paginator *ngIf=\"pagedMode\"\n [length]=\"filterActive ? filteredRows.length : serverTotal + overlayDelta\"\n [pageIndex]=\"pageIndex\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [hidePageSize]=\"smallScreen\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial'}\"\n (page)=\"onServerPage($event)\">\n </mat-paginator>\n\n </div>\n \n <!-- Added (Quiet Loading): the initial-load stage for the NON-table views.\n Without this, card/capsule/grouped showed literally nothing during a first load \u2014 the view component\n renders an empty dataSource and the \"No Data\" line is suppressed while the stage owns the space.\n The progress header is identical to the table's so the two feel like one feature; only the ghost\n furniture differs, because column-shaped rows are wrong in a card grid. -->\n <div *ngIf=\"loadingStage === 'initial' && config?.viewType && config?.viewType !== 'table'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div *ngIf=\"config?.viewType === 'capsule'\" class=\"tin-load-ghost-capsules\" aria-hidden=\"true\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let c of ghostCapsules\"></span>\n </div>\n\n <div *ngIf=\"config?.viewType === 'card'\" class=\"tin-load-ghost-cards\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-card\" *ngFor=\"let c of ghostCards\">\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-title\"></span>\n <span class=\"tin-skel tin-skel-text\"></span>\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-short\"></span>\n </div>\n </div>\n\n <div *ngIf=\"config?.viewType === 'grouped'\" class=\"tin-load-ghost-groups\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-group\" *ngFor=\"let g of ghostGroups\">\n <span class=\"tin-skel tin-load-ghost-group-head\"></span>\n <!-- Pills, not rows: a group card's body is a wrap of chips, so full-width bars promised a table\n and the stage did not resemble what replaced it. -->\n <div class=\"tin-load-ghost-group-items\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let r of ghostCards\"></span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Capsules -->\n <spa-capsules *ngIf=\"config?.viewType === 'capsule' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n (actionClick)=\"actionClicked($event.name, $event.row)\">\n </spa-capsules>\n\n\n <!-- Cards -->\n <spa-cards *ngIf=\"config?.viewType === 'card' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked($event.name, $event.row)\"\n (columnClick)=\"columnClicked($event.column, $event.row)\"\n (showBannerEvent)=\"showBanner($event)\">\n </spa-cards>\n\n <!-- Groups - Added: New grouped view type -->\n <!-- Changed: the grouped view no longer renders its own filter field. It used to sit on a row of its own\n beneath the buttons row, which left both rows half empty and, more importantly, was a SECOND filter with\n no refresh button. The standard header filter (which has refresh, like every other table) now drives it,\n with its text relayed in through filterText. -->\n <spa-groups *ngIf=\"config?.viewType === 'grouped' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [showOwnFilter]=\"false\"\n [filterText]=\"groupFilterText\"\n (actionClick)=\"actionClicked($event.name, $event.row, $event.group, $event.button)\">\n </spa-groups>\n\n\n <div class=\"tin-center\">\n <p *ngIf=\"dataSource?.length == 0 && loadingStage !== 'initial'\"><em>{{config.noDataMessage ?? 'No Data'}}</em></p> <!-- Changed (Quiet Loading): the stage owns the space until it completes, then hands straight over to this message \u2014 no \"No Data\" flashing underneath the ghost rows -->\n </div>\n\n </ng-container> <!-- Added: end section-collapse wrapper -->\n\n</ng-container>\n\n\n<ng-container *ngIf=\"!hasFormAccess\">\n <div class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n</ng-container>\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}.dlg-header-classic{padding-left:24px;padding-right:24px}.dlg-title-classic{font-size:20px;font-weight:500;margin-top:10px;margin-bottom:5px}.dialog-header-titles{display:flex;flex-direction:column;justify-content:center;min-width:0}.tin-dlg-head .dialog-header-titles label{margin:0}.tbl-section-header{display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:8px;border:1px solid rgba(0,0,0,.08);border-radius:10px;background:transparent;cursor:pointer;transition:border-color .15s}.tbl-section-header:hover{border-color:#90a4ae}.tbl-section-header.tbl-section-static{cursor:default}.tbl-section-icon{color:#546e7a}.tbl-section-title{font-size:14px;font-weight:600;color:#000000d1}.tbl-section-count{background:#e3f2fd;color:#1565c0;border-radius:12px;padding:2px 10px;font-size:12px}.tbl-section-chip{font-size:12px;font-weight:500;color:#0009}.tbl-section-spacer{margin-left:auto}.tbl-section-chevron{color:#90a4ae}.tbl-section-caption{margin:-4px 12px 10px;font-size:12.5px;line-height:1.45;color:#0000008c;max-width:82ch}.tbl-section-more{padding:0 12px 8px}.tbl-section-link{background:none;border:none;color:#1565c0;cursor:pointer;font-size:13px;padding:0}.tbl-section-link:hover{text-decoration:underline}.tbl-section-icon,.tbl-section-chevron,.tbl-section-count{flex:0 0 auto}.tbl-section-title{flex:1 1 auto;min-width:0}@media (max-width: 700px){.tbl-section-header{gap:8px;padding:10px}.tbl-section-chip,.tbl-section-btn-text.has-icon{display:none}.tbl-section-header button{min-width:0;padding:0 10px}}table.tin-no-col-headers tr.mat-mdc-header-row{display:none}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i14.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i14.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i14.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i14.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i14.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i14.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i14.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i14.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i14.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i14.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i15$1.MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SearchComponent, selector: "spa-search", inputs: ["config", "smallScreen", "tableDataSource"], outputs: ["searchClick"] }, { kind: "component", type: TableHeaderComponent, selector: "app-table-header", inputs: ["lastSearch", "config", "hideTitle", "tableDataSource", "tileConfig", "smallScreen", "tileReload", "showFilterButton", "data", "tileData", "isRealTime", "isConnected", "refreshing"], outputs: ["createClick", "customClick", "refreshClick", "tileClick", "tileUnClick", "filterChange"] }, { kind: "component", type: TableRowComponent, selector: "app-table-row", inputs: ["column", "row", "config", "smallScreen"], outputs: ["actionClick", "columnClick", "showBannerEvent"] }, { kind: "component", type: TableActionComponent, selector: "app-table-action", inputs: ["displayedButtons", "config", "row", "smallScreen"], outputs: ["actionClick"] }, { kind: "component", type: InlineCellComponent, selector: "app-inline-cell", inputs: ["field", "data"], outputs: ["valueChange"] }, { kind: "component", type: CapsulesComponent, selector: "spa-capsules", inputs: ["config", "dataSource", "displayedButtons"], outputs: ["actionClick"] }, { kind: "component", type: CardsComponent, selector: "spa-cards", inputs: ["config", "dataSource", "displayedButtons", "smallScreen"], outputs: ["actionClick", "columnClick", "showBannerEvent"] }, { kind: "component", type: GroupsComponent, selector: "spa-groups", inputs: ["config", "dataSource", "displayedButtons", "showOwnFilter", "filterText"], outputs: ["actionClick"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
|
|
18807
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TableComponent, isStandalone: false, selector: "spa-table", inputs: { data: "data", tileData: "tileData", config: "config", localMode: "localMode", parentDetails: "parentDetails", reload: "reload", activeTab: "activeTab", inTab: "inTab", nestingLevel: "nestingLevel" }, outputs: { dataLoad: "dataLoad", totalChange: "totalChange", actionSuccess: "actionSuccess", refreshClick: "refreshClick", searchClick: "searchClick", createClick: "createClick", actionClick: "actionClick", inputChange: "inputChange", actionResponse: "actionResponse" }, viewQueries: [{ propertyName: "tablePaginator", first: true, predicate: ["tablePaginator"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "\n<ng-container *ngIf=\"hasFormAccess && !sectionHidden\"> <!-- Changed: sectionConfig.hideWhenEmpty hides the whole table -->\n\n <!-- Added: collapsible flat section header (sectionConfig) \u2014 1px border, no elevation, whole row toggles -->\n <div class=\"tbl-section-header\" *ngIf=\"config.sectionConfig\" (click)=\"toggleSection()\" [attr.aria-expanded]=\"!sectionCollapsed\" [class.tbl-section-static]=\"config.sectionConfig.collapsible === false\" role=\"button\" tabindex=\"0\" (keydown.enter)=\"toggleSection()\">\n <mat-icon class=\"tbl-section-icon\" *ngIf=\"config.sectionConfig.icon\">{{ config.sectionConfig.icon }}</mat-icon>\n <span class=\"tbl-section-title\">{{ config.sectionConfig.title }}</span>\n <span class=\"tbl-section-count\" *ngIf=\"config.sectionConfig.showCount !== false\">{{ dataSource?.length || 0 }}</span>\n <span class=\"tbl-section-chip\" *ngFor=\"let chip of sectionChips()\" [style.color]=\"chip.color\">{{ chip.text }}</span>\n <span class=\"tbl-section-spacer\"></span>\n <!-- Changed: the label is wrapped so a NARROW screen can drop it and leave an icon-only button. Only a\n button that HAS an icon loses its text \u2014 otherwise it would collapse to a blank square. -->\n <button mat-stroked-button color=\"primary\" *ngFor=\"let btn of sectionButtons()\" (click)=\"sectionButtonClicked(btn, $event)\" [matTooltip]=\"btn.display || btn.name\"><mat-icon *ngIf=\"btn.icon?.name\">{{ btn.icon.name }}</mat-icon><span class=\"tbl-section-btn-text\" [class.has-icon]=\"!!btn.icon?.name\">{{ btn.display || btn.name }}</span></button>\n <mat-icon class=\"tbl-section-chevron\" *ngIf=\"config.sectionConfig.collapsible !== false\">{{ sectionCollapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </div>\n\n <!-- Added: the section's \"why this list exists\" line. It sits UNDER the title (a reason only makes sense once\n the list has been named) and only while the section is open \u2014 a shut section already has its own summary\n line, and a paragraph over the top of that is noise. -->\n <p class=\"tbl-section-caption\" *ngIf=\"config.sectionConfig?.caption && !sectionCollapsed\">{{ config.sectionConfig.caption }}</p>\n\n <!-- Added: collapsed section affordance \u2014 mirrors the Day Book \"Show the N\" pattern -->\n <div class=\"tbl-section-more\" *ngIf=\"config.sectionConfig && sectionCollapsed && (dataSource?.length || 0) > 0\">\n <button type=\"button\" class=\"tbl-section-link\" (click)=\"toggleSection()\">Show the {{ dataSource.length }}</button>\n </div>\n\n <ng-container *ngIf=\"!config.sectionConfig || !sectionCollapsed\"> <!-- Added: section collapse hides the table body -->\n\n <!-- Search -->\n <spa-search\n *ngIf=\"config.searchConfig\" [config]=\"config.searchConfig\" [smallScreen]=\"smallScreen\" [tableDataSource]=\"tableDataSource\" style=\"margin-bottom: 20px;\" (searchClick)=\"searchClicked($event)\">\n </spa-search>\n\n <!-- Header -->\n <!-- Changed: [isRealTime] binds the RESOLVED value, not the raw config. It was `config.realTime`, so the live\n indicator only ever appeared on a table that set the flag ITSELF \u2014 a table relying on the app-wide\n `tableDefaults.realTime` was genuinely subscribed to SignalR (setupRealTimeSubscriptions and\n realTimeRefreshOrFallback both resolve through effRealTime) and simply never showed the dot. That is\n how \"Trips and Loads have no real time\" got reported: a status light disagreeing with the system it\n reports on. effRealTime also gets the inverse right \u2014 realTime:true under a global false still lights,\n and realTime:false under a global true stays dark. -->\n <app-table-header\n [config]=\"config\" [data]=\"dataSource\" [tableDataSource]=\"tableDataSource\" [tileConfig]=\"config.tileConfig\" [tileData]=\"tileData\" [tileReload]=\"tileReload\" [lastSearch]=\"lastSearch\" [smallScreen]=\"smallScreen\"\n [showFilterButton]=\"showFilterButton\" [isRealTime]=\"effRealTime\" [isConnected]=\"isSignalRConnected\" [refreshing]=\"loadingStage === 'refresh'\"\n (createClick)=\"newModel()\" (customClick)=\"customModel($event,null)\"\n (refreshClick)=\"refreshClicked()\" (tileClick)=\"tileClicked($event)\" (tileUnClick)=\"tileUnClicked($event)\" (filterChange)=\"filterChanged($event)\">\n </app-table-header>\n\n <!-- Added (Quiet Loading D4): refresh with data already on screen \u2014 a 2px line flush under the header and the\n spinning refresh icon are the ONLY signals. Rows stay live, clickable and un-dimmed while they swap. -->\n <div *ngIf=\"loadingStage === 'refresh'\" class=\"tin-load-line\" aria-hidden=\"true\">\n <span class=\"tin-load-line-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n\n <!-- Added: paged-mode filter hint \u2014 the client filter only covers rows loaded so far.\n Changed: the condition moved into filterScopePartial so this banner and the empty-state message below\n read the SAME predicate. Stated independently they drifted: this banner said \"only the 20 loaded rows\n of 240\" while the message underneath offered \"all 240 rows\". This banner owns SCOPE (what was searched)\n and the remedy; the message below owns the RESULT; the paginator owns the COUNT. One fact each. -->\n <div *ngIf=\"filterScopePartial\" class=\"paged-filter-hint\">\n <mat-icon>info</mat-icon>\n <span>Filtering only the {{loadedRows.length}} loaded rows of {{serverTotal}}. {{ config.searchConfig ? 'Use Search for complete results.' : 'Refine with search for complete results.' }}</span>\n </div>\n\n\n <!-- Table -->\n <div *ngIf=\"!config.viewType || config?.viewType === 'table'\">\n\n <p *ngIf=\"!config\"><em>Configure Table</em></p>\n <p *ngIf=\"!dataSource && !loadingStage && !loadFailed\"><em>Loading...</em></p> <!-- Changed (Quiet Loading): the bare text is replaced by the stage below while a quiet load is on screen --><!-- Changed: ...and a FAILED load is not a running one \u2014 without the loadFailed guard this line sat there forever once the error dialog was dismissed -->\n\n <!-- Added (Quiet Loading D3): first load, nothing on screen yet. The progress module is the hero (eased bar +\n counting percentage + caption) and the ghost rows hold the exact space the real rows will fill, so the\n table does not jump when data lands. Only ever rendered when quiet loading is on. -->\n <div *ngIf=\"loadingStage === 'initial'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div class=\"tin-load-ghosts\">\n <div class=\"tin-load-ghost-row\" *ngFor=\"let r of ghostRows\">\n <div class=\"tin-load-ghost-cell\" *ngFor=\"let c of ghostColumns; let i = index\">\n <span class=\"tin-skel\" [style.width.%]=\"ghostWidth(i, c)\" [style.animation-delay.ms]=\"r * 120\"></span> <!-- staggered sweep: each row starts 120ms after the one above -->\n </div>\n </div>\n </div>\n\n </div>\n\n <div *ngIf=\"dataSource && loadingStage !== 'initial' && (!smallScreen || (smallScreen && dataSource?.length > 0))\" [class.tin-load-in]=\"effQuietLoading\"> <!-- Changed (Quiet Loading): the empty header-only table is suppressed while the initial stage stands in for it, and the real rows fade in where the ghosts were (D3) -->\n\n <table mat-table [dataSource]=\"tableDataSource\" [trackBy]=\"trackByRow\" [ngClass]=\"elevation\" [class.tin-no-col-headers]=\"config.hideColumnHeaders\"> <!-- Changed: optional column-header suppression -->\n\n <ng-container *ngFor=\"let column of config.columns\" [matColumnDef]=\"column.name\">\n <th mat-header-cell *matHeaderCellDef >{{ column.alias ?? column.name | camelToWords }}</th>\n <td mat-cell *matCellDef=\"let row;\" class=\"right-padding\" >\n\n <!-- Added: inline edit \u2014 editable cells swap to their form-field editor while the row is in edit mode -->\n <app-inline-cell *ngIf=\"isRowEditing(row) && getInlineField(column); else displayCell\" [field]=\"getInlineField(column)\" [data]=\"editingModel\"></app-inline-cell>\n\n <!-- Rows -->\n <ng-template #displayCell>\n <app-table-row [column]=\"column\" [row]=\"row\" [config]=\"config\" [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked(column.name, row)\" (columnClick)=\"columnClicked(column, row)\" (showBannerEvent)=\"showBanner($event)\">\n </app-table-row>\n </ng-template>\n\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"action\">\n <th mat-header-cell *matHeaderCellDef> Action </th>\n <td mat-cell *matCellDef=\"let row\" [ngStyle]=\"{width:false ? '20px' : actionsWidth}\">\n <div class=\"action-buttons-container\">\n\n <!-- Added: inline edit \u2014 while a row edits in place, its actions collapse to submit/cancel -->\n <ng-container *ngIf=\"isRowEditing(row); else rowActions\">\n <button mat-icon-button matTooltip=\"Save\" matTooltipPosition=\"above\" (click)=\"submitInlineEdit()\"><mat-icon class=\"inline-save\">check</mat-icon></button> <!-- Changed: dropped color=\"primary\" \u2014 the icon now carries a green save cue -->\n <button mat-icon-button matTooltip=\"Cancel\" matTooltipPosition=\"above\" (click)=\"cancelInlineEdit()\"><mat-icon class=\"inline-cancel\">close</mat-icon></button> <!-- Changed: red cancel cue -->\n </ng-container>\n\n <!-- Actions -->\n <ng-template #rowActions>\n <app-table-action\n [displayedButtons]=\"displayedButtons\" [config]=\"config\" [smallScreen]=\"smallScreen\" [row]=\"row\" (actionClick)=\"actionClicked($event.name, $event.row)\">\n </app-table-action>\n </ng-template>\n\n </div>\n </td>\n </ng-container>\n\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\" [ngClass]=\"{'make-gray': (config.greyOut && config.greyOut(row)) || row.pendingApproval, 'row-editing': isRowEditing(row)}\"></tr> <!-- Changed: row-editing flags the row that is open for inline edit -->\n </table>\n\n </div>\n\n <!-- Changed: Removed *ngIf condition to keep paginator always in DOM and maintain ViewChild reference -->\n <!-- Changed: Added CSS class binding to hide when no data instead of conditional rendering -->\n <!-- Changed: Legacy paginator only renders in non-paged mode (pagedMode is constant per instance, set before first render) -->\n <!-- Changed: hidePageSize on a phone. The \"Items per page\" label and its select cost a whole row of a\n narrow screen for a control almost nobody touches there \u2014 the default is what gets used. The range\n (\"1 \u2013 10 of 54\") and the arrows stay, which is the part that is actually navigated. Bound to\n smallScreen, the component's existing breakpoint (max-width 600px, live via BreakpointObserver), so\n the paginator agrees with how this table already decides what \"mobile\" means rather than\n introducing a third breakpoint. -->\n <mat-paginator *ngIf=\"!pagedMode\"\n #tablePaginator\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [hidePageSize]=\"smallScreen\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial' || singlePagePaginatorHidden}\"\n showFirstLastButtons>\n </mat-paginator>\n\n <!-- Added: manual paginator for server-side paged mode \u2014 fully state-bound, never attached to MatTableDataSource. Always visible: when filtering it pages the in-memory filtered subset (length = filtered count); otherwise the server window (length = true total). No first/last jump (would force fetching the whole gap). -->\n <mat-paginator *ngIf=\"pagedMode\"\n [length]=\"filterActive ? filteredRows.length : serverTotal + overlayDelta\"\n [pageIndex]=\"pageIndex\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [hidePageSize]=\"smallScreen\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial' || singlePagePaginatorHidden}\"\n (page)=\"onServerPage($event)\">\n </mat-paginator>\n\n </div>\n \n <!-- Added (Quiet Loading): the initial-load stage for the NON-table views.\n Without this, card/capsule/grouped showed literally nothing during a first load \u2014 the view component\n renders an empty dataSource and the \"No Data\" line is suppressed while the stage owns the space.\n The progress header is identical to the table's so the two feel like one feature; only the ghost\n furniture differs, because column-shaped rows are wrong in a card grid. -->\n <div *ngIf=\"loadingStage === 'initial' && config?.viewType && config?.viewType !== 'table'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div *ngIf=\"config?.viewType === 'capsule'\" class=\"tin-load-ghost-capsules\" aria-hidden=\"true\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let c of ghostCapsules\"></span>\n </div>\n\n <div *ngIf=\"config?.viewType === 'card'\" class=\"tin-load-ghost-cards\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-card\" *ngFor=\"let c of ghostCards\">\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-title\"></span>\n <span class=\"tin-skel tin-skel-text\"></span>\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-short\"></span>\n </div>\n </div>\n\n <div *ngIf=\"config?.viewType === 'grouped'\" class=\"tin-load-ghost-groups\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-group\" *ngFor=\"let g of ghostGroups\">\n <span class=\"tin-skel tin-load-ghost-group-head\"></span>\n <!-- Pills, not rows: a group card's body is a wrap of chips, so full-width bars promised a table\n and the stage did not resemble what replaced it. -->\n <div class=\"tin-load-ghost-group-items\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let r of ghostCards\"></span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Capsules -->\n <spa-capsules *ngIf=\"config?.viewType === 'capsule' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n (actionClick)=\"actionClicked($event.name, $event.row)\">\n </spa-capsules>\n\n\n <!-- Cards -->\n <spa-cards *ngIf=\"config?.viewType === 'card' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked($event.name, $event.row)\"\n (columnClick)=\"columnClicked($event.column, $event.row)\"\n (showBannerEvent)=\"showBanner($event)\">\n </spa-cards>\n\n <!-- Groups - Added: New grouped view type -->\n <!-- Changed: the grouped view no longer renders its own filter field. It used to sit on a row of its own\n beneath the buttons row, which left both rows half empty and, more importantly, was a SECOND filter with\n no refresh button. The standard header filter (which has refresh, like every other table) now drives it,\n with its text relayed in through filterText. -->\n <spa-groups *ngIf=\"config?.viewType === 'grouped' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [showOwnFilter]=\"false\"\n [filterText]=\"groupFilterText\"\n (actionClick)=\"actionClicked($event.name, $event.row, $event.group, $event.button)\">\n </spa-groups>\n\n\n <div class=\"tin-center\">\n <p *ngIf=\"dataSource?.length == 0 && loadingStage !== 'initial' && !loadFailed\"><em>{{config.noDataMessage ?? 'No Data'}}</em></p> <!-- Changed (Quiet Loading): the stage owns the space until it completes, then hands straight over to this message \u2014 no \"No Data\" flashing underneath the ghost rows --><!-- Changed: ...and NOT when the load failed. A failed read leaves dataSource as an empty array, so this line confidently reported \"No Data\" \u2014 an assertion about the data \u2014 when the truth was that we never got any. That state now has its own sentence below. -->\n\n <!-- Added: the load-failed state. \"There is nothing here\" and \"we could not find out\" are different facts and\n this grid stated the first for both. Gated on having no rows, so a failed REFRESH leaves the rows already\n on screen alone \u2014 the error dialog covers that case and an empty state under live rows would be a lie. -->\n <div *ngIf=\"!(dataSource?.length > 0) && !loadingStage && loadFailed\" class=\"tin-filter-empty\" role=\"status\">\n <p class=\"tin-filter-empty-title\"><em>Couldn\u2019t load {{stageEntityName}}</em></p>\n <p class=\"tin-filter-empty-hint\">Check your connection, then use the refresh button to try again.</p>\n </div>\n\n <!-- Added: the OTHER empty state. The line above means \"there is nothing here yet\"; this one means \"nothing\n matches what you typed\", and until now it rendered nothing at all \u2014 a header, a \"0 of 0\" paginator and\n no reason. filteredToNothing is false whenever dataSource is empty, so the two can never both show and\n the line above is reached on exactly the rows it always was. role=\"status\" announces the change to a\n screen reader, which otherwise gets silence when the rows disappear. -->\n <!-- Changed: ONE message became TWO, because \"no matches\" means two different things and only one of them\n was ever true. In paged mode the client filter runs over the loaded WINDOW and issues no fetch, so with\n a partial window \"No matches for X\" overstates what was searched and \"see all 240 rows\" implies the\n filter covered 240 when it covered 20 \u2014 while the banner at the top of the grid was simultaneously\n saying so. The split is driven by filterScopePartial, the same getter that banner binds to, so the two\n cannot contradict each other. Neither variant restates a COUNT the paginator or the banner already owns. -->\n\n <!-- COMPLETE scope: the filter saw every row there is (any unpaged grid, or a paged one holding the full\n set \u2014 search/POST mode and the no-total degrade both land here). The total is honest, so it is offered. -->\n <div *ngIf=\"filteredToNothing && !filterScopePartial\" class=\"tin-filter-empty\" role=\"status\">\n <p class=\"tin-filter-empty-title\"><em>{{filterEmptyTitle}}</em></p>\n <p class=\"tin-filter-empty-hint\">Clear the filter to see all {{filteredTotal}} {{filteredRowNoun}}.</p>\n </div>\n\n <!-- PARTIAL scope: the filter saw only the rows loaded so far. The headline says exactly that, and the hint\n stops at the way back \u2014 the banner above already carries both the counts and the \"use search\" remedy,\n so repeating either here would be the contradiction this split exists to remove. -->\n <div *ngIf=\"filteredToNothing && filterScopePartial\" class=\"tin-filter-empty\" role=\"status\">\n <p class=\"tin-filter-empty-title\"><em>{{filterEmptyTitlePartial}}</em></p>\n <p class=\"tin-filter-empty-hint\">Clear the filter to keep browsing.</p>\n </div>\n </div>\n\n </ng-container> <!-- Added: end section-collapse wrapper -->\n\n</ng-container>\n\n\n<ng-container *ngIf=\"!hasFormAccess\">\n <div class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n</ng-container>\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}.dlg-header-classic{padding-left:24px;padding-right:24px}.dlg-title-classic{font-size:20px;font-weight:500;margin-top:10px;margin-bottom:5px}.dialog-header-titles{display:flex;flex-direction:column;justify-content:center;min-width:0}.tin-dlg-head .dialog-header-titles label{margin:0}.tbl-section-header{display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:8px;border:1px solid rgba(0,0,0,.08);border-radius:10px;background:transparent;cursor:pointer;transition:border-color .15s}.tbl-section-header:hover{border-color:#90a4ae}.tbl-section-header.tbl-section-static{cursor:default}.tbl-section-icon{color:#546e7a}.tbl-section-title{font-size:14px;font-weight:600;color:#000000d1}.tbl-section-count{background:#e3f2fd;color:#1565c0;border-radius:12px;padding:2px 10px;font-size:12px}.tbl-section-chip{font-size:12px;font-weight:500;color:#0009}.tbl-section-spacer{margin-left:auto}.tbl-section-chevron{color:#90a4ae}.tbl-section-caption{margin:-4px 12px 10px;font-size:12.5px;line-height:1.45;color:#0000008c;max-width:82ch}.tbl-section-more{padding:0 12px 8px}.tbl-section-link{background:none;border:none;color:#1565c0;cursor:pointer;font-size:13px;padding:0}.tbl-section-link:hover{text-decoration:underline}.tbl-section-icon,.tbl-section-chevron,.tbl-section-count{flex:0 0 auto}.tbl-section-title{flex:1 1 auto;min-width:0}@media (max-width: 700px){.tbl-section-header{gap:8px;padding:10px}.tbl-section-chip,.tbl-section-btn-text.has-icon{display:none}.tbl-section-header button{min-width:0;padding:0 10px}}table.tin-no-col-headers tr.mat-mdc-header-row{display:none}.tin-filter-empty{text-align:center;padding:24px 12px}.tin-filter-empty-title{margin:0}.tin-filter-empty-hint{margin:6px 0 0;font-size:.85em;opacity:.7;overflow-wrap:anywhere}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i14.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i14.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i14.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i14.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i14.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i14.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i14.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i14.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i14.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i14.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i15$1.MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SearchComponent, selector: "spa-search", inputs: ["config", "smallScreen", "tableDataSource"], outputs: ["searchClick"] }, { kind: "component", type: TableHeaderComponent, selector: "app-table-header", inputs: ["lastSearch", "config", "hideTitle", "tableDataSource", "tileConfig", "smallScreen", "tileReload", "showFilterButton", "data", "tileData", "isRealTime", "isConnected", "refreshing"], outputs: ["createClick", "customClick", "refreshClick", "tileClick", "tileUnClick", "filterChange"] }, { kind: "component", type: TableRowComponent, selector: "app-table-row", inputs: ["column", "row", "config", "smallScreen"], outputs: ["actionClick", "columnClick", "showBannerEvent"] }, { kind: "component", type: TableActionComponent, selector: "app-table-action", inputs: ["displayedButtons", "config", "row", "smallScreen"], outputs: ["actionClick"] }, { kind: "component", type: InlineCellComponent, selector: "app-inline-cell", inputs: ["field", "data"], outputs: ["valueChange"] }, { kind: "component", type: CapsulesComponent, selector: "spa-capsules", inputs: ["config", "dataSource", "displayedButtons"], outputs: ["actionClick"] }, { kind: "component", type: CardsComponent, selector: "spa-cards", inputs: ["config", "dataSource", "displayedButtons", "smallScreen"], outputs: ["actionClick", "columnClick", "showBannerEvent"] }, { kind: "component", type: GroupsComponent, selector: "spa-groups", inputs: ["config", "dataSource", "displayedButtons", "showOwnFilter", "filterText"], outputs: ["actionClick"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
|
|
18441
18808
|
}
|
|
18442
18809
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableComponent, decorators: [{
|
|
18443
18810
|
type: Component,
|
|
18444
|
-
args: [{ selector: 'spa-table', standalone: false, template: "\n<ng-container *ngIf=\"hasFormAccess && !sectionHidden\"> <!-- Changed: sectionConfig.hideWhenEmpty hides the whole table -->\n\n <!-- Added: collapsible flat section header (sectionConfig) \u2014 1px border, no elevation, whole row toggles -->\n <div class=\"tbl-section-header\" *ngIf=\"config.sectionConfig\" (click)=\"toggleSection()\" [attr.aria-expanded]=\"!sectionCollapsed\" [class.tbl-section-static]=\"config.sectionConfig.collapsible === false\" role=\"button\" tabindex=\"0\" (keydown.enter)=\"toggleSection()\">\n <mat-icon class=\"tbl-section-icon\" *ngIf=\"config.sectionConfig.icon\">{{ config.sectionConfig.icon }}</mat-icon>\n <span class=\"tbl-section-title\">{{ config.sectionConfig.title }}</span>\n <span class=\"tbl-section-count\" *ngIf=\"config.sectionConfig.showCount !== false\">{{ dataSource?.length || 0 }}</span>\n <span class=\"tbl-section-chip\" *ngFor=\"let chip of sectionChips()\" [style.color]=\"chip.color\">{{ chip.text }}</span>\n <span class=\"tbl-section-spacer\"></span>\n <!-- Changed: the label is wrapped so a NARROW screen can drop it and leave an icon-only button. Only a\n button that HAS an icon loses its text \u2014 otherwise it would collapse to a blank square. -->\n <button mat-stroked-button color=\"primary\" *ngFor=\"let btn of sectionButtons()\" (click)=\"sectionButtonClicked(btn, $event)\" [matTooltip]=\"btn.display || btn.name\"><mat-icon *ngIf=\"btn.icon?.name\">{{ btn.icon.name }}</mat-icon><span class=\"tbl-section-btn-text\" [class.has-icon]=\"!!btn.icon?.name\">{{ btn.display || btn.name }}</span></button>\n <mat-icon class=\"tbl-section-chevron\" *ngIf=\"config.sectionConfig.collapsible !== false\">{{ sectionCollapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </div>\n\n <!-- Added: the section's \"why this list exists\" line. It sits UNDER the title (a reason only makes sense once\n the list has been named) and only while the section is open \u2014 a shut section already has its own summary\n line, and a paragraph over the top of that is noise. -->\n <p class=\"tbl-section-caption\" *ngIf=\"config.sectionConfig?.caption && !sectionCollapsed\">{{ config.sectionConfig.caption }}</p>\n\n <!-- Added: collapsed section affordance \u2014 mirrors the Day Book \"Show the N\" pattern -->\n <div class=\"tbl-section-more\" *ngIf=\"config.sectionConfig && sectionCollapsed && (dataSource?.length || 0) > 0\">\n <button type=\"button\" class=\"tbl-section-link\" (click)=\"toggleSection()\">Show the {{ dataSource.length }}</button>\n </div>\n\n <ng-container *ngIf=\"!config.sectionConfig || !sectionCollapsed\"> <!-- Added: section collapse hides the table body -->\n\n <!-- Search -->\n <spa-search\n *ngIf=\"config.searchConfig\" [config]=\"config.searchConfig\" [smallScreen]=\"smallScreen\" [tableDataSource]=\"tableDataSource\" style=\"margin-bottom: 20px;\" (searchClick)=\"searchClicked($event)\">\n </spa-search>\n\n <!-- Header -->\n <!-- Changed: [isRealTime] binds the RESOLVED value, not the raw config. It was `config.realTime`, so the live\n indicator only ever appeared on a table that set the flag ITSELF \u2014 a table relying on the app-wide\n `tableDefaults.realTime` was genuinely subscribed to SignalR (setupRealTimeSubscriptions and\n realTimeRefreshOrFallback both resolve through effRealTime) and simply never showed the dot. That is\n how \"Trips and Loads have no real time\" got reported: a status light disagreeing with the system it\n reports on. effRealTime also gets the inverse right \u2014 realTime:true under a global false still lights,\n and realTime:false under a global true stays dark. -->\n <app-table-header\n [config]=\"config\" [data]=\"dataSource\" [tableDataSource]=\"tableDataSource\" [tileConfig]=\"config.tileConfig\" [tileData]=\"tileData\" [tileReload]=\"tileReload\" [lastSearch]=\"lastSearch\" [smallScreen]=\"smallScreen\"\n [showFilterButton]=\"showFilterButton\" [isRealTime]=\"effRealTime\" [isConnected]=\"isSignalRConnected\" [refreshing]=\"loadingStage === 'refresh'\"\n (createClick)=\"newModel()\" (customClick)=\"customModel($event,null)\"\n (refreshClick)=\"refreshClicked()\" (tileClick)=\"tileClicked($event)\" (tileUnClick)=\"tileUnClicked($event)\" (filterChange)=\"filterChanged($event)\">\n </app-table-header>\n\n <!-- Added (Quiet Loading D4): refresh with data already on screen \u2014 a 2px line flush under the header and the\n spinning refresh icon are the ONLY signals. Rows stay live, clickable and un-dimmed while they swap. -->\n <div *ngIf=\"loadingStage === 'refresh'\" class=\"tin-load-line\" aria-hidden=\"true\">\n <span class=\"tin-load-line-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n\n <!-- Added: paged-mode filter hint \u2014 the client filter only covers rows loaded so far -->\n <div *ngIf=\"pagedMode && filterActive && loadedRows.length < serverTotal\" class=\"paged-filter-hint\">\n <mat-icon>info</mat-icon>\n <span>Filtering only the {{loadedRows.length}} loaded rows of {{serverTotal}}. {{ config.searchConfig ? 'Use Search for complete results.' : 'Refine with search for complete results.' }}</span>\n </div>\n\n\n <!-- Table -->\n <div *ngIf=\"!config.viewType || config?.viewType === 'table'\">\n\n <p *ngIf=\"!config\"><em>Configure Table</em></p>\n <p *ngIf=\"!dataSource && !loadingStage\"><em>Loading...</em></p> <!-- Changed (Quiet Loading): the bare text is replaced by the stage below while a quiet load is on screen -->\n\n <!-- Added (Quiet Loading D3): first load, nothing on screen yet. The progress module is the hero (eased bar +\n counting percentage + caption) and the ghost rows hold the exact space the real rows will fill, so the\n table does not jump when data lands. Only ever rendered when quiet loading is on. -->\n <div *ngIf=\"loadingStage === 'initial'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div class=\"tin-load-ghosts\">\n <div class=\"tin-load-ghost-row\" *ngFor=\"let r of ghostRows\">\n <div class=\"tin-load-ghost-cell\" *ngFor=\"let c of ghostColumns; let i = index\">\n <span class=\"tin-skel\" [style.width.%]=\"ghostWidth(i, c)\" [style.animation-delay.ms]=\"r * 120\"></span> <!-- staggered sweep: each row starts 120ms after the one above -->\n </div>\n </div>\n </div>\n\n </div>\n\n <div *ngIf=\"dataSource && loadingStage !== 'initial' && (!smallScreen || (smallScreen && dataSource?.length > 0))\" [class.tin-load-in]=\"effQuietLoading\"> <!-- Changed (Quiet Loading): the empty header-only table is suppressed while the initial stage stands in for it, and the real rows fade in where the ghosts were (D3) -->\n\n <table mat-table [dataSource]=\"tableDataSource\" [trackBy]=\"trackByRow\" [ngClass]=\"elevation\" [class.tin-no-col-headers]=\"config.hideColumnHeaders\"> <!-- Changed: optional column-header suppression -->\n\n <ng-container *ngFor=\"let column of config.columns\" [matColumnDef]=\"column.name\">\n <th mat-header-cell *matHeaderCellDef >{{ column.alias ?? column.name | camelToWords }}</th>\n <td mat-cell *matCellDef=\"let row;\" class=\"right-padding\" >\n\n <!-- Added: inline edit \u2014 editable cells swap to their form-field editor while the row is in edit mode -->\n <app-inline-cell *ngIf=\"isRowEditing(row) && getInlineField(column); else displayCell\" [field]=\"getInlineField(column)\" [data]=\"editingModel\"></app-inline-cell>\n\n <!-- Rows -->\n <ng-template #displayCell>\n <app-table-row [column]=\"column\" [row]=\"row\" [config]=\"config\" [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked(column.name, row)\" (columnClick)=\"columnClicked(column, row)\" (showBannerEvent)=\"showBanner($event)\">\n </app-table-row>\n </ng-template>\n\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"action\">\n <th mat-header-cell *matHeaderCellDef> Action </th>\n <td mat-cell *matCellDef=\"let row\" [ngStyle]=\"{width:false ? '20px' : actionsWidth}\">\n <div class=\"action-buttons-container\">\n\n <!-- Added: inline edit \u2014 while a row edits in place, its actions collapse to submit/cancel -->\n <ng-container *ngIf=\"isRowEditing(row); else rowActions\">\n <button mat-icon-button matTooltip=\"Save\" matTooltipPosition=\"above\" (click)=\"submitInlineEdit()\"><mat-icon class=\"inline-save\">check</mat-icon></button> <!-- Changed: dropped color=\"primary\" \u2014 the icon now carries a green save cue -->\n <button mat-icon-button matTooltip=\"Cancel\" matTooltipPosition=\"above\" (click)=\"cancelInlineEdit()\"><mat-icon class=\"inline-cancel\">close</mat-icon></button> <!-- Changed: red cancel cue -->\n </ng-container>\n\n <!-- Actions -->\n <ng-template #rowActions>\n <app-table-action\n [displayedButtons]=\"displayedButtons\" [config]=\"config\" [smallScreen]=\"smallScreen\" [row]=\"row\" (actionClick)=\"actionClicked($event.name, $event.row)\">\n </app-table-action>\n </ng-template>\n\n </div>\n </td>\n </ng-container>\n\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\" [ngClass]=\"{'make-gray': (config.greyOut && config.greyOut(row)) || row.pendingApproval, 'row-editing': isRowEditing(row)}\"></tr> <!-- Changed: row-editing flags the row that is open for inline edit -->\n </table>\n\n </div>\n\n <!-- Changed: Removed *ngIf condition to keep paginator always in DOM and maintain ViewChild reference -->\n <!-- Changed: Added CSS class binding to hide when no data instead of conditional rendering -->\n <!-- Changed: Legacy paginator only renders in non-paged mode (pagedMode is constant per instance, set before first render) -->\n <!-- Changed: hidePageSize on a phone. The \"Items per page\" label and its select cost a whole row of a\n narrow screen for a control almost nobody touches there \u2014 the default is what gets used. The range\n (\"1 \u2013 10 of 54\") and the arrows stay, which is the part that is actually navigated. Bound to\n smallScreen, the component's existing breakpoint (max-width 600px, live via BreakpointObserver), so\n the paginator agrees with how this table already decides what \"mobile\" means rather than\n introducing a third breakpoint. -->\n <mat-paginator *ngIf=\"!pagedMode\"\n #tablePaginator\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [hidePageSize]=\"smallScreen\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial'}\"\n showFirstLastButtons>\n </mat-paginator>\n\n <!-- Added: manual paginator for server-side paged mode \u2014 fully state-bound, never attached to MatTableDataSource. Always visible: when filtering it pages the in-memory filtered subset (length = filtered count); otherwise the server window (length = true total). No first/last jump (would force fetching the whole gap). -->\n <mat-paginator *ngIf=\"pagedMode\"\n [length]=\"filterActive ? filteredRows.length : serverTotal + overlayDelta\"\n [pageIndex]=\"pageIndex\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [hidePageSize]=\"smallScreen\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial'}\"\n (page)=\"onServerPage($event)\">\n </mat-paginator>\n\n </div>\n \n <!-- Added (Quiet Loading): the initial-load stage for the NON-table views.\n Without this, card/capsule/grouped showed literally nothing during a first load \u2014 the view component\n renders an empty dataSource and the \"No Data\" line is suppressed while the stage owns the space.\n The progress header is identical to the table's so the two feel like one feature; only the ghost\n furniture differs, because column-shaped rows are wrong in a card grid. -->\n <div *ngIf=\"loadingStage === 'initial' && config?.viewType && config?.viewType !== 'table'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div *ngIf=\"config?.viewType === 'capsule'\" class=\"tin-load-ghost-capsules\" aria-hidden=\"true\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let c of ghostCapsules\"></span>\n </div>\n\n <div *ngIf=\"config?.viewType === 'card'\" class=\"tin-load-ghost-cards\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-card\" *ngFor=\"let c of ghostCards\">\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-title\"></span>\n <span class=\"tin-skel tin-skel-text\"></span>\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-short\"></span>\n </div>\n </div>\n\n <div *ngIf=\"config?.viewType === 'grouped'\" class=\"tin-load-ghost-groups\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-group\" *ngFor=\"let g of ghostGroups\">\n <span class=\"tin-skel tin-load-ghost-group-head\"></span>\n <!-- Pills, not rows: a group card's body is a wrap of chips, so full-width bars promised a table\n and the stage did not resemble what replaced it. -->\n <div class=\"tin-load-ghost-group-items\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let r of ghostCards\"></span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Capsules -->\n <spa-capsules *ngIf=\"config?.viewType === 'capsule' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n (actionClick)=\"actionClicked($event.name, $event.row)\">\n </spa-capsules>\n\n\n <!-- Cards -->\n <spa-cards *ngIf=\"config?.viewType === 'card' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked($event.name, $event.row)\"\n (columnClick)=\"columnClicked($event.column, $event.row)\"\n (showBannerEvent)=\"showBanner($event)\">\n </spa-cards>\n\n <!-- Groups - Added: New grouped view type -->\n <!-- Changed: the grouped view no longer renders its own filter field. It used to sit on a row of its own\n beneath the buttons row, which left both rows half empty and, more importantly, was a SECOND filter with\n no refresh button. The standard header filter (which has refresh, like every other table) now drives it,\n with its text relayed in through filterText. -->\n <spa-groups *ngIf=\"config?.viewType === 'grouped' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [showOwnFilter]=\"false\"\n [filterText]=\"groupFilterText\"\n (actionClick)=\"actionClicked($event.name, $event.row, $event.group, $event.button)\">\n </spa-groups>\n\n\n <div class=\"tin-center\">\n <p *ngIf=\"dataSource?.length == 0 && loadingStage !== 'initial'\"><em>{{config.noDataMessage ?? 'No Data'}}</em></p> <!-- Changed (Quiet Loading): the stage owns the space until it completes, then hands straight over to this message \u2014 no \"No Data\" flashing underneath the ghost rows -->\n </div>\n\n </ng-container> <!-- Added: end section-collapse wrapper -->\n\n</ng-container>\n\n\n<ng-container *ngIf=\"!hasFormAccess\">\n <div class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n</ng-container>\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}.dlg-header-classic{padding-left:24px;padding-right:24px}.dlg-title-classic{font-size:20px;font-weight:500;margin-top:10px;margin-bottom:5px}.dialog-header-titles{display:flex;flex-direction:column;justify-content:center;min-width:0}.tin-dlg-head .dialog-header-titles label{margin:0}.tbl-section-header{display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:8px;border:1px solid rgba(0,0,0,.08);border-radius:10px;background:transparent;cursor:pointer;transition:border-color .15s}.tbl-section-header:hover{border-color:#90a4ae}.tbl-section-header.tbl-section-static{cursor:default}.tbl-section-icon{color:#546e7a}.tbl-section-title{font-size:14px;font-weight:600;color:#000000d1}.tbl-section-count{background:#e3f2fd;color:#1565c0;border-radius:12px;padding:2px 10px;font-size:12px}.tbl-section-chip{font-size:12px;font-weight:500;color:#0009}.tbl-section-spacer{margin-left:auto}.tbl-section-chevron{color:#90a4ae}.tbl-section-caption{margin:-4px 12px 10px;font-size:12.5px;line-height:1.45;color:#0000008c;max-width:82ch}.tbl-section-more{padding:0 12px 8px}.tbl-section-link{background:none;border:none;color:#1565c0;cursor:pointer;font-size:13px;padding:0}.tbl-section-link:hover{text-decoration:underline}.tbl-section-icon,.tbl-section-chevron,.tbl-section-count{flex:0 0 auto}.tbl-section-title{flex:1 1 auto;min-width:0}@media (max-width: 700px){.tbl-section-header{gap:8px;padding:10px}.tbl-section-chip,.tbl-section-btn-text.has-icon{display:none}.tbl-section-header button{min-width:0;padding:0 10px}}table.tin-no-col-headers tr.mat-mdc-header-row{display:none}\n"] }]
|
|
18811
|
+
args: [{ selector: 'spa-table', standalone: false, template: "\n<ng-container *ngIf=\"hasFormAccess && !sectionHidden\"> <!-- Changed: sectionConfig.hideWhenEmpty hides the whole table -->\n\n <!-- Added: collapsible flat section header (sectionConfig) \u2014 1px border, no elevation, whole row toggles -->\n <div class=\"tbl-section-header\" *ngIf=\"config.sectionConfig\" (click)=\"toggleSection()\" [attr.aria-expanded]=\"!sectionCollapsed\" [class.tbl-section-static]=\"config.sectionConfig.collapsible === false\" role=\"button\" tabindex=\"0\" (keydown.enter)=\"toggleSection()\">\n <mat-icon class=\"tbl-section-icon\" *ngIf=\"config.sectionConfig.icon\">{{ config.sectionConfig.icon }}</mat-icon>\n <span class=\"tbl-section-title\">{{ config.sectionConfig.title }}</span>\n <span class=\"tbl-section-count\" *ngIf=\"config.sectionConfig.showCount !== false\">{{ dataSource?.length || 0 }}</span>\n <span class=\"tbl-section-chip\" *ngFor=\"let chip of sectionChips()\" [style.color]=\"chip.color\">{{ chip.text }}</span>\n <span class=\"tbl-section-spacer\"></span>\n <!-- Changed: the label is wrapped so a NARROW screen can drop it and leave an icon-only button. Only a\n button that HAS an icon loses its text \u2014 otherwise it would collapse to a blank square. -->\n <button mat-stroked-button color=\"primary\" *ngFor=\"let btn of sectionButtons()\" (click)=\"sectionButtonClicked(btn, $event)\" [matTooltip]=\"btn.display || btn.name\"><mat-icon *ngIf=\"btn.icon?.name\">{{ btn.icon.name }}</mat-icon><span class=\"tbl-section-btn-text\" [class.has-icon]=\"!!btn.icon?.name\">{{ btn.display || btn.name }}</span></button>\n <mat-icon class=\"tbl-section-chevron\" *ngIf=\"config.sectionConfig.collapsible !== false\">{{ sectionCollapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </div>\n\n <!-- Added: the section's \"why this list exists\" line. It sits UNDER the title (a reason only makes sense once\n the list has been named) and only while the section is open \u2014 a shut section already has its own summary\n line, and a paragraph over the top of that is noise. -->\n <p class=\"tbl-section-caption\" *ngIf=\"config.sectionConfig?.caption && !sectionCollapsed\">{{ config.sectionConfig.caption }}</p>\n\n <!-- Added: collapsed section affordance \u2014 mirrors the Day Book \"Show the N\" pattern -->\n <div class=\"tbl-section-more\" *ngIf=\"config.sectionConfig && sectionCollapsed && (dataSource?.length || 0) > 0\">\n <button type=\"button\" class=\"tbl-section-link\" (click)=\"toggleSection()\">Show the {{ dataSource.length }}</button>\n </div>\n\n <ng-container *ngIf=\"!config.sectionConfig || !sectionCollapsed\"> <!-- Added: section collapse hides the table body -->\n\n <!-- Search -->\n <spa-search\n *ngIf=\"config.searchConfig\" [config]=\"config.searchConfig\" [smallScreen]=\"smallScreen\" [tableDataSource]=\"tableDataSource\" style=\"margin-bottom: 20px;\" (searchClick)=\"searchClicked($event)\">\n </spa-search>\n\n <!-- Header -->\n <!-- Changed: [isRealTime] binds the RESOLVED value, not the raw config. It was `config.realTime`, so the live\n indicator only ever appeared on a table that set the flag ITSELF \u2014 a table relying on the app-wide\n `tableDefaults.realTime` was genuinely subscribed to SignalR (setupRealTimeSubscriptions and\n realTimeRefreshOrFallback both resolve through effRealTime) and simply never showed the dot. That is\n how \"Trips and Loads have no real time\" got reported: a status light disagreeing with the system it\n reports on. effRealTime also gets the inverse right \u2014 realTime:true under a global false still lights,\n and realTime:false under a global true stays dark. -->\n <app-table-header\n [config]=\"config\" [data]=\"dataSource\" [tableDataSource]=\"tableDataSource\" [tileConfig]=\"config.tileConfig\" [tileData]=\"tileData\" [tileReload]=\"tileReload\" [lastSearch]=\"lastSearch\" [smallScreen]=\"smallScreen\"\n [showFilterButton]=\"showFilterButton\" [isRealTime]=\"effRealTime\" [isConnected]=\"isSignalRConnected\" [refreshing]=\"loadingStage === 'refresh'\"\n (createClick)=\"newModel()\" (customClick)=\"customModel($event,null)\"\n (refreshClick)=\"refreshClicked()\" (tileClick)=\"tileClicked($event)\" (tileUnClick)=\"tileUnClicked($event)\" (filterChange)=\"filterChanged($event)\">\n </app-table-header>\n\n <!-- Added (Quiet Loading D4): refresh with data already on screen \u2014 a 2px line flush under the header and the\n spinning refresh icon are the ONLY signals. Rows stay live, clickable and un-dimmed while they swap. -->\n <div *ngIf=\"loadingStage === 'refresh'\" class=\"tin-load-line\" aria-hidden=\"true\">\n <span class=\"tin-load-line-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n\n <!-- Added: paged-mode filter hint \u2014 the client filter only covers rows loaded so far.\n Changed: the condition moved into filterScopePartial so this banner and the empty-state message below\n read the SAME predicate. Stated independently they drifted: this banner said \"only the 20 loaded rows\n of 240\" while the message underneath offered \"all 240 rows\". This banner owns SCOPE (what was searched)\n and the remedy; the message below owns the RESULT; the paginator owns the COUNT. One fact each. -->\n <div *ngIf=\"filterScopePartial\" class=\"paged-filter-hint\">\n <mat-icon>info</mat-icon>\n <span>Filtering only the {{loadedRows.length}} loaded rows of {{serverTotal}}. {{ config.searchConfig ? 'Use Search for complete results.' : 'Refine with search for complete results.' }}</span>\n </div>\n\n\n <!-- Table -->\n <div *ngIf=\"!config.viewType || config?.viewType === 'table'\">\n\n <p *ngIf=\"!config\"><em>Configure Table</em></p>\n <p *ngIf=\"!dataSource && !loadingStage && !loadFailed\"><em>Loading...</em></p> <!-- Changed (Quiet Loading): the bare text is replaced by the stage below while a quiet load is on screen --><!-- Changed: ...and a FAILED load is not a running one \u2014 without the loadFailed guard this line sat there forever once the error dialog was dismissed -->\n\n <!-- Added (Quiet Loading D3): first load, nothing on screen yet. The progress module is the hero (eased bar +\n counting percentage + caption) and the ghost rows hold the exact space the real rows will fill, so the\n table does not jump when data lands. Only ever rendered when quiet loading is on. -->\n <div *ngIf=\"loadingStage === 'initial'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div class=\"tin-load-ghosts\">\n <div class=\"tin-load-ghost-row\" *ngFor=\"let r of ghostRows\">\n <div class=\"tin-load-ghost-cell\" *ngFor=\"let c of ghostColumns; let i = index\">\n <span class=\"tin-skel\" [style.width.%]=\"ghostWidth(i, c)\" [style.animation-delay.ms]=\"r * 120\"></span> <!-- staggered sweep: each row starts 120ms after the one above -->\n </div>\n </div>\n </div>\n\n </div>\n\n <div *ngIf=\"dataSource && loadingStage !== 'initial' && (!smallScreen || (smallScreen && dataSource?.length > 0))\" [class.tin-load-in]=\"effQuietLoading\"> <!-- Changed (Quiet Loading): the empty header-only table is suppressed while the initial stage stands in for it, and the real rows fade in where the ghosts were (D3) -->\n\n <table mat-table [dataSource]=\"tableDataSource\" [trackBy]=\"trackByRow\" [ngClass]=\"elevation\" [class.tin-no-col-headers]=\"config.hideColumnHeaders\"> <!-- Changed: optional column-header suppression -->\n\n <ng-container *ngFor=\"let column of config.columns\" [matColumnDef]=\"column.name\">\n <th mat-header-cell *matHeaderCellDef >{{ column.alias ?? column.name | camelToWords }}</th>\n <td mat-cell *matCellDef=\"let row;\" class=\"right-padding\" >\n\n <!-- Added: inline edit \u2014 editable cells swap to their form-field editor while the row is in edit mode -->\n <app-inline-cell *ngIf=\"isRowEditing(row) && getInlineField(column); else displayCell\" [field]=\"getInlineField(column)\" [data]=\"editingModel\"></app-inline-cell>\n\n <!-- Rows -->\n <ng-template #displayCell>\n <app-table-row [column]=\"column\" [row]=\"row\" [config]=\"config\" [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked(column.name, row)\" (columnClick)=\"columnClicked(column, row)\" (showBannerEvent)=\"showBanner($event)\">\n </app-table-row>\n </ng-template>\n\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"action\">\n <th mat-header-cell *matHeaderCellDef> Action </th>\n <td mat-cell *matCellDef=\"let row\" [ngStyle]=\"{width:false ? '20px' : actionsWidth}\">\n <div class=\"action-buttons-container\">\n\n <!-- Added: inline edit \u2014 while a row edits in place, its actions collapse to submit/cancel -->\n <ng-container *ngIf=\"isRowEditing(row); else rowActions\">\n <button mat-icon-button matTooltip=\"Save\" matTooltipPosition=\"above\" (click)=\"submitInlineEdit()\"><mat-icon class=\"inline-save\">check</mat-icon></button> <!-- Changed: dropped color=\"primary\" \u2014 the icon now carries a green save cue -->\n <button mat-icon-button matTooltip=\"Cancel\" matTooltipPosition=\"above\" (click)=\"cancelInlineEdit()\"><mat-icon class=\"inline-cancel\">close</mat-icon></button> <!-- Changed: red cancel cue -->\n </ng-container>\n\n <!-- Actions -->\n <ng-template #rowActions>\n <app-table-action\n [displayedButtons]=\"displayedButtons\" [config]=\"config\" [smallScreen]=\"smallScreen\" [row]=\"row\" (actionClick)=\"actionClicked($event.name, $event.row)\">\n </app-table-action>\n </ng-template>\n\n </div>\n </td>\n </ng-container>\n\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\" [ngClass]=\"{'make-gray': (config.greyOut && config.greyOut(row)) || row.pendingApproval, 'row-editing': isRowEditing(row)}\"></tr> <!-- Changed: row-editing flags the row that is open for inline edit -->\n </table>\n\n </div>\n\n <!-- Changed: Removed *ngIf condition to keep paginator always in DOM and maintain ViewChild reference -->\n <!-- Changed: Added CSS class binding to hide when no data instead of conditional rendering -->\n <!-- Changed: Legacy paginator only renders in non-paged mode (pagedMode is constant per instance, set before first render) -->\n <!-- Changed: hidePageSize on a phone. The \"Items per page\" label and its select cost a whole row of a\n narrow screen for a control almost nobody touches there \u2014 the default is what gets used. The range\n (\"1 \u2013 10 of 54\") and the arrows stay, which is the part that is actually navigated. Bound to\n smallScreen, the component's existing breakpoint (max-width 600px, live via BreakpointObserver), so\n the paginator agrees with how this table already decides what \"mobile\" means rather than\n introducing a third breakpoint. -->\n <mat-paginator *ngIf=\"!pagedMode\"\n #tablePaginator\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [hidePageSize]=\"smallScreen\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial' || singlePagePaginatorHidden}\"\n showFirstLastButtons>\n </mat-paginator>\n\n <!-- Added: manual paginator for server-side paged mode \u2014 fully state-bound, never attached to MatTableDataSource. Always visible: when filtering it pages the in-memory filtered subset (length = filtered count); otherwise the server window (length = true total). No first/last jump (would force fetching the whole gap). -->\n <mat-paginator *ngIf=\"pagedMode\"\n [length]=\"filterActive ? filteredRows.length : serverTotal + overlayDelta\"\n [pageIndex]=\"pageIndex\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [hidePageSize]=\"smallScreen\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial' || singlePagePaginatorHidden}\"\n (page)=\"onServerPage($event)\">\n </mat-paginator>\n\n </div>\n \n <!-- Added (Quiet Loading): the initial-load stage for the NON-table views.\n Without this, card/capsule/grouped showed literally nothing during a first load \u2014 the view component\n renders an empty dataSource and the \"No Data\" line is suppressed while the stage owns the space.\n The progress header is identical to the table's so the two feel like one feature; only the ghost\n furniture differs, because column-shaped rows are wrong in a card grid. -->\n <div *ngIf=\"loadingStage === 'initial' && config?.viewType && config?.viewType !== 'table'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div *ngIf=\"config?.viewType === 'capsule'\" class=\"tin-load-ghost-capsules\" aria-hidden=\"true\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let c of ghostCapsules\"></span>\n </div>\n\n <div *ngIf=\"config?.viewType === 'card'\" class=\"tin-load-ghost-cards\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-card\" *ngFor=\"let c of ghostCards\">\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-title\"></span>\n <span class=\"tin-skel tin-skel-text\"></span>\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-short\"></span>\n </div>\n </div>\n\n <div *ngIf=\"config?.viewType === 'grouped'\" class=\"tin-load-ghost-groups\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-group\" *ngFor=\"let g of ghostGroups\">\n <span class=\"tin-skel tin-load-ghost-group-head\"></span>\n <!-- Pills, not rows: a group card's body is a wrap of chips, so full-width bars promised a table\n and the stage did not resemble what replaced it. -->\n <div class=\"tin-load-ghost-group-items\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let r of ghostCards\"></span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Capsules -->\n <spa-capsules *ngIf=\"config?.viewType === 'capsule' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n (actionClick)=\"actionClicked($event.name, $event.row)\">\n </spa-capsules>\n\n\n <!-- Cards -->\n <spa-cards *ngIf=\"config?.viewType === 'card' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked($event.name, $event.row)\"\n (columnClick)=\"columnClicked($event.column, $event.row)\"\n (showBannerEvent)=\"showBanner($event)\">\n </spa-cards>\n\n <!-- Groups - Added: New grouped view type -->\n <!-- Changed: the grouped view no longer renders its own filter field. It used to sit on a row of its own\n beneath the buttons row, which left both rows half empty and, more importantly, was a SECOND filter with\n no refresh button. The standard header filter (which has refresh, like every other table) now drives it,\n with its text relayed in through filterText. -->\n <spa-groups *ngIf=\"config?.viewType === 'grouped' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [showOwnFilter]=\"false\"\n [filterText]=\"groupFilterText\"\n (actionClick)=\"actionClicked($event.name, $event.row, $event.group, $event.button)\">\n </spa-groups>\n\n\n <div class=\"tin-center\">\n <p *ngIf=\"dataSource?.length == 0 && loadingStage !== 'initial' && !loadFailed\"><em>{{config.noDataMessage ?? 'No Data'}}</em></p> <!-- Changed (Quiet Loading): the stage owns the space until it completes, then hands straight over to this message \u2014 no \"No Data\" flashing underneath the ghost rows --><!-- Changed: ...and NOT when the load failed. A failed read leaves dataSource as an empty array, so this line confidently reported \"No Data\" \u2014 an assertion about the data \u2014 when the truth was that we never got any. That state now has its own sentence below. -->\n\n <!-- Added: the load-failed state. \"There is nothing here\" and \"we could not find out\" are different facts and\n this grid stated the first for both. Gated on having no rows, so a failed REFRESH leaves the rows already\n on screen alone \u2014 the error dialog covers that case and an empty state under live rows would be a lie. -->\n <div *ngIf=\"!(dataSource?.length > 0) && !loadingStage && loadFailed\" class=\"tin-filter-empty\" role=\"status\">\n <p class=\"tin-filter-empty-title\"><em>Couldn\u2019t load {{stageEntityName}}</em></p>\n <p class=\"tin-filter-empty-hint\">Check your connection, then use the refresh button to try again.</p>\n </div>\n\n <!-- Added: the OTHER empty state. The line above means \"there is nothing here yet\"; this one means \"nothing\n matches what you typed\", and until now it rendered nothing at all \u2014 a header, a \"0 of 0\" paginator and\n no reason. filteredToNothing is false whenever dataSource is empty, so the two can never both show and\n the line above is reached on exactly the rows it always was. role=\"status\" announces the change to a\n screen reader, which otherwise gets silence when the rows disappear. -->\n <!-- Changed: ONE message became TWO, because \"no matches\" means two different things and only one of them\n was ever true. In paged mode the client filter runs over the loaded WINDOW and issues no fetch, so with\n a partial window \"No matches for X\" overstates what was searched and \"see all 240 rows\" implies the\n filter covered 240 when it covered 20 \u2014 while the banner at the top of the grid was simultaneously\n saying so. The split is driven by filterScopePartial, the same getter that banner binds to, so the two\n cannot contradict each other. Neither variant restates a COUNT the paginator or the banner already owns. -->\n\n <!-- COMPLETE scope: the filter saw every row there is (any unpaged grid, or a paged one holding the full\n set \u2014 search/POST mode and the no-total degrade both land here). The total is honest, so it is offered. -->\n <div *ngIf=\"filteredToNothing && !filterScopePartial\" class=\"tin-filter-empty\" role=\"status\">\n <p class=\"tin-filter-empty-title\"><em>{{filterEmptyTitle}}</em></p>\n <p class=\"tin-filter-empty-hint\">Clear the filter to see all {{filteredTotal}} {{filteredRowNoun}}.</p>\n </div>\n\n <!-- PARTIAL scope: the filter saw only the rows loaded so far. The headline says exactly that, and the hint\n stops at the way back \u2014 the banner above already carries both the counts and the \"use search\" remedy,\n so repeating either here would be the contradiction this split exists to remove. -->\n <div *ngIf=\"filteredToNothing && filterScopePartial\" class=\"tin-filter-empty\" role=\"status\">\n <p class=\"tin-filter-empty-title\"><em>{{filterEmptyTitlePartial}}</em></p>\n <p class=\"tin-filter-empty-hint\">Clear the filter to keep browsing.</p>\n </div>\n </div>\n\n </ng-container> <!-- Added: end section-collapse wrapper -->\n\n</ng-container>\n\n\n<ng-container *ngIf=\"!hasFormAccess\">\n <div class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n</ng-container>\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}.dlg-header-classic{padding-left:24px;padding-right:24px}.dlg-title-classic{font-size:20px;font-weight:500;margin-top:10px;margin-bottom:5px}.dialog-header-titles{display:flex;flex-direction:column;justify-content:center;min-width:0}.tin-dlg-head .dialog-header-titles label{margin:0}.tbl-section-header{display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:8px;border:1px solid rgba(0,0,0,.08);border-radius:10px;background:transparent;cursor:pointer;transition:border-color .15s}.tbl-section-header:hover{border-color:#90a4ae}.tbl-section-header.tbl-section-static{cursor:default}.tbl-section-icon{color:#546e7a}.tbl-section-title{font-size:14px;font-weight:600;color:#000000d1}.tbl-section-count{background:#e3f2fd;color:#1565c0;border-radius:12px;padding:2px 10px;font-size:12px}.tbl-section-chip{font-size:12px;font-weight:500;color:#0009}.tbl-section-spacer{margin-left:auto}.tbl-section-chevron{color:#90a4ae}.tbl-section-caption{margin:-4px 12px 10px;font-size:12.5px;line-height:1.45;color:#0000008c;max-width:82ch}.tbl-section-more{padding:0 12px 8px}.tbl-section-link{background:none;border:none;color:#1565c0;cursor:pointer;font-size:13px;padding:0}.tbl-section-link:hover{text-decoration:underline}.tbl-section-icon,.tbl-section-chevron,.tbl-section-count{flex:0 0 auto}.tbl-section-title{flex:1 1 auto;min-width:0}@media (max-width: 700px){.tbl-section-header{gap:8px;padding:10px}.tbl-section-chip,.tbl-section-btn-text.has-icon{display:none}.tbl-section-header button{min-width:0;padding:0 10px}}table.tin-no-col-headers tr.mat-mdc-header-row{display:none}.tin-filter-empty{text-align:center;padding:24px 12px}.tin-filter-empty-title{margin:0}.tin-filter-empty-hint{margin:6px 0 0;font-size:.85em;opacity:.7;overflow-wrap:anywhere}\n"] }]
|
|
18445
18812
|
}], ctorParameters: () => [{ type: DataServiceLib }, { type: MessageService }, { type: i1$4.BreakpointObserver }, { type: i4.MatDialog }, { type: ButtonService }, { type: DialogService }, { type: TableConfigService }, { type: ConditionService }, { type: AuthService }, { type: SignalRService }, { type: OfflineService }, { type: ApiErrorService }, { type: undefined, decorators: [{
|
|
18446
18813
|
type: Optional
|
|
18447
18814
|
}, {
|
|
@@ -18502,6 +18869,7 @@ class DayBookComponent {
|
|
|
18502
18869
|
this.messageService = inject(MessageService); // Added: page actions confirm and toast like every other button in the suite
|
|
18503
18870
|
this.apiErrorService = inject(ApiErrorService);
|
|
18504
18871
|
this.buttonService = inject(ButtonService);
|
|
18872
|
+
this.zone = inject(NgZone, { optional: true }); // Added: optional so a bare test harness instantiating this class directly still constructs
|
|
18505
18873
|
this.book = null;
|
|
18506
18874
|
this.loading = false; // the re-entrancy guard — deliberately NOT bound in the template (see showLoading)
|
|
18507
18875
|
// Added (NG0100): what the template actually binds. `loading` flips inside load(), and load() can be reached
|
|
@@ -18582,7 +18950,10 @@ class DayBookComponent {
|
|
|
18582
18950
|
// pure config silently lost its Refresh. These come from config instead, and the ng-content slot is still
|
|
18583
18951
|
// there beside them for anyone who does host the page themselves.
|
|
18584
18952
|
buildActions() {
|
|
18585
|
-
|
|
18953
|
+
// Changed: also honour the button's opt-in capability. Day Book actions carry NO formConfig, so nothing in
|
|
18954
|
+
// the library could ever gate them — hasCapability is the only gate that reaches here. Buttons that name no
|
|
18955
|
+
// capability are unaffected (hasCapability returns true); only isItemVisible ran before and it still runs.
|
|
18956
|
+
this.pageActions = (this.config?.actions || []).filter(b => Core.isItemVisible(b, this.book) && this.buttonService.hasCapability(b));
|
|
18586
18957
|
}
|
|
18587
18958
|
pageActionClicked(button) {
|
|
18588
18959
|
if (button.onClick) {
|
|
@@ -18714,6 +19085,13 @@ class DayBookComponent {
|
|
|
18714
19085
|
const cfg = { ...section.tableConfig };
|
|
18715
19086
|
cfg.sectionConfig = section.tableConfig.sectionConfig ? { ...section.tableConfig.sectionConfig, collapsed: section.collapsed !== false } : undefined;
|
|
18716
19087
|
cfg.elevation = cfg.elevation ?? 'none'; // flat tables on the section cards — Day Book design language
|
|
19088
|
+
// Added (phone density): a Day Book stacks 9-16 of these cards, and on a phone each one was spending
|
|
19089
|
+
// 56px on a pager that read "1 – 4 of 4" — the largest single item in the card, navigating nothing.
|
|
19090
|
+
// The count it reports is already on the section header's own badge, so nothing is lost. This is a Day
|
|
19091
|
+
// Book decision made ONCE in the library rather than repeated in five app configs, exactly as the
|
|
19092
|
+
// carousel stage strip is; an app that wants its pagers back still sets the flag false on its own
|
|
19093
|
+
// tableConfig. Desktop is untouched — the flag only ever acts below 600px.
|
|
19094
|
+
cfg.hideSinglePagePaginator = cfg.hideSinglePagePaginator ?? true;
|
|
18717
19095
|
if (cfg.sectionConfig) {
|
|
18718
19096
|
cfg.sectionConfig.hideWhenEmpty = cfg.sectionConfig.hideWhenEmpty !== false; // sections disappear when empty by default
|
|
18719
19097
|
// Added: the section's "why this list exists" line rides INSIDE the table's own header, under the
|
|
@@ -18792,20 +19170,66 @@ class DayBookComponent {
|
|
|
18792
19170
|
this.groupState[group.lane.key] = group.collapsed;
|
|
18793
19171
|
localStorage.setItem('tin-daybook-groups', JSON.stringify(this.groupState));
|
|
18794
19172
|
}
|
|
18795
|
-
// Un-collapse BEFORE scrolling (piglet's fix, applied suite-wide) — a click must never land on a bare header
|
|
19173
|
+
// Un-collapse BEFORE scrolling (piglet's fix, applied suite-wide) — a click must never land on a bare header.
|
|
19174
|
+
//
|
|
19175
|
+
// Changed (owner, 2026-08-09): a chip scrolled to its section and left it SHUT, so the operator had to click
|
|
19176
|
+
// again to see the list he had just asked for. There are THREE doors between a chip and its rows, not one:
|
|
19177
|
+
//
|
|
19178
|
+
// chip → lane GROUP (.db-gbody is *ngIf="!g.collapsed") → SECTION (spa-table's own sectionCollapsed)
|
|
19179
|
+
//
|
|
19180
|
+
// and the previous code only really cleared the third. It opened the group by flipping `group.collapsed`, then
|
|
19181
|
+
// in the SAME synchronous tick asked `this.tables` for the section's table — but Angular had not re-rendered
|
|
19182
|
+
// yet, so that table did not exist in the QueryList and the expand silently no-opped. Which is exactly the
|
|
19183
|
+
// reported symptom, and it applied to every chip: chips are only rendered on a COLLAPSED group, so the broken
|
|
19184
|
+
// path was the only path a chip could ever take.
|
|
19185
|
+
//
|
|
19186
|
+
// The fix does not race the renderer. Level 2 seeds the section's own cloned config, so a table that has not
|
|
19187
|
+
// been created yet is BORN open (TableComponent.ngOnInit seeds sectionCollapsed off exactly this field), and
|
|
19188
|
+
// only expands a live instance if there already is one. Nothing here has to guess when Angular will catch up.
|
|
18796
19189
|
goTo(key) {
|
|
18797
19190
|
const target = this.sectionConfigs.find(x => x.section.key === key);
|
|
18798
19191
|
if (target) {
|
|
18799
|
-
//
|
|
18800
|
-
// must open the group before it opens the section, or the scroll lands on nothing that is in the DOM.
|
|
19192
|
+
// Level 1 — the lane group. Nothing inside a shut group is in the DOM at all.
|
|
18801
19193
|
const group = this.visibleGroups.find(g => g.lane.key === target.section.lane);
|
|
18802
|
-
if (group
|
|
18803
|
-
this.toggleGroup(group);
|
|
18804
|
-
|
|
18805
|
-
if (
|
|
18806
|
-
|
|
18807
|
-
|
|
18808
|
-
|
|
19194
|
+
if (group?.collapsed)
|
|
19195
|
+
this.toggleGroup(group); // expand-only: an already-open group is left alone, and so is every OTHER group — this is not an accordion
|
|
19196
|
+
// Level 2 — the section. Seeding the config covers the not-yet-created table; expandSection covers the live one.
|
|
19197
|
+
if (target.cfg?.sectionConfig)
|
|
19198
|
+
target.cfg.sectionConfig.collapsed = false; // cfg is this section's own clone (buildSections), never the app's shared singleton
|
|
19199
|
+
target.collapsed = false;
|
|
19200
|
+
this.tables?.find(t => t.config === target.cfg)?.expandSection(); // expand, never toggle — a toggle would shut a section that was already open
|
|
19201
|
+
}
|
|
19202
|
+
// Level 3 — scroll, but only once the two expansions above have actually changed the layout.
|
|
19203
|
+
this.scrollWhenSettled('sec-' + key);
|
|
19204
|
+
}
|
|
19205
|
+
// Added: the scroll has to be the LAST thing that happens, and a fixed delay cannot promise that. A section
|
|
19206
|
+
// that is still collapsed when we scroll is ~110px of bare chrome: we land on that position, the section then
|
|
19207
|
+
// expands to several hundred px and grows UNDER the viewport, and the operator ends up parked below the list
|
|
19208
|
+
// he asked for — looking, from his side, exactly like the bug we are fixing. So instead of guessing a delay we
|
|
19209
|
+
// watch the target until its height stops changing (the same measurement two frames running), which covers the
|
|
19210
|
+
// group render, the table's own creation and the paginator re-attach without knowing anything about any of
|
|
19211
|
+
// them. The frame cap stops a section whose height never settles (a live-updating list) from never scrolling.
|
|
19212
|
+
// Runs outside Angular: this is a poll, and every frame of it would otherwise drag a change-detection pass
|
|
19213
|
+
// behind it for no reason — the house rule about not putting work on the CD path applies to timers too.
|
|
19214
|
+
scrollWhenSettled(id) {
|
|
19215
|
+
const smooth = !window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches; // a jump, not a glide, when the operator has asked for less motion
|
|
19216
|
+
const run = () => {
|
|
19217
|
+
let previous = -1;
|
|
19218
|
+
let frames = 0;
|
|
19219
|
+
const step = () => {
|
|
19220
|
+
const element = document.getElementById(id);
|
|
19221
|
+
const height = element ? element.getBoundingClientRect().height : -1;
|
|
19222
|
+
if ((element && height === previous) || frames >= 20) {
|
|
19223
|
+
element?.scrollIntoView({ behavior: smooth ? 'smooth' : 'auto', block: 'start' });
|
|
19224
|
+
return;
|
|
19225
|
+
}
|
|
19226
|
+
previous = height;
|
|
19227
|
+
frames++;
|
|
19228
|
+
requestAnimationFrame(step);
|
|
19229
|
+
};
|
|
19230
|
+
requestAnimationFrame(step);
|
|
19231
|
+
};
|
|
19232
|
+
this.zone ? this.zone.runOutsideAngular(run) : run();
|
|
18809
19233
|
}
|
|
18810
19234
|
//---------- Header facts ----------
|
|
18811
19235
|
get booksFrom() {
|
|
@@ -18827,11 +19251,11 @@ class DayBookComponent {
|
|
|
18827
19251
|
return stages.length > 0 && stages.every(s => !this.stageCount(s));
|
|
18828
19252
|
}
|
|
18829
19253
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DayBookComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
18830
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: DayBookComponent, isStandalone: false, selector: "spa-day-book", inputs: { dayBookConfig: "dayBookConfig" }, viewQueries: [{ propertyName: "tables", predicate: TableComponent, descendants: true }], ngImport: i0, template: "<div class=\"db-page\" *ngIf=\"config\">\n\n <!-- Header: title, subtitle, books-from caption, owed total, app-supplied actions -->\n <div class=\"db-head\">\n <div class=\"db-head-text\">\n <h1>{{ title }}</h1>\n <p class=\"db-sub\" *ngIf=\"config.subtitle\">{{ config.subtitle }}</p>\n <span class=\"db-caption\" *ngIf=\"booksFrom\">books from {{ booksFrom | date: 'mediumDate' }}</span>\n </div>\n <span class=\"db-owed\" *ngIf=\"owed !== null\">Owed to us: <b>{{ owed | currency }}</b></span>\n <!-- The page's own actions (refresh, an escape hatch to a full screen). Changed: they now come from\n config.actions, because an app with no component of its own has nothing to project with \u2014 the\n ng-content slot stays beside them for anyone who does host the page themselves. -->\n <!-- Changed: an action WITH an icon renders icon-only (owner, 2026-08-08 \u2014 \"the refresh and all trips\n buttons can just be icons with no text\"). The label is not lost, it moves to the tooltip and to\n aria-label, so the control still announces itself to a screen reader and on hover. An action with\n no icon keeps its text button, because an unlabelled blank button would be unusable. -->\n <div class=\"db-head-actions\">\n <ng-container *ngFor=\"let a of pageActions\">\n <button mat-icon-button color=\"primary\" type=\"button\" *ngIf=\"a.icon?.name; else textAction\" (click)=\"pageActionClicked(a)\" [matTooltip]=\"a.tip || a.display || a.name\" [attr.aria-label]=\"a.display || a.name\">\n <mat-icon>{{ a.icon.name }}</mat-icon>\n </button>\n <ng-template #textAction>\n <button mat-stroked-button color=\"primary\" type=\"button\" (click)=\"pageActionClicked(a)\" [title]=\"a.tip || ''\">{{ a.display || a.name }}</button>\n </ng-template>\n </ng-container>\n <ng-content select=\"[dayBookActions]\"></ng-content>\n </div>\n </div>\n\n <!-- Lane pills -->\n <div class=\"db-lanes\" *ngIf=\"lanes.length\">\n <button type=\"button\" class=\"db-lane\" *ngFor=\"let l of lanes\" [class.active]=\"lane === l.key\" (click)=\"setLane(l.key)\">\n <mat-icon *ngIf=\"l.icon\">{{ l.icon }}</mat-icon>{{ l.label }}\n </button>\n </div>\n\n <!-- Added: the library owns the loading state \u2014 first load only, a refresh keeps the page on screen.\n Changed: binds showLoading, the copy that only ever moves between change-detection passes (NG0100) -->\n <div class=\"db-loading\" *ngIf=\"showLoading && !book\">{{ config.loadingMessage || 'Loading\u2026' }}</div>\n\n <ng-container *ngIf=\"book\">\n\n <!-- The stage row. Changed: always spa-tiles \u2014 zero-count tiles hide -->\n <spa-tiles *ngIf=\"tileConfig\" [config]=\"tileConfig\" [data]=\"tileData\" (tileClick)=\"onTileClick($event)\"></spa-tiles>\n\n <!-- All clear -->\n <div class=\"db-clear\" *ngIf=\"allClear\">\n <mat-icon>task_alt</mat-icon>\n <span>{{ config.allClearMessage || 'Nothing outstanding \u2014 all caught up.' }}</span>\n </div>\n\n <!-- Sections. A book carries 9-16 of these cards and, on 'Everything', all of them used to stack in one\n column \u2014 so they are grouped into one collapsible lane group each, with the first open and the rest\n showing only their identity: label, count, money and a chip per list. Sections that declare no lane\n sit above the groups, and with grouping off (one lane, a picked lane, or groupLanes:false) the page\n renders exactly as it did before. -->\n <ng-container *ngFor=\"let s of ungroupedSections\">\n <ng-container *ngTemplateOutlet=\"sectionCard; context: s.outletContext\"></ng-container>\n </ng-container>\n\n <section class=\"db-group\" *ngFor=\"let g of visibleGroups\" [class.closed]=\"g.collapsed\">\n <button type=\"button\" class=\"db-ghead\" (click)=\"toggleGroup(g)\" [attr.aria-expanded]=\"!g.collapsed\">\n <mat-icon *ngIf=\"g.lane.icon\">{{ g.lane.icon }}</mat-icon>\n <span class=\"g-label\">{{ g.lane.label }}</span>\n <span class=\"db-count\">{{ g.count }}</span>\n <span class=\"g-value\" *ngIf=\"g.value\">{{ g.value | currency: undefined : 'symbol' : '1.0-0' }}</span>\n <span class=\"g-lists\">{{ g.sections.length }} {{ g.sections.length === 1 ? 'list' : 'lists' }}</span>\n <mat-icon class=\"g-chev\">{{ g.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n <!-- Collapsed, the group still says what it is about \u2014 one chip per non-empty list, and a chip is a way\n in: it opens the group and scrolls to that section, the same path a stage click takes. -->\n <div class=\"db-chips\" *ngIf=\"g.collapsed\">\n <button type=\"button\" class=\"db-chip\" *ngFor=\"let c of g.chips\" (click)=\"goTo(c.key)\">{{ c.label }}<span>{{ c.count }}</span></button>\n <span class=\"db-chip-none\" *ngIf=\"!g.chips.length\">Nothing outstanding here</span>\n </div>\n <div class=\"db-gbody\" *ngIf=\"!g.collapsed\">\n <ng-container *ngFor=\"let s of g.sections\">\n <ng-container *ngTemplateOutlet=\"sectionCard; context: s.outletContext\"></ng-container>\n </ng-container>\n </div>\n </section>\n\n <!-- One section card, drawn from two places (flat above, and inside a lane group). Changed: ONE rendering\n path \u2014 a section is a spa-table fed local rows off the book object. The table draws its own section\n header (icon, title, count, chips, buttons, caption, chevron); (actionSuccess) reloads the book so a\n row that has just been actioned leaves the list instead of sitting there looking undone \u2014 the section\n has no loadAction of its own to refresh from. -->\n <ng-template #sectionCard let-s>\n <mat-card class=\"db-section\" [id]=\"'sec-' + s.section.key\" [class.alert]=\"s.section.tone === 'alert'\" [class.quiet]=\"s.section.tone === 'quiet'\">\n <!-- the \"why this list exists\" line rides inside the table's own header, under the title. With no\n section header there is nothing to ride, so the card carries it instead. -->\n <p class=\"db-why\" *ngIf=\"s.caption && !s.cfg?.sectionConfig\">{{ s.caption }}</p>\n <spa-table [config]=\"s.cfg\" [data]=\"s.rows\" (actionSuccess)=\"load()\"></spa-table>\n </mat-card>\n </ng-template>\n\n </ng-container>\n\n</div>\n\n<!-- Graceful empty state (feature not configured) -->\n<div class=\"db-empty\" *ngIf=\"!config\">\n <mat-icon>checklist</mat-icon>\n <p>The Day Book is not configured for this app.</p>\n</div>\n", styles: [":host{--db-accent: #1565c0;--db-accent-soft: #e3f2fd;--db-alert: #e53935;--db-alert-text: #c62828;--db-quiet: #b0bec5}.db-page{max-width:1200px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.db-head{display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap}.db-head-text h1{margin:0;font-size:24px}.db-head-text .db-sub{margin:4px 0 0;font-size:14px;color:#0000008c}.db-caption{font-size:12px;color:#0000008c}.db-owed{font-size:13px;color:#000000b3;margin-left:auto}.db-head-actions{display:flex;gap:8px;margin-left:auto}.db-lanes{display:flex;gap:6px;flex-wrap:wrap;align-items:center}.db-lane{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(0,0,0,.12);border-radius:16px;background:transparent;padding:4px 14px;font:inherit;font-size:13px;cursor:pointer;transition:border-color .15s,background .15s}.db-lane:hover{border-color:#90a4ae}.db-lane.active{border-color:var(--db-accent);background:var(--db-accent-soft);color:var(--db-accent);font-weight:500}.db-lane mat-icon{font-size:17px;width:17px;height:17px}@media (max-width: 700px){.db-head{gap:6px 10px;align-items:center}.db-head-text{flex:1 1 auto;min-width:0;order:1}.db-head-text h1{font-size:20px}.db-head-text .db-sub{display:none}.db-caption{font-size:11px}.db-head-actions{order:2;margin-left:auto;gap:2px}.db-owed{order:3;flex:1 0 100%;margin-left:0;font-size:13px}.db-lanes{flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}.db-lanes::-webkit-scrollbar{display:none}.db-lane{flex:0 0 auto}}.db-loading{padding:40px;text-align:center;color:#00000080}.db-clear{display:flex;align-items:center;gap:10px;border:1px solid #c8e6c9;background:#f6fbf6;border-radius:10px;padding:14px 16px;color:#2e7d32}.db-section{padding:12px 16px;scroll-margin-top:12px;border-left:4px solid transparent}.db-section.alert{border-left-color:var(--db-alert)}.db-section.quiet{border-left-color:var(--db-quiet)}.db-count{background:#00000012;border-radius:11px;padding:1px 9px;font-size:13px;font-weight:600;font-variant-numeric:tabular-nums}.db-why{margin:0 0 10px;color:#0000008c;font-size:13px;max-width:82ch}.db-group{display:flex;flex-direction:column;gap:12px}.db-group.closed{gap:0}.db-ghead{display:flex;align-items:center;gap:10px;width:100%;border:1px solid #e0e0e0;border-radius:6px;background:#fafafa;padding:11px 14px;font:inherit;cursor:pointer;text-align:left;transition:background .15s,border-color .15s}.db-ghead:hover{background:#f2f5f7;border-color:#cfd8dc}.db-group.closed .db-ghead{border-bottom-left-radius:0;border-bottom-right-radius:0}.db-ghead>mat-icon{font-size:20px;width:20px;height:20px;color:var(--db-accent)}.db-ghead .g-label{font-size:15px;font-weight:600;letter-spacing:.2px}.db-ghead .g-value{font-size:13px;font-weight:600;color:#000000b3;font-variant-numeric:tabular-nums}.db-ghead .g-lists{font-size:12px;color:#00000073;margin-left:auto}.db-ghead .g-chev{font-size:22px;width:22px;height:22px;color:#00000073}.db-chips{display:flex;flex-wrap:wrap;gap:6px;border:1px solid #e0e0e0;border-top:0;border-radius:0 0 6px 6px;background:#fff;padding:9px 14px 11px}.db-chip{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(0,0,0,.12);border-radius:14px;background:transparent;padding:2px 6px 2px 11px;font:inherit;font-size:12.5px;color:#000000b3;cursor:pointer}.db-chip:hover{border-color:var(--db-accent);color:var(--db-accent)}.db-chip span{background:#00000012;border-radius:10px;padding:0 7px;font-weight:600;font-variant-numeric:tabular-nums}.db-chip-none{font-size:12.5px;color:#00000073}.db-gbody{display:flex;flex-direction:column;gap:12px}.db-empty{padding:48px;text-align:center;color:#00000080}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: TilesComponent, selector: "spa-tiles", inputs: ["config", "lastSearch", "data", "reload"], outputs: ["tileActionSelected", "tileClick", "tileUnClick"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }, { kind: "pipe", type: i1$2.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }] }); }
|
|
19254
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: DayBookComponent, isStandalone: false, selector: "spa-day-book", inputs: { dayBookConfig: "dayBookConfig" }, viewQueries: [{ propertyName: "tables", predicate: TableComponent, descendants: true }], ngImport: i0, template: "<div class=\"db-page\" *ngIf=\"config\">\n\n <!-- Header: title, subtitle, books-from caption, owed total, app-supplied actions -->\n <div class=\"db-head\">\n <div class=\"db-head-text\">\n <h1>{{ title }}</h1>\n <p class=\"db-sub\" *ngIf=\"config.subtitle\">{{ config.subtitle }}</p>\n <span class=\"db-caption\" *ngIf=\"booksFrom\">books from {{ booksFrom | date: 'mediumDate' }}</span>\n </div>\n <span class=\"db-owed\" *ngIf=\"owed !== null\">Owed to us: <b>{{ owed | currency }}</b></span>\n <!-- The page's own actions (refresh, an escape hatch to a full screen). Changed: they now come from\n config.actions, because an app with no component of its own has nothing to project with \u2014 the\n ng-content slot stays beside them for anyone who does host the page themselves. -->\n <!-- Changed: an action WITH an icon renders icon-only (owner, 2026-08-08 \u2014 \"the refresh and all trips\n buttons can just be icons with no text\"). The label is not lost, it moves to the tooltip and to\n aria-label, so the control still announces itself to a screen reader and on hover. An action with\n no icon keeps its text button, because an unlabelled blank button would be unusable. -->\n <div class=\"db-head-actions\">\n <ng-container *ngFor=\"let a of pageActions\">\n <button mat-icon-button color=\"primary\" type=\"button\" *ngIf=\"a.icon?.name; else textAction\" (click)=\"pageActionClicked(a)\" [matTooltip]=\"a.tip || a.display || a.name\" [attr.aria-label]=\"a.display || a.name\">\n <mat-icon>{{ a.icon.name }}</mat-icon>\n </button>\n <ng-template #textAction>\n <button mat-stroked-button color=\"primary\" type=\"button\" (click)=\"pageActionClicked(a)\" [title]=\"a.tip || ''\">{{ a.display || a.name }}</button>\n </ng-template>\n </ng-container>\n <ng-content select=\"[dayBookActions]\"></ng-content>\n </div>\n </div>\n\n <!-- Lane pills -->\n <div class=\"db-lanes\" *ngIf=\"lanes.length\">\n <button type=\"button\" class=\"db-lane\" *ngFor=\"let l of lanes\" [class.active]=\"lane === l.key\" (click)=\"setLane(l.key)\">\n <mat-icon *ngIf=\"l.icon\">{{ l.icon }}</mat-icon>{{ l.label }}\n </button>\n </div>\n\n <!-- Added: the library owns the loading state \u2014 first load only, a refresh keeps the page on screen.\n Changed: binds showLoading, the copy that only ever moves between change-detection passes (NG0100) -->\n <div class=\"db-loading\" *ngIf=\"showLoading && !book\">{{ config.loadingMessage || 'Loading\u2026' }}</div>\n\n <ng-container *ngIf=\"book\">\n\n <!-- The stage row. Changed: always spa-tiles \u2014 zero-count tiles hide -->\n <spa-tiles *ngIf=\"tileConfig\" [config]=\"tileConfig\" [data]=\"tileData\" (tileClick)=\"onTileClick($event)\"></spa-tiles>\n\n <!-- All clear -->\n <div class=\"db-clear\" *ngIf=\"allClear\">\n <mat-icon>task_alt</mat-icon>\n <span>{{ config.allClearMessage || 'Nothing outstanding \u2014 all caught up.' }}</span>\n </div>\n\n <!-- Sections. A book carries 9-16 of these cards and, on 'Everything', all of them used to stack in one\n column \u2014 so they are grouped into one collapsible lane group each, with the first open and the rest\n showing only their identity: label, count, money and a chip per list. Sections that declare no lane\n sit above the groups, and with grouping off (one lane, a picked lane, or groupLanes:false) the page\n renders exactly as it did before. -->\n <ng-container *ngFor=\"let s of ungroupedSections\">\n <ng-container *ngTemplateOutlet=\"sectionCard; context: s.outletContext\"></ng-container>\n </ng-container>\n\n <section class=\"db-group\" *ngFor=\"let g of visibleGroups\" [class.closed]=\"g.collapsed\">\n <button type=\"button\" class=\"db-ghead\" (click)=\"toggleGroup(g)\" [attr.aria-expanded]=\"!g.collapsed\">\n <mat-icon *ngIf=\"g.lane.icon\">{{ g.lane.icon }}</mat-icon>\n <span class=\"g-label\">{{ g.lane.label }}</span>\n <span class=\"db-count\">{{ g.count }}</span>\n <span class=\"g-value\" *ngIf=\"g.value\">{{ g.value | currency: undefined : 'symbol' : '1.0-0' }}</span>\n <span class=\"g-lists\">{{ g.sections.length }} {{ g.sections.length === 1 ? 'list' : 'lists' }}</span>\n <mat-icon class=\"g-chev\">{{ g.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n <!-- Collapsed, the group still says what it is about \u2014 one chip per non-empty list, and a chip is a way\n in: it opens the group and scrolls to that section, the same path a stage click takes. -->\n <div class=\"db-chips\" *ngIf=\"g.collapsed\">\n <button type=\"button\" class=\"db-chip\" *ngFor=\"let c of g.chips\" (click)=\"goTo(c.key)\">{{ c.label }}<span>{{ c.count }}</span></button>\n <span class=\"db-chip-none\" *ngIf=\"!g.chips.length\">Nothing outstanding here</span>\n </div>\n <div class=\"db-gbody\" *ngIf=\"!g.collapsed\">\n <ng-container *ngFor=\"let s of g.sections\">\n <ng-container *ngTemplateOutlet=\"sectionCard; context: s.outletContext\"></ng-container>\n </ng-container>\n </div>\n </section>\n\n <!-- One section card, drawn from two places (flat above, and inside a lane group). Changed: ONE rendering\n path \u2014 a section is a spa-table fed local rows off the book object. The table draws its own section\n header (icon, title, count, chips, buttons, caption, chevron); (actionSuccess) reloads the book so a\n row that has just been actioned leaves the list instead of sitting there looking undone \u2014 the section\n has no loadAction of its own to refresh from. -->\n <ng-template #sectionCard let-s>\n <mat-card class=\"db-section\" [id]=\"'sec-' + s.section.key\" [class.alert]=\"s.section.tone === 'alert'\" [class.quiet]=\"s.section.tone === 'quiet'\">\n <!-- the \"why this list exists\" line rides inside the table's own header, under the title. With no\n section header there is nothing to ride, so the card carries it instead. -->\n <p class=\"db-why\" *ngIf=\"s.caption && !s.cfg?.sectionConfig\">{{ s.caption }}</p>\n <spa-table [config]=\"s.cfg\" [data]=\"s.rows\" (actionSuccess)=\"load()\"></spa-table>\n </mat-card>\n </ng-template>\n\n </ng-container>\n\n</div>\n\n<!-- Graceful empty state (feature not configured) -->\n<div class=\"db-empty\" *ngIf=\"!config\">\n <mat-icon>checklist</mat-icon>\n <p>The Day Book is not configured for this app.</p>\n</div>\n", styles: [":host{--db-accent: #1565c0;--db-accent-soft: #e3f2fd;--db-alert: #e53935;--db-alert-text: #c62828;--db-quiet: #b0bec5}.db-page{max-width:1200px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.db-head{display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap}.db-head-text h1{margin:0;font-size:24px}.db-head-text .db-sub{margin:4px 0 0;font-size:14px;color:#0000008c}.db-caption{font-size:12px;color:#0000008c}.db-owed{font-size:13px;color:#000000b3;margin-left:auto}.db-head-actions{display:flex;gap:8px;margin-left:auto}.db-lanes{display:flex;gap:6px;flex-wrap:wrap;align-items:center}.db-lane{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(0,0,0,.12);border-radius:16px;background:transparent;padding:4px 14px;font:inherit;font-size:13px;cursor:pointer;transition:border-color .15s,background .15s}.db-lane:hover{border-color:#90a4ae}.db-lane.active{border-color:var(--db-accent);background:var(--db-accent-soft);color:var(--db-accent);font-weight:500}.db-lane mat-icon{font-size:17px;width:17px;height:17px}.db-loading{padding:40px;text-align:center;color:#00000080}.db-clear{display:flex;align-items:center;gap:10px;border:1px solid #c8e6c9;background:#f6fbf6;border-radius:10px;padding:14px 16px;color:#2e7d32}.db-section{padding:12px 16px;scroll-margin-top:12px;border-left:4px solid transparent}.db-section.alert{border-left-color:var(--db-alert)}.db-section.quiet{border-left-color:var(--db-quiet)}.db-count{background:#00000012;border-radius:11px;padding:1px 9px;font-size:13px;font-weight:600;font-variant-numeric:tabular-nums}.db-why{margin:0 0 10px;color:#0000008c;font-size:13px;max-width:82ch}.db-group{display:flex;flex-direction:column;gap:12px}.db-group.closed{gap:0}.db-ghead{display:flex;align-items:center;gap:10px;width:100%;border:1px solid #e0e0e0;border-radius:6px;background:#fafafa;padding:11px 14px;font:inherit;cursor:pointer;text-align:left;transition:background .15s,border-color .15s}.db-ghead:hover{background:#f2f5f7;border-color:#cfd8dc}.db-group.closed .db-ghead{border-bottom-left-radius:0;border-bottom-right-radius:0}.db-ghead>mat-icon{font-size:20px;width:20px;height:20px;color:var(--db-accent)}.db-ghead .g-label{font-size:15px;font-weight:600;letter-spacing:.2px}.db-ghead .g-value{font-size:13px;font-weight:600;color:#000000b3;font-variant-numeric:tabular-nums}.db-ghead .g-lists{font-size:12px;color:#00000073;margin-left:auto}.db-ghead .g-chev{font-size:22px;width:22px;height:22px;color:#00000073}.db-chips{display:flex;flex-wrap:wrap;gap:6px;border:1px solid #e0e0e0;border-top:0;border-radius:0 0 6px 6px;background:#fff;padding:9px 14px 11px}.db-chip{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(0,0,0,.12);border-radius:14px;background:transparent;padding:2px 6px 2px 11px;font:inherit;font-size:12.5px;color:#000000b3;cursor:pointer}.db-chip:hover{border-color:var(--db-accent);color:var(--db-accent)}.db-chip span{background:#00000012;border-radius:10px;padding:0 7px;font-weight:600;font-variant-numeric:tabular-nums}.db-chip-none{font-size:12.5px;color:#00000073}.db-gbody{display:flex;flex-direction:column;gap:12px}.db-empty{padding:48px;text-align:center;color:#00000080}@media (max-width: 700px){.db-head{gap:6px 10px;align-items:center}.db-head-text{flex:1 1 auto;min-width:0;order:1}.db-head-text h1{font-size:20px}.db-head-text .db-sub{display:none}.db-caption{font-size:11px}.db-head-actions{order:2;margin-left:auto;gap:2px}.db-owed{order:3;flex:1 0 100%;margin-left:0;font-size:13px}.db-lanes{flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}.db-lanes::-webkit-scrollbar{display:none}.db-lane{flex:0 0 auto;min-height:32px}.db-page{padding:10px 0;gap:12px}.db-section{padding:10px;border-left-width:3px}.db-why{margin-bottom:8px;font-size:12.5px}.db-group{gap:8px}.db-gbody{gap:10px}.db-ghead{padding:8px 10px;gap:8px;min-height:40px}.db-ghead>mat-icon{font-size:18px;width:18px;height:18px}.db-ghead .g-label{font-size:14px}.db-ghead .g-value{font-size:12.5px}.db-ghead .g-lists{font-size:11px}.db-ghead .g-chev{font-size:20px;width:20px;height:20px}.db-count{font-size:12px;padding:1px 7px}.db-chips{padding:7px 10px 8px;gap:6px}.db-chip{min-height:32px}.db-clear{padding:10px 12px;gap:8px}.db-loading{padding:24px}.db-empty{padding:32px 16px}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: TilesComponent, selector: "spa-tiles", inputs: ["config", "lastSearch", "data", "reload"], outputs: ["tileActionSelected", "tileClick", "tileUnClick"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }, { kind: "pipe", type: i1$2.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }] }); }
|
|
18831
19255
|
}
|
|
18832
19256
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DayBookComponent, decorators: [{
|
|
18833
19257
|
type: Component,
|
|
18834
|
-
args: [{ selector: 'spa-day-book', standalone: false, template: "<div class=\"db-page\" *ngIf=\"config\">\n\n <!-- Header: title, subtitle, books-from caption, owed total, app-supplied actions -->\n <div class=\"db-head\">\n <div class=\"db-head-text\">\n <h1>{{ title }}</h1>\n <p class=\"db-sub\" *ngIf=\"config.subtitle\">{{ config.subtitle }}</p>\n <span class=\"db-caption\" *ngIf=\"booksFrom\">books from {{ booksFrom | date: 'mediumDate' }}</span>\n </div>\n <span class=\"db-owed\" *ngIf=\"owed !== null\">Owed to us: <b>{{ owed | currency }}</b></span>\n <!-- The page's own actions (refresh, an escape hatch to a full screen). Changed: they now come from\n config.actions, because an app with no component of its own has nothing to project with \u2014 the\n ng-content slot stays beside them for anyone who does host the page themselves. -->\n <!-- Changed: an action WITH an icon renders icon-only (owner, 2026-08-08 \u2014 \"the refresh and all trips\n buttons can just be icons with no text\"). The label is not lost, it moves to the tooltip and to\n aria-label, so the control still announces itself to a screen reader and on hover. An action with\n no icon keeps its text button, because an unlabelled blank button would be unusable. -->\n <div class=\"db-head-actions\">\n <ng-container *ngFor=\"let a of pageActions\">\n <button mat-icon-button color=\"primary\" type=\"button\" *ngIf=\"a.icon?.name; else textAction\" (click)=\"pageActionClicked(a)\" [matTooltip]=\"a.tip || a.display || a.name\" [attr.aria-label]=\"a.display || a.name\">\n <mat-icon>{{ a.icon.name }}</mat-icon>\n </button>\n <ng-template #textAction>\n <button mat-stroked-button color=\"primary\" type=\"button\" (click)=\"pageActionClicked(a)\" [title]=\"a.tip || ''\">{{ a.display || a.name }}</button>\n </ng-template>\n </ng-container>\n <ng-content select=\"[dayBookActions]\"></ng-content>\n </div>\n </div>\n\n <!-- Lane pills -->\n <div class=\"db-lanes\" *ngIf=\"lanes.length\">\n <button type=\"button\" class=\"db-lane\" *ngFor=\"let l of lanes\" [class.active]=\"lane === l.key\" (click)=\"setLane(l.key)\">\n <mat-icon *ngIf=\"l.icon\">{{ l.icon }}</mat-icon>{{ l.label }}\n </button>\n </div>\n\n <!-- Added: the library owns the loading state \u2014 first load only, a refresh keeps the page on screen.\n Changed: binds showLoading, the copy that only ever moves between change-detection passes (NG0100) -->\n <div class=\"db-loading\" *ngIf=\"showLoading && !book\">{{ config.loadingMessage || 'Loading\u2026' }}</div>\n\n <ng-container *ngIf=\"book\">\n\n <!-- The stage row. Changed: always spa-tiles \u2014 zero-count tiles hide -->\n <spa-tiles *ngIf=\"tileConfig\" [config]=\"tileConfig\" [data]=\"tileData\" (tileClick)=\"onTileClick($event)\"></spa-tiles>\n\n <!-- All clear -->\n <div class=\"db-clear\" *ngIf=\"allClear\">\n <mat-icon>task_alt</mat-icon>\n <span>{{ config.allClearMessage || 'Nothing outstanding \u2014 all caught up.' }}</span>\n </div>\n\n <!-- Sections. A book carries 9-16 of these cards and, on 'Everything', all of them used to stack in one\n column \u2014 so they are grouped into one collapsible lane group each, with the first open and the rest\n showing only their identity: label, count, money and a chip per list. Sections that declare no lane\n sit above the groups, and with grouping off (one lane, a picked lane, or groupLanes:false) the page\n renders exactly as it did before. -->\n <ng-container *ngFor=\"let s of ungroupedSections\">\n <ng-container *ngTemplateOutlet=\"sectionCard; context: s.outletContext\"></ng-container>\n </ng-container>\n\n <section class=\"db-group\" *ngFor=\"let g of visibleGroups\" [class.closed]=\"g.collapsed\">\n <button type=\"button\" class=\"db-ghead\" (click)=\"toggleGroup(g)\" [attr.aria-expanded]=\"!g.collapsed\">\n <mat-icon *ngIf=\"g.lane.icon\">{{ g.lane.icon }}</mat-icon>\n <span class=\"g-label\">{{ g.lane.label }}</span>\n <span class=\"db-count\">{{ g.count }}</span>\n <span class=\"g-value\" *ngIf=\"g.value\">{{ g.value | currency: undefined : 'symbol' : '1.0-0' }}</span>\n <span class=\"g-lists\">{{ g.sections.length }} {{ g.sections.length === 1 ? 'list' : 'lists' }}</span>\n <mat-icon class=\"g-chev\">{{ g.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n <!-- Collapsed, the group still says what it is about \u2014 one chip per non-empty list, and a chip is a way\n in: it opens the group and scrolls to that section, the same path a stage click takes. -->\n <div class=\"db-chips\" *ngIf=\"g.collapsed\">\n <button type=\"button\" class=\"db-chip\" *ngFor=\"let c of g.chips\" (click)=\"goTo(c.key)\">{{ c.label }}<span>{{ c.count }}</span></button>\n <span class=\"db-chip-none\" *ngIf=\"!g.chips.length\">Nothing outstanding here</span>\n </div>\n <div class=\"db-gbody\" *ngIf=\"!g.collapsed\">\n <ng-container *ngFor=\"let s of g.sections\">\n <ng-container *ngTemplateOutlet=\"sectionCard; context: s.outletContext\"></ng-container>\n </ng-container>\n </div>\n </section>\n\n <!-- One section card, drawn from two places (flat above, and inside a lane group). Changed: ONE rendering\n path \u2014 a section is a spa-table fed local rows off the book object. The table draws its own section\n header (icon, title, count, chips, buttons, caption, chevron); (actionSuccess) reloads the book so a\n row that has just been actioned leaves the list instead of sitting there looking undone \u2014 the section\n has no loadAction of its own to refresh from. -->\n <ng-template #sectionCard let-s>\n <mat-card class=\"db-section\" [id]=\"'sec-' + s.section.key\" [class.alert]=\"s.section.tone === 'alert'\" [class.quiet]=\"s.section.tone === 'quiet'\">\n <!-- the \"why this list exists\" line rides inside the table's own header, under the title. With no\n section header there is nothing to ride, so the card carries it instead. -->\n <p class=\"db-why\" *ngIf=\"s.caption && !s.cfg?.sectionConfig\">{{ s.caption }}</p>\n <spa-table [config]=\"s.cfg\" [data]=\"s.rows\" (actionSuccess)=\"load()\"></spa-table>\n </mat-card>\n </ng-template>\n\n </ng-container>\n\n</div>\n\n<!-- Graceful empty state (feature not configured) -->\n<div class=\"db-empty\" *ngIf=\"!config\">\n <mat-icon>checklist</mat-icon>\n <p>The Day Book is not configured for this app.</p>\n</div>\n", styles: [":host{--db-accent: #1565c0;--db-accent-soft: #e3f2fd;--db-alert: #e53935;--db-alert-text: #c62828;--db-quiet: #b0bec5}.db-page{max-width:1200px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.db-head{display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap}.db-head-text h1{margin:0;font-size:24px}.db-head-text .db-sub{margin:4px 0 0;font-size:14px;color:#0000008c}.db-caption{font-size:12px;color:#0000008c}.db-owed{font-size:13px;color:#000000b3;margin-left:auto}.db-head-actions{display:flex;gap:8px;margin-left:auto}.db-lanes{display:flex;gap:6px;flex-wrap:wrap;align-items:center}.db-lane{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(0,0,0,.12);border-radius:16px;background:transparent;padding:4px 14px;font:inherit;font-size:13px;cursor:pointer;transition:border-color .15s,background .15s}.db-lane:hover{border-color:#90a4ae}.db-lane.active{border-color:var(--db-accent);background:var(--db-accent-soft);color:var(--db-accent);font-weight:500}.db-lane mat-icon{font-size:17px;width:17px;height:17px}@media (max-width: 700px){.db-head{gap:6px 10px;align-items:center}.db-head-text{flex:1 1 auto;min-width:0;order:1}.db-head-text h1{font-size:20px}.db-head-text .db-sub{display:none}.db-caption{font-size:11px}.db-head-actions{order:2;margin-left:auto;gap:2px}.db-owed{order:3;flex:1 0 100%;margin-left:0;font-size:13px}.db-lanes{flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}.db-lanes::-webkit-scrollbar{display:none}.db-lane{flex:0 0 auto}}.db-loading{padding:40px;text-align:center;color:#00000080}.db-clear{display:flex;align-items:center;gap:10px;border:1px solid #c8e6c9;background:#f6fbf6;border-radius:10px;padding:14px 16px;color:#2e7d32}.db-section{padding:12px 16px;scroll-margin-top:12px;border-left:4px solid transparent}.db-section.alert{border-left-color:var(--db-alert)}.db-section.quiet{border-left-color:var(--db-quiet)}.db-count{background:#00000012;border-radius:11px;padding:1px 9px;font-size:13px;font-weight:600;font-variant-numeric:tabular-nums}.db-why{margin:0 0 10px;color:#0000008c;font-size:13px;max-width:82ch}.db-group{display:flex;flex-direction:column;gap:12px}.db-group.closed{gap:0}.db-ghead{display:flex;align-items:center;gap:10px;width:100%;border:1px solid #e0e0e0;border-radius:6px;background:#fafafa;padding:11px 14px;font:inherit;cursor:pointer;text-align:left;transition:background .15s,border-color .15s}.db-ghead:hover{background:#f2f5f7;border-color:#cfd8dc}.db-group.closed .db-ghead{border-bottom-left-radius:0;border-bottom-right-radius:0}.db-ghead>mat-icon{font-size:20px;width:20px;height:20px;color:var(--db-accent)}.db-ghead .g-label{font-size:15px;font-weight:600;letter-spacing:.2px}.db-ghead .g-value{font-size:13px;font-weight:600;color:#000000b3;font-variant-numeric:tabular-nums}.db-ghead .g-lists{font-size:12px;color:#00000073;margin-left:auto}.db-ghead .g-chev{font-size:22px;width:22px;height:22px;color:#00000073}.db-chips{display:flex;flex-wrap:wrap;gap:6px;border:1px solid #e0e0e0;border-top:0;border-radius:0 0 6px 6px;background:#fff;padding:9px 14px 11px}.db-chip{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(0,0,0,.12);border-radius:14px;background:transparent;padding:2px 6px 2px 11px;font:inherit;font-size:12.5px;color:#000000b3;cursor:pointer}.db-chip:hover{border-color:var(--db-accent);color:var(--db-accent)}.db-chip span{background:#00000012;border-radius:10px;padding:0 7px;font-weight:600;font-variant-numeric:tabular-nums}.db-chip-none{font-size:12.5px;color:#00000073}.db-gbody{display:flex;flex-direction:column;gap:12px}.db-empty{padding:48px;text-align:center;color:#00000080}\n"] }]
|
|
19258
|
+
args: [{ selector: 'spa-day-book', standalone: false, template: "<div class=\"db-page\" *ngIf=\"config\">\n\n <!-- Header: title, subtitle, books-from caption, owed total, app-supplied actions -->\n <div class=\"db-head\">\n <div class=\"db-head-text\">\n <h1>{{ title }}</h1>\n <p class=\"db-sub\" *ngIf=\"config.subtitle\">{{ config.subtitle }}</p>\n <span class=\"db-caption\" *ngIf=\"booksFrom\">books from {{ booksFrom | date: 'mediumDate' }}</span>\n </div>\n <span class=\"db-owed\" *ngIf=\"owed !== null\">Owed to us: <b>{{ owed | currency }}</b></span>\n <!-- The page's own actions (refresh, an escape hatch to a full screen). Changed: they now come from\n config.actions, because an app with no component of its own has nothing to project with \u2014 the\n ng-content slot stays beside them for anyone who does host the page themselves. -->\n <!-- Changed: an action WITH an icon renders icon-only (owner, 2026-08-08 \u2014 \"the refresh and all trips\n buttons can just be icons with no text\"). The label is not lost, it moves to the tooltip and to\n aria-label, so the control still announces itself to a screen reader and on hover. An action with\n no icon keeps its text button, because an unlabelled blank button would be unusable. -->\n <div class=\"db-head-actions\">\n <ng-container *ngFor=\"let a of pageActions\">\n <button mat-icon-button color=\"primary\" type=\"button\" *ngIf=\"a.icon?.name; else textAction\" (click)=\"pageActionClicked(a)\" [matTooltip]=\"a.tip || a.display || a.name\" [attr.aria-label]=\"a.display || a.name\">\n <mat-icon>{{ a.icon.name }}</mat-icon>\n </button>\n <ng-template #textAction>\n <button mat-stroked-button color=\"primary\" type=\"button\" (click)=\"pageActionClicked(a)\" [title]=\"a.tip || ''\">{{ a.display || a.name }}</button>\n </ng-template>\n </ng-container>\n <ng-content select=\"[dayBookActions]\"></ng-content>\n </div>\n </div>\n\n <!-- Lane pills -->\n <div class=\"db-lanes\" *ngIf=\"lanes.length\">\n <button type=\"button\" class=\"db-lane\" *ngFor=\"let l of lanes\" [class.active]=\"lane === l.key\" (click)=\"setLane(l.key)\">\n <mat-icon *ngIf=\"l.icon\">{{ l.icon }}</mat-icon>{{ l.label }}\n </button>\n </div>\n\n <!-- Added: the library owns the loading state \u2014 first load only, a refresh keeps the page on screen.\n Changed: binds showLoading, the copy that only ever moves between change-detection passes (NG0100) -->\n <div class=\"db-loading\" *ngIf=\"showLoading && !book\">{{ config.loadingMessage || 'Loading\u2026' }}</div>\n\n <ng-container *ngIf=\"book\">\n\n <!-- The stage row. Changed: always spa-tiles \u2014 zero-count tiles hide -->\n <spa-tiles *ngIf=\"tileConfig\" [config]=\"tileConfig\" [data]=\"tileData\" (tileClick)=\"onTileClick($event)\"></spa-tiles>\n\n <!-- All clear -->\n <div class=\"db-clear\" *ngIf=\"allClear\">\n <mat-icon>task_alt</mat-icon>\n <span>{{ config.allClearMessage || 'Nothing outstanding \u2014 all caught up.' }}</span>\n </div>\n\n <!-- Sections. A book carries 9-16 of these cards and, on 'Everything', all of them used to stack in one\n column \u2014 so they are grouped into one collapsible lane group each, with the first open and the rest\n showing only their identity: label, count, money and a chip per list. Sections that declare no lane\n sit above the groups, and with grouping off (one lane, a picked lane, or groupLanes:false) the page\n renders exactly as it did before. -->\n <ng-container *ngFor=\"let s of ungroupedSections\">\n <ng-container *ngTemplateOutlet=\"sectionCard; context: s.outletContext\"></ng-container>\n </ng-container>\n\n <section class=\"db-group\" *ngFor=\"let g of visibleGroups\" [class.closed]=\"g.collapsed\">\n <button type=\"button\" class=\"db-ghead\" (click)=\"toggleGroup(g)\" [attr.aria-expanded]=\"!g.collapsed\">\n <mat-icon *ngIf=\"g.lane.icon\">{{ g.lane.icon }}</mat-icon>\n <span class=\"g-label\">{{ g.lane.label }}</span>\n <span class=\"db-count\">{{ g.count }}</span>\n <span class=\"g-value\" *ngIf=\"g.value\">{{ g.value | currency: undefined : 'symbol' : '1.0-0' }}</span>\n <span class=\"g-lists\">{{ g.sections.length }} {{ g.sections.length === 1 ? 'list' : 'lists' }}</span>\n <mat-icon class=\"g-chev\">{{ g.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n <!-- Collapsed, the group still says what it is about \u2014 one chip per non-empty list, and a chip is a way\n in: it opens the group and scrolls to that section, the same path a stage click takes. -->\n <div class=\"db-chips\" *ngIf=\"g.collapsed\">\n <button type=\"button\" class=\"db-chip\" *ngFor=\"let c of g.chips\" (click)=\"goTo(c.key)\">{{ c.label }}<span>{{ c.count }}</span></button>\n <span class=\"db-chip-none\" *ngIf=\"!g.chips.length\">Nothing outstanding here</span>\n </div>\n <div class=\"db-gbody\" *ngIf=\"!g.collapsed\">\n <ng-container *ngFor=\"let s of g.sections\">\n <ng-container *ngTemplateOutlet=\"sectionCard; context: s.outletContext\"></ng-container>\n </ng-container>\n </div>\n </section>\n\n <!-- One section card, drawn from two places (flat above, and inside a lane group). Changed: ONE rendering\n path \u2014 a section is a spa-table fed local rows off the book object. The table draws its own section\n header (icon, title, count, chips, buttons, caption, chevron); (actionSuccess) reloads the book so a\n row that has just been actioned leaves the list instead of sitting there looking undone \u2014 the section\n has no loadAction of its own to refresh from. -->\n <ng-template #sectionCard let-s>\n <mat-card class=\"db-section\" [id]=\"'sec-' + s.section.key\" [class.alert]=\"s.section.tone === 'alert'\" [class.quiet]=\"s.section.tone === 'quiet'\">\n <!-- the \"why this list exists\" line rides inside the table's own header, under the title. With no\n section header there is nothing to ride, so the card carries it instead. -->\n <p class=\"db-why\" *ngIf=\"s.caption && !s.cfg?.sectionConfig\">{{ s.caption }}</p>\n <spa-table [config]=\"s.cfg\" [data]=\"s.rows\" (actionSuccess)=\"load()\"></spa-table>\n </mat-card>\n </ng-template>\n\n </ng-container>\n\n</div>\n\n<!-- Graceful empty state (feature not configured) -->\n<div class=\"db-empty\" *ngIf=\"!config\">\n <mat-icon>checklist</mat-icon>\n <p>The Day Book is not configured for this app.</p>\n</div>\n", styles: [":host{--db-accent: #1565c0;--db-accent-soft: #e3f2fd;--db-alert: #e53935;--db-alert-text: #c62828;--db-quiet: #b0bec5}.db-page{max-width:1200px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.db-head{display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap}.db-head-text h1{margin:0;font-size:24px}.db-head-text .db-sub{margin:4px 0 0;font-size:14px;color:#0000008c}.db-caption{font-size:12px;color:#0000008c}.db-owed{font-size:13px;color:#000000b3;margin-left:auto}.db-head-actions{display:flex;gap:8px;margin-left:auto}.db-lanes{display:flex;gap:6px;flex-wrap:wrap;align-items:center}.db-lane{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(0,0,0,.12);border-radius:16px;background:transparent;padding:4px 14px;font:inherit;font-size:13px;cursor:pointer;transition:border-color .15s,background .15s}.db-lane:hover{border-color:#90a4ae}.db-lane.active{border-color:var(--db-accent);background:var(--db-accent-soft);color:var(--db-accent);font-weight:500}.db-lane mat-icon{font-size:17px;width:17px;height:17px}.db-loading{padding:40px;text-align:center;color:#00000080}.db-clear{display:flex;align-items:center;gap:10px;border:1px solid #c8e6c9;background:#f6fbf6;border-radius:10px;padding:14px 16px;color:#2e7d32}.db-section{padding:12px 16px;scroll-margin-top:12px;border-left:4px solid transparent}.db-section.alert{border-left-color:var(--db-alert)}.db-section.quiet{border-left-color:var(--db-quiet)}.db-count{background:#00000012;border-radius:11px;padding:1px 9px;font-size:13px;font-weight:600;font-variant-numeric:tabular-nums}.db-why{margin:0 0 10px;color:#0000008c;font-size:13px;max-width:82ch}.db-group{display:flex;flex-direction:column;gap:12px}.db-group.closed{gap:0}.db-ghead{display:flex;align-items:center;gap:10px;width:100%;border:1px solid #e0e0e0;border-radius:6px;background:#fafafa;padding:11px 14px;font:inherit;cursor:pointer;text-align:left;transition:background .15s,border-color .15s}.db-ghead:hover{background:#f2f5f7;border-color:#cfd8dc}.db-group.closed .db-ghead{border-bottom-left-radius:0;border-bottom-right-radius:0}.db-ghead>mat-icon{font-size:20px;width:20px;height:20px;color:var(--db-accent)}.db-ghead .g-label{font-size:15px;font-weight:600;letter-spacing:.2px}.db-ghead .g-value{font-size:13px;font-weight:600;color:#000000b3;font-variant-numeric:tabular-nums}.db-ghead .g-lists{font-size:12px;color:#00000073;margin-left:auto}.db-ghead .g-chev{font-size:22px;width:22px;height:22px;color:#00000073}.db-chips{display:flex;flex-wrap:wrap;gap:6px;border:1px solid #e0e0e0;border-top:0;border-radius:0 0 6px 6px;background:#fff;padding:9px 14px 11px}.db-chip{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(0,0,0,.12);border-radius:14px;background:transparent;padding:2px 6px 2px 11px;font:inherit;font-size:12.5px;color:#000000b3;cursor:pointer}.db-chip:hover{border-color:var(--db-accent);color:var(--db-accent)}.db-chip span{background:#00000012;border-radius:10px;padding:0 7px;font-weight:600;font-variant-numeric:tabular-nums}.db-chip-none{font-size:12.5px;color:#00000073}.db-gbody{display:flex;flex-direction:column;gap:12px}.db-empty{padding:48px;text-align:center;color:#00000080}@media (max-width: 700px){.db-head{gap:6px 10px;align-items:center}.db-head-text{flex:1 1 auto;min-width:0;order:1}.db-head-text h1{font-size:20px}.db-head-text .db-sub{display:none}.db-caption{font-size:11px}.db-head-actions{order:2;margin-left:auto;gap:2px}.db-owed{order:3;flex:1 0 100%;margin-left:0;font-size:13px}.db-lanes{flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}.db-lanes::-webkit-scrollbar{display:none}.db-lane{flex:0 0 auto;min-height:32px}.db-page{padding:10px 0;gap:12px}.db-section{padding:10px;border-left-width:3px}.db-why{margin-bottom:8px;font-size:12.5px}.db-group{gap:8px}.db-gbody{gap:10px}.db-ghead{padding:8px 10px;gap:8px;min-height:40px}.db-ghead>mat-icon{font-size:18px;width:18px;height:18px}.db-ghead .g-label{font-size:14px}.db-ghead .g-value{font-size:12.5px}.db-ghead .g-lists{font-size:11px}.db-ghead .g-chev{font-size:20px;width:20px;height:20px}.db-count{font-size:12px;padding:1px 7px}.db-chips{padding:7px 10px 8px;gap:6px}.db-chip{min-height:32px}.db-clear{padding:10px 12px;gap:8px}.db-loading{padding:24px}.db-empty{padding:32px 16px}}\n"] }]
|
|
18835
19259
|
}], propDecorators: { tables: [{
|
|
18836
19260
|
type: ViewChildren,
|
|
18837
19261
|
args: [TableComponent]
|
|
@@ -19723,11 +20147,11 @@ class FormComponent {
|
|
|
19723
20147
|
processForm() {
|
|
19724
20148
|
}
|
|
19725
20149
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: FormComponent, deps: [{ token: MessageService }, { token: DataServiceLib }, { token: AuthService }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
19726
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: FormComponent, isStandalone: false, selector: "spa-form", inputs: { files: "files", data: "data", config: "config" }, outputs: { buttonClick: "buttonClick", inputChange: "inputChange" }, queries: [{ propertyName: "dynamicSelectTemplate", first: true, predicate: ["dynamicSelect"], descendants: true }], viewQueries: [{ propertyName: "defaultDynamicSelectTemplate", first: true, predicate: ["defaultDynamicSelect"], descendants: true, static: true }], ngImport: i0, template: "\n\n\n<div class=\"tin-form-container\" >\n <div [ngClass]=\"[multiColumn ? 'tin-grid' : 'tin-col', config.notesConfig ? 'width-75' : 'width-100']\" class=\"form-main-content\">\n\n <div *ngIf=\"!hasAccess\" class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n \n <div [ngClass]=\"field.span || field.type =='section' || field.type =='file' || field.type =='file-view' || field.type =='editor' ? 'span-col' : ''\" *ngFor=\"let field of visibleFields\"><!-- TS-11: bind cached visibleFields instead of getVisibleFields() per CD -->\n \n <ng-container>\n \n <ng-container [ngSwitch]=\"field.type\" class=\"highlight\">\n \n <div *ngSwitchCase=\"'section'\" class=\"title d-flex align-items-center\" (click)=\"toggleSection(field)\" style=\"cursor: pointer;\">\n <label style=\"font-size: larger;margin-right: 10px;\">{{field.alias ?? field.name | camelToWords}}</label>\n <mat-icon *ngIf=\"field.infoMessage\" (click)=\"onInfoClick($event, field.infoMessage)\" style=\"color: steelblue; font-size: 14px;\">info</mat-icon>\n <!-- <button mat-icon-button class=\"info-icon-button\" matTooltip=\"Info\" matTooltipPosition=\"above\">\n \n </button> -->\n <mat-icon *ngIf=\"hasSectionFields(field.name)\">{{shouldSectionCollapse(field) ? 'expand_more' : 'expand_less'}}</mat-icon>\n </div>\n \n <ng-container *ngSwitchCase=\"'file'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode !='view'\">\n <spa-attach [message]=\"field.alias ?? 'Drag and Drop files here'\" [(files)]=\"files\" [fileOptions]=\"field.fileOptions\"></spa-attach>\n </div>\n </ng-container>\n \n <ng-container *ngSwitchCase=\"'file-view'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode && config.mode !='create'\">\n <spa-viewer [fileAction]=\"field.loadAction\" [path]=\"field.path\" [folderName]=\"data[field.keyField]\" ></spa-viewer>\n </div>\n </ng-container>\n \n <spa-html *ngSwitchCase=\"'html'\" [display]=\"field.alias | camelToWords\" [value]=\"data[field.name]\" [maxHeight]=\"field.maxHeight\"></spa-html>\n \n <label *ngSwitchCase=\"'blank'\"></label>\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-label *ngSwitchCase=\"'label'\" [display]=\"field.alias ?? field.name | camelToWords\" [value]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [format]=\"field.format ?? 'text'\" [suffix]=\"field.suffix\" [size]=\"field.size\"></spa-label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [infoMessage]=\"field.infoMessage\" ></spa-check>\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [min]=\"field?.min\" [max]=\"field?.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [min]=\"field.min\" [max]=\"field.max\" [infoMessage]=\"field.infoMessage\" ></spa-datetime>\n \n <spa-email *ngSwitchCase=\"'email'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-email>\n \n <spa-text-mask *ngSwitchCase=\"'text-mask'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\"></spa-text-mask>\n \n \n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: field,\n field: field,\n data: data,\n testReadOnly: testReadOnly.bind(this),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n \n <spa-select-multi *ngSwitchCase=\"'select-multi'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [selectAll]=\"field.selectAll\">\n </spa-select-multi>\n \n <spa-text-multi *ngSwitchCase=\"'text-multi'\" [strict]=\"field.strict\" [display]=\"field.alias ?? field.name | camelToWords\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\"></spa-text-multi>\n \n \n \n <ng-container *ngSwitchCase=\"'composite'\">\n <div class=\"composite-field-container\">\n <div class=\"composite-field-group\">\n <ng-container *ngFor=\"let subfield of getVisibleSubfields(field)\">\n <ng-container [ngSwitch]=\"subfield.type\">\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [infoMessage]=\"subfield.infoMessage\" ></spa-check>\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [infoMessage]=\"subfield.infoMessage\" ></spa-datetime>\n \n <!-- Fixed: composite select subfields projected the PARENT composite field into the\n dynamic select template (wrong options/value binding) and omitted testRequired,\n so the template's testRequired(field) call threw and killed the whole dialog.\n Context now mirrors the top-level select outlet, with the SUBFIELD, and readonly\n cascades composite parent || subfield like every other subfield type. -->\n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: subfield,\n field: subfield,\n data: data,\n testReadOnly: compositeReadOnly(field),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [options]=\"subfield.options\" [optionDisplay]=\"subfield.optionDisplay ?? 'name'\" [optionValue]=\"subfield.optionValue ?? 'value'\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [loadAction]=\"resolveLoadAction(subfield)\" [regex]=\"subfield.regex\" [field]=\"subfield\" [data]=\"data\" [detailsConfig]=\"subfield.detailsConfig\" [masterField]=\"subfield.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [rows]=\"subfield.rows\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [editorConfig]=\"subfield.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text>\n \n \n </ng-container>\n </ng-container>\n </div>\n </div>\n </ng-container>\n \n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [regex]=\"field.regex\" [field]=\"field\" [data]=\"data\" [detailsConfig]=\"field.detailsConfig\" [masterField]=\"field.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [rows]=\"field.rows\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [editorConfig]=\"field.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text>\n \n </ng-container>\n \n </ng-container>\n \n </div>\n \n \n <div class=\"span-col-center\" *ngIf=\"config.button\">\n <button mat-raised-button color=\"primary\" (click)=\"buttonClicked()\" cdkFocusInitial>{{buttonDisplay}}</button>\n </div>\n \n \n </div>\n <!-- Fixed: built-in fallback select template \u2014 spa-form used consumers' projected #dynamicSelect\n (only detailsDialog provides one), so selects silently rendered NOTHING when spa-form was used\n directly. The projected template (if any) still wins; this default carries the same bindings. -->\n <ng-template #defaultDynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\" let-resolveLoadAction=\"resolveLoadAction\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"resolveLoadAction(field)\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n\n <div class=\"notes-section\" *ngIf=\"config.notesConfig\">\n <spa-notes\n [title]=\"config.notesConfig.title || 'Notes'\"\n [notes]=\"config.notesConfig.notes || []\"\n [loadAction]=\"config.notesConfig.loadAction\"\n [loadIDField]=\"config.notesConfig.loadIDField\"\n [data]=\"data\"\n [nameField]=\"config.notesConfig.nameField || 'createdByName'\"\n [dateField]=\"config.notesConfig.dateField || 'createdDate'\"\n [commentField]=\"config.notesConfig.commentField || 'details'\">\n </spa-notes>\n </div>\n</div>\n\n", styles: [".title{margin-top:.5em;margin-bottom:.5em;font-size:larger;font-weight:300;color:#0b447e}.composite-field-group{display:flex;flex-direction:row;flex-wrap:wrap;gap:12px}.tin-form-container{display:flex;flex-direction:row;width:100%;gap:16px}.width-100{width:100%!important}.width-75{width:70%!important}.notes-section{width:400px;border-left:1px solid #e0e0e0}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i1$2.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1$2.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1$2.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextMaskComponent, selector: "spa-text-mask", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "regex", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextAreaComponent, selector: "spa-text-area", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "rows", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextSingleComponent, selector: "spa-text-single", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "width", "copyContent", "clearContent", "options", "optionDisplay", "optionValue", "loadAction", "required", "min", "max", "regex", "suffix", "infoMessage", "field", "data", "detailsConfig", "masterField"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: DateComponent, selector: "spa-date", inputs: ["required", "min", "max", "readonly", "hint", "value", "display", "placeholder", "width", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: DatetimeComponent, selector: "spa-datetime", inputs: ["display", "value", "readonly", "width", "min", "max", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: LabelComponent, selector: "spa-label", inputs: ["display", "value", "format", "suffix", "size"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: MoneyComponent, selector: "spa-money", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "currency", "required", "min", "max", "infoMessage", "copyContent", "clearContent", "suffix"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: AttachComponent, selector: "spa-attach", inputs: ["fileOptions", "message", "files", "enableUpload"], outputs: ["filesChange", "upload"] }, { kind: "component", type: NumberComponent, selector: "spa-number", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "step", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: ViewerComponent, selector: "spa-viewer", inputs: ["fileAction", "path", "folderName", "fileNames", "removable", "display", "title"], outputs: ["remove"] }, { kind: "component", type: EmailComponent, selector: "spa-email", inputs: ["display", "value", "readonly", "required", "hint", "suffix", "infoMessage", "copyContent", "clearContent", "options", "optionValue"], outputs: ["valueChange"] }, { kind: "component", type: TextMultiComponent, selector: "spa-text-multi", inputs: ["display", "value", "readonly", "required", "hint", "strict", "suffix", "infoMessage", "copyContent", "clearContent", "options", "optionDisplay", "optionValue", "loadAction"], outputs: ["valueChange", "hoverChange"] }, { kind: "component", type: SelectMultiComponent, selector: "spa-select-multi", inputs: ["display", "value", "readonly", "required", "hint", "options", "optionDisplay", "optionValue", "infoMessage", "copyContent", "clearContent", "nullable", "placeholder", "width", "suffix", "loadAction", "selectAll"], outputs: ["valueChange", "hoverChange"] }, { kind: "component", type: HtmlComponent, selector: "spa-html", inputs: ["value", "maxHeight", "display"] }, { kind: "component", type: EditorComponent, selector: "spa-editor", inputs: ["display", "value", "readonly", "required", "hint", "infoMessage", "placeholder", "width", "height", "minHeight", "defaultFontName", "editorConfig"], outputs: ["valueChange"] }, { kind: "component", type: NotesComponent, selector: "spa-notes", inputs: ["title", "notes", "loadAction", "loadIDField", "data", "nameField", "dateField", "commentField"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
|
|
20150
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: FormComponent, isStandalone: false, selector: "spa-form", inputs: { files: "files", data: "data", config: "config" }, outputs: { buttonClick: "buttonClick", inputChange: "inputChange" }, queries: [{ propertyName: "dynamicSelectTemplate", first: true, predicate: ["dynamicSelect"], descendants: true }], viewQueries: [{ propertyName: "defaultDynamicSelectTemplate", first: true, predicate: ["defaultDynamicSelect"], descendants: true, static: true }], ngImport: i0, template: "\n\n\n<div class=\"tin-form-container\" >\n <div [ngClass]=\"[multiColumn ? 'tin-grid' : 'tin-col', config.notesConfig ? 'width-75' : 'width-100']\" class=\"form-main-content\">\n\n <div *ngIf=\"!hasAccess\" class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n \n <div [ngClass]=\"field.span || field.type =='section' || field.type =='file' || field.type =='file-view' || field.type =='editor' ? 'span-col' : ''\" *ngFor=\"let field of visibleFields\"><!-- TS-11: bind cached visibleFields instead of getVisibleFields() per CD -->\n \n <ng-container>\n \n <ng-container [ngSwitch]=\"field.type\" class=\"highlight\">\n \n <div *ngSwitchCase=\"'section'\" class=\"title d-flex align-items-center\" (click)=\"toggleSection(field)\" style=\"cursor: pointer;\">\n <label style=\"font-size: larger;margin-right: 10px;\">{{field.alias ?? field.name | camelToWords}}</label>\n <mat-icon *ngIf=\"field.infoMessage\" (click)=\"onInfoClick($event, field.infoMessage)\" style=\"color: steelblue; font-size: 14px;\">info</mat-icon>\n <!-- <button mat-icon-button class=\"info-icon-button\" matTooltip=\"Info\" matTooltipPosition=\"above\">\n \n </button> -->\n <mat-icon *ngIf=\"hasSectionFields(field.name)\">{{shouldSectionCollapse(field) ? 'expand_more' : 'expand_less'}}</mat-icon>\n </div>\n \n <ng-container *ngSwitchCase=\"'file'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode !='view'\">\n <spa-attach [message]=\"field.alias ?? 'Drag and Drop files here'\" [(files)]=\"files\" [fileOptions]=\"field.fileOptions\"></spa-attach>\n </div>\n </ng-container>\n \n <ng-container *ngSwitchCase=\"'file-view'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode && config.mode !='create'\">\n <spa-viewer [fileAction]=\"field.loadAction\" [path]=\"field.path\" [folderName]=\"data[field.keyField]\" ></spa-viewer>\n </div>\n </ng-container>\n \n <spa-html *ngSwitchCase=\"'html'\" [display]=\"field.alias | camelToWords\" [value]=\"data[field.name]\" [maxHeight]=\"field.maxHeight\"></spa-html>\n \n <label *ngSwitchCase=\"'blank'\"></label>\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-label *ngSwitchCase=\"'label'\" [display]=\"field.alias ?? field.name | camelToWords\" [value]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [format]=\"field.format ?? 'text'\" [suffix]=\"field.suffix\" [size]=\"field.size\"></spa-label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [infoMessage]=\"field.infoMessage\" [hint]=\"field.hint\" ></spa-check><!-- Changed: field.hint was dropped on the floor for checkboxes only \u2014 every other type already passed it through -->\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [min]=\"field?.min\" [max]=\"field?.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [min]=\"field.min\" [max]=\"field.max\" [infoMessage]=\"field.infoMessage\" ></spa-datetime>\n \n <spa-email *ngSwitchCase=\"'email'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-email>\n \n <spa-text-mask *ngSwitchCase=\"'text-mask'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\"></spa-text-mask>\n \n \n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: field,\n field: field,\n data: data,\n testReadOnly: testReadOnly.bind(this),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n \n <spa-select-multi *ngSwitchCase=\"'select-multi'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [selectAll]=\"field.selectAll\">\n </spa-select-multi>\n \n <spa-text-multi *ngSwitchCase=\"'text-multi'\" [strict]=\"field.strict\" [display]=\"field.alias ?? field.name | camelToWords\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\"></spa-text-multi>\n \n \n \n <ng-container *ngSwitchCase=\"'composite'\">\n <div class=\"composite-field-container\">\n <div class=\"composite-field-group\">\n <ng-container *ngFor=\"let subfield of getVisibleSubfields(field)\">\n <ng-container [ngSwitch]=\"subfield.type\">\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [infoMessage]=\"subfield.infoMessage\" [hint]=\"subfield.hint\" ></spa-check><!-- Changed: same hint pass-through as the top-level checkbox above -->\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [infoMessage]=\"subfield.infoMessage\" ></spa-datetime>\n \n <!-- Fixed: composite select subfields projected the PARENT composite field into the\n dynamic select template (wrong options/value binding) and omitted testRequired,\n so the template's testRequired(field) call threw and killed the whole dialog.\n Context now mirrors the top-level select outlet, with the SUBFIELD, and readonly\n cascades composite parent || subfield like every other subfield type. -->\n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: subfield,\n field: subfield,\n data: data,\n testReadOnly: compositeReadOnly(field),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [options]=\"subfield.options\" [optionDisplay]=\"subfield.optionDisplay ?? 'name'\" [optionValue]=\"subfield.optionValue ?? 'value'\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [loadAction]=\"resolveLoadAction(subfield)\" [regex]=\"subfield.regex\" [field]=\"subfield\" [data]=\"data\" [detailsConfig]=\"subfield.detailsConfig\" [masterField]=\"subfield.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [rows]=\"subfield.rows\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [editorConfig]=\"subfield.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text>\n \n \n </ng-container>\n </ng-container>\n </div>\n </div>\n </ng-container>\n \n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [regex]=\"field.regex\" [field]=\"field\" [data]=\"data\" [detailsConfig]=\"field.detailsConfig\" [masterField]=\"field.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [rows]=\"field.rows\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [editorConfig]=\"field.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text>\n \n </ng-container>\n \n </ng-container>\n \n </div>\n \n \n <div class=\"span-col-center\" *ngIf=\"config.button\">\n <button mat-raised-button color=\"primary\" (click)=\"buttonClicked()\" cdkFocusInitial>{{buttonDisplay}}</button>\n </div>\n \n \n </div>\n <!-- Fixed: built-in fallback select template \u2014 spa-form used consumers' projected #dynamicSelect\n (only detailsDialog provides one), so selects silently rendered NOTHING when spa-form was used\n directly. The projected template (if any) still wins; this default carries the same bindings. -->\n <ng-template #defaultDynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\" let-resolveLoadAction=\"resolveLoadAction\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"resolveLoadAction(field)\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n\n <div class=\"notes-section\" *ngIf=\"config.notesConfig\">\n <spa-notes\n [title]=\"config.notesConfig.title || 'Notes'\"\n [notes]=\"config.notesConfig.notes || []\"\n [loadAction]=\"config.notesConfig.loadAction\"\n [loadIDField]=\"config.notesConfig.loadIDField\"\n [data]=\"data\"\n [nameField]=\"config.notesConfig.nameField || 'createdByName'\"\n [dateField]=\"config.notesConfig.dateField || 'createdDate'\"\n [commentField]=\"config.notesConfig.commentField || 'details'\">\n </spa-notes>\n </div>\n</div>\n\n", styles: [".title{margin-top:.5em;margin-bottom:.5em;font-size:larger;font-weight:300;color:#0b447e}.composite-field-group{display:flex;flex-direction:row;flex-wrap:wrap;gap:12px}.tin-form-container{display:flex;flex-direction:row;width:100%;gap:16px}.width-100{width:100%!important}.width-75{width:70%!important}.notes-section{width:400px;border-left:1px solid #e0e0e0}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i1$2.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1$2.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1$2.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextMaskComponent, selector: "spa-text-mask", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "regex", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextAreaComponent, selector: "spa-text-area", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "rows", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextSingleComponent, selector: "spa-text-single", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "width", "copyContent", "clearContent", "options", "optionDisplay", "optionValue", "loadAction", "required", "min", "max", "regex", "suffix", "infoMessage", "field", "data", "detailsConfig", "masterField"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage", "hint"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: DateComponent, selector: "spa-date", inputs: ["required", "min", "max", "readonly", "hint", "value", "display", "placeholder", "width", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: DatetimeComponent, selector: "spa-datetime", inputs: ["display", "value", "readonly", "width", "min", "max", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: LabelComponent, selector: "spa-label", inputs: ["display", "value", "format", "suffix", "size"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: MoneyComponent, selector: "spa-money", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "currency", "required", "min", "max", "infoMessage", "copyContent", "clearContent", "suffix"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: AttachComponent, selector: "spa-attach", inputs: ["fileOptions", "message", "files", "enableUpload"], outputs: ["filesChange", "upload"] }, { kind: "component", type: NumberComponent, selector: "spa-number", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "step", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: ViewerComponent, selector: "spa-viewer", inputs: ["fileAction", "path", "folderName", "fileNames", "removable", "display", "title"], outputs: ["remove"] }, { kind: "component", type: EmailComponent, selector: "spa-email", inputs: ["display", "value", "readonly", "required", "hint", "suffix", "infoMessage", "copyContent", "clearContent", "options", "optionValue"], outputs: ["valueChange"] }, { kind: "component", type: TextMultiComponent, selector: "spa-text-multi", inputs: ["display", "value", "readonly", "required", "hint", "strict", "suffix", "infoMessage", "copyContent", "clearContent", "options", "optionDisplay", "optionValue", "loadAction"], outputs: ["valueChange", "hoverChange"] }, { kind: "component", type: SelectMultiComponent, selector: "spa-select-multi", inputs: ["display", "value", "readonly", "required", "hint", "options", "optionDisplay", "optionValue", "infoMessage", "copyContent", "clearContent", "nullable", "placeholder", "width", "suffix", "loadAction", "selectAll"], outputs: ["valueChange", "hoverChange"] }, { kind: "component", type: HtmlComponent, selector: "spa-html", inputs: ["value", "maxHeight", "display"] }, { kind: "component", type: EditorComponent, selector: "spa-editor", inputs: ["display", "value", "readonly", "required", "hint", "infoMessage", "placeholder", "width", "height", "minHeight", "defaultFontName", "editorConfig"], outputs: ["valueChange"] }, { kind: "component", type: NotesComponent, selector: "spa-notes", inputs: ["title", "notes", "loadAction", "loadIDField", "data", "nameField", "dateField", "commentField"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
|
|
19727
20151
|
}
|
|
19728
20152
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: FormComponent, decorators: [{
|
|
19729
20153
|
type: Component,
|
|
19730
|
-
args: [{ selector: 'spa-form', standalone: false, template: "\n\n\n<div class=\"tin-form-container\" >\n <div [ngClass]=\"[multiColumn ? 'tin-grid' : 'tin-col', config.notesConfig ? 'width-75' : 'width-100']\" class=\"form-main-content\">\n\n <div *ngIf=\"!hasAccess\" class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n \n <div [ngClass]=\"field.span || field.type =='section' || field.type =='file' || field.type =='file-view' || field.type =='editor' ? 'span-col' : ''\" *ngFor=\"let field of visibleFields\"><!-- TS-11: bind cached visibleFields instead of getVisibleFields() per CD -->\n \n <ng-container>\n \n <ng-container [ngSwitch]=\"field.type\" class=\"highlight\">\n \n <div *ngSwitchCase=\"'section'\" class=\"title d-flex align-items-center\" (click)=\"toggleSection(field)\" style=\"cursor: pointer;\">\n <label style=\"font-size: larger;margin-right: 10px;\">{{field.alias ?? field.name | camelToWords}}</label>\n <mat-icon *ngIf=\"field.infoMessage\" (click)=\"onInfoClick($event, field.infoMessage)\" style=\"color: steelblue; font-size: 14px;\">info</mat-icon>\n <!-- <button mat-icon-button class=\"info-icon-button\" matTooltip=\"Info\" matTooltipPosition=\"above\">\n \n </button> -->\n <mat-icon *ngIf=\"hasSectionFields(field.name)\">{{shouldSectionCollapse(field) ? 'expand_more' : 'expand_less'}}</mat-icon>\n </div>\n \n <ng-container *ngSwitchCase=\"'file'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode !='view'\">\n <spa-attach [message]=\"field.alias ?? 'Drag and Drop files here'\" [(files)]=\"files\" [fileOptions]=\"field.fileOptions\"></spa-attach>\n </div>\n </ng-container>\n \n <ng-container *ngSwitchCase=\"'file-view'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode && config.mode !='create'\">\n <spa-viewer [fileAction]=\"field.loadAction\" [path]=\"field.path\" [folderName]=\"data[field.keyField]\" ></spa-viewer>\n </div>\n </ng-container>\n \n <spa-html *ngSwitchCase=\"'html'\" [display]=\"field.alias | camelToWords\" [value]=\"data[field.name]\" [maxHeight]=\"field.maxHeight\"></spa-html>\n \n <label *ngSwitchCase=\"'blank'\"></label>\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-label *ngSwitchCase=\"'label'\" [display]=\"field.alias ?? field.name | camelToWords\" [value]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [format]=\"field.format ?? 'text'\" [suffix]=\"field.suffix\" [size]=\"field.size\"></spa-label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [infoMessage]=\"field.infoMessage\" ></spa-check>\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [min]=\"field?.min\" [max]=\"field?.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [min]=\"field.min\" [max]=\"field.max\" [infoMessage]=\"field.infoMessage\" ></spa-datetime>\n \n <spa-email *ngSwitchCase=\"'email'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-email>\n \n <spa-text-mask *ngSwitchCase=\"'text-mask'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\"></spa-text-mask>\n \n \n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: field,\n field: field,\n data: data,\n testReadOnly: testReadOnly.bind(this),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n \n <spa-select-multi *ngSwitchCase=\"'select-multi'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [selectAll]=\"field.selectAll\">\n </spa-select-multi>\n \n <spa-text-multi *ngSwitchCase=\"'text-multi'\" [strict]=\"field.strict\" [display]=\"field.alias ?? field.name | camelToWords\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\"></spa-text-multi>\n \n \n \n <ng-container *ngSwitchCase=\"'composite'\">\n <div class=\"composite-field-container\">\n <div class=\"composite-field-group\">\n <ng-container *ngFor=\"let subfield of getVisibleSubfields(field)\">\n <ng-container [ngSwitch]=\"subfield.type\">\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [infoMessage]=\"subfield.infoMessage\" ></spa-check>\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [infoMessage]=\"subfield.infoMessage\" ></spa-datetime>\n \n <!-- Fixed: composite select subfields projected the PARENT composite field into the\n dynamic select template (wrong options/value binding) and omitted testRequired,\n so the template's testRequired(field) call threw and killed the whole dialog.\n Context now mirrors the top-level select outlet, with the SUBFIELD, and readonly\n cascades composite parent || subfield like every other subfield type. -->\n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: subfield,\n field: subfield,\n data: data,\n testReadOnly: compositeReadOnly(field),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [options]=\"subfield.options\" [optionDisplay]=\"subfield.optionDisplay ?? 'name'\" [optionValue]=\"subfield.optionValue ?? 'value'\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [loadAction]=\"resolveLoadAction(subfield)\" [regex]=\"subfield.regex\" [field]=\"subfield\" [data]=\"data\" [detailsConfig]=\"subfield.detailsConfig\" [masterField]=\"subfield.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [rows]=\"subfield.rows\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [editorConfig]=\"subfield.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text>\n \n \n </ng-container>\n </ng-container>\n </div>\n </div>\n </ng-container>\n \n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [regex]=\"field.regex\" [field]=\"field\" [data]=\"data\" [detailsConfig]=\"field.detailsConfig\" [masterField]=\"field.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [rows]=\"field.rows\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [editorConfig]=\"field.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text>\n \n </ng-container>\n \n </ng-container>\n \n </div>\n \n \n <div class=\"span-col-center\" *ngIf=\"config.button\">\n <button mat-raised-button color=\"primary\" (click)=\"buttonClicked()\" cdkFocusInitial>{{buttonDisplay}}</button>\n </div>\n \n \n </div>\n <!-- Fixed: built-in fallback select template \u2014 spa-form used consumers' projected #dynamicSelect\n (only detailsDialog provides one), so selects silently rendered NOTHING when spa-form was used\n directly. The projected template (if any) still wins; this default carries the same bindings. -->\n <ng-template #defaultDynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\" let-resolveLoadAction=\"resolveLoadAction\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"resolveLoadAction(field)\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n\n <div class=\"notes-section\" *ngIf=\"config.notesConfig\">\n <spa-notes\n [title]=\"config.notesConfig.title || 'Notes'\"\n [notes]=\"config.notesConfig.notes || []\"\n [loadAction]=\"config.notesConfig.loadAction\"\n [loadIDField]=\"config.notesConfig.loadIDField\"\n [data]=\"data\"\n [nameField]=\"config.notesConfig.nameField || 'createdByName'\"\n [dateField]=\"config.notesConfig.dateField || 'createdDate'\"\n [commentField]=\"config.notesConfig.commentField || 'details'\">\n </spa-notes>\n </div>\n</div>\n\n", styles: [".title{margin-top:.5em;margin-bottom:.5em;font-size:larger;font-weight:300;color:#0b447e}.composite-field-group{display:flex;flex-direction:row;flex-wrap:wrap;gap:12px}.tin-form-container{display:flex;flex-direction:row;width:100%;gap:16px}.width-100{width:100%!important}.width-75{width:70%!important}.notes-section{width:400px;border-left:1px solid #e0e0e0}\n"] }]
|
|
20154
|
+
args: [{ selector: 'spa-form', standalone: false, template: "\n\n\n<div class=\"tin-form-container\" >\n <div [ngClass]=\"[multiColumn ? 'tin-grid' : 'tin-col', config.notesConfig ? 'width-75' : 'width-100']\" class=\"form-main-content\">\n\n <div *ngIf=\"!hasAccess\" class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n \n <div [ngClass]=\"field.span || field.type =='section' || field.type =='file' || field.type =='file-view' || field.type =='editor' ? 'span-col' : ''\" *ngFor=\"let field of visibleFields\"><!-- TS-11: bind cached visibleFields instead of getVisibleFields() per CD -->\n \n <ng-container>\n \n <ng-container [ngSwitch]=\"field.type\" class=\"highlight\">\n \n <div *ngSwitchCase=\"'section'\" class=\"title d-flex align-items-center\" (click)=\"toggleSection(field)\" style=\"cursor: pointer;\">\n <label style=\"font-size: larger;margin-right: 10px;\">{{field.alias ?? field.name | camelToWords}}</label>\n <mat-icon *ngIf=\"field.infoMessage\" (click)=\"onInfoClick($event, field.infoMessage)\" style=\"color: steelblue; font-size: 14px;\">info</mat-icon>\n <!-- <button mat-icon-button class=\"info-icon-button\" matTooltip=\"Info\" matTooltipPosition=\"above\">\n \n </button> -->\n <mat-icon *ngIf=\"hasSectionFields(field.name)\">{{shouldSectionCollapse(field) ? 'expand_more' : 'expand_less'}}</mat-icon>\n </div>\n \n <ng-container *ngSwitchCase=\"'file'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode !='view'\">\n <spa-attach [message]=\"field.alias ?? 'Drag and Drop files here'\" [(files)]=\"files\" [fileOptions]=\"field.fileOptions\"></spa-attach>\n </div>\n </ng-container>\n \n <ng-container *ngSwitchCase=\"'file-view'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode && config.mode !='create'\">\n <spa-viewer [fileAction]=\"field.loadAction\" [path]=\"field.path\" [folderName]=\"data[field.keyField]\" ></spa-viewer>\n </div>\n </ng-container>\n \n <spa-html *ngSwitchCase=\"'html'\" [display]=\"field.alias | camelToWords\" [value]=\"data[field.name]\" [maxHeight]=\"field.maxHeight\"></spa-html>\n \n <label *ngSwitchCase=\"'blank'\"></label>\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-label *ngSwitchCase=\"'label'\" [display]=\"field.alias ?? field.name | camelToWords\" [value]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [format]=\"field.format ?? 'text'\" [suffix]=\"field.suffix\" [size]=\"field.size\"></spa-label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [infoMessage]=\"field.infoMessage\" [hint]=\"field.hint\" ></spa-check><!-- Changed: field.hint was dropped on the floor for checkboxes only \u2014 every other type already passed it through -->\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [min]=\"field?.min\" [max]=\"field?.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [min]=\"field.min\" [max]=\"field.max\" [infoMessage]=\"field.infoMessage\" ></spa-datetime>\n \n <spa-email *ngSwitchCase=\"'email'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-email>\n \n <spa-text-mask *ngSwitchCase=\"'text-mask'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\"></spa-text-mask>\n \n \n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: field,\n field: field,\n data: data,\n testReadOnly: testReadOnly.bind(this),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n \n <spa-select-multi *ngSwitchCase=\"'select-multi'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [selectAll]=\"field.selectAll\">\n </spa-select-multi>\n \n <spa-text-multi *ngSwitchCase=\"'text-multi'\" [strict]=\"field.strict\" [display]=\"field.alias ?? field.name | camelToWords\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\"></spa-text-multi>\n \n \n \n <ng-container *ngSwitchCase=\"'composite'\">\n <div class=\"composite-field-container\">\n <div class=\"composite-field-group\">\n <ng-container *ngFor=\"let subfield of getVisibleSubfields(field)\">\n <ng-container [ngSwitch]=\"subfield.type\">\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [infoMessage]=\"subfield.infoMessage\" [hint]=\"subfield.hint\" ></spa-check><!-- Changed: same hint pass-through as the top-level checkbox above -->\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [infoMessage]=\"subfield.infoMessage\" ></spa-datetime>\n \n <!-- Fixed: composite select subfields projected the PARENT composite field into the\n dynamic select template (wrong options/value binding) and omitted testRequired,\n so the template's testRequired(field) call threw and killed the whole dialog.\n Context now mirrors the top-level select outlet, with the SUBFIELD, and readonly\n cascades composite parent || subfield like every other subfield type. -->\n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: subfield,\n field: subfield,\n data: data,\n testReadOnly: compositeReadOnly(field),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [options]=\"subfield.options\" [optionDisplay]=\"subfield.optionDisplay ?? 'name'\" [optionValue]=\"subfield.optionValue ?? 'value'\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [loadAction]=\"resolveLoadAction(subfield)\" [regex]=\"subfield.regex\" [field]=\"subfield\" [data]=\"data\" [detailsConfig]=\"subfield.detailsConfig\" [masterField]=\"subfield.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [rows]=\"subfield.rows\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [editorConfig]=\"subfield.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text>\n \n \n </ng-container>\n </ng-container>\n </div>\n </div>\n </ng-container>\n \n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [regex]=\"field.regex\" [field]=\"field\" [data]=\"data\" [detailsConfig]=\"field.detailsConfig\" [masterField]=\"field.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [rows]=\"field.rows\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [editorConfig]=\"field.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text>\n \n </ng-container>\n \n </ng-container>\n \n </div>\n \n \n <div class=\"span-col-center\" *ngIf=\"config.button\">\n <button mat-raised-button color=\"primary\" (click)=\"buttonClicked()\" cdkFocusInitial>{{buttonDisplay}}</button>\n </div>\n \n \n </div>\n <!-- Fixed: built-in fallback select template \u2014 spa-form used consumers' projected #dynamicSelect\n (only detailsDialog provides one), so selects silently rendered NOTHING when spa-form was used\n directly. The projected template (if any) still wins; this default carries the same bindings. -->\n <ng-template #defaultDynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\" let-resolveLoadAction=\"resolveLoadAction\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"resolveLoadAction(field)\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n\n <div class=\"notes-section\" *ngIf=\"config.notesConfig\">\n <spa-notes\n [title]=\"config.notesConfig.title || 'Notes'\"\n [notes]=\"config.notesConfig.notes || []\"\n [loadAction]=\"config.notesConfig.loadAction\"\n [loadIDField]=\"config.notesConfig.loadIDField\"\n [data]=\"data\"\n [nameField]=\"config.notesConfig.nameField || 'createdByName'\"\n [dateField]=\"config.notesConfig.dateField || 'createdDate'\"\n [commentField]=\"config.notesConfig.commentField || 'details'\">\n </spa-notes>\n </div>\n</div>\n\n", styles: [".title{margin-top:.5em;margin-bottom:.5em;font-size:larger;font-weight:300;color:#0b447e}.composite-field-group{display:flex;flex-direction:row;flex-wrap:wrap;gap:12px}.tin-form-container{display:flex;flex-direction:row;width:100%;gap:16px}.width-100{width:100%!important}.width-75{width:70%!important}.notes-section{width:400px;border-left:1px solid #e0e0e0}\n"] }]
|
|
19731
20155
|
}], ctorParameters: () => [{ type: MessageService }, { type: DataServiceLib }, { type: AuthService }, { type: ApiErrorService }], propDecorators: { dynamicSelectTemplate: [{
|
|
19732
20156
|
type: ContentChild,
|
|
19733
20157
|
args: ['dynamicSelect']
|
|
@@ -19768,6 +20192,11 @@ class AppConfigurationComponent {
|
|
|
19768
20192
|
this.loading = false;
|
|
19769
20193
|
this.saving = false;
|
|
19770
20194
|
this.canEdit = true;
|
|
20195
|
+
// Added: the page's own record that the last save was REFUSED. Without it the only evidence was the 5s toast
|
|
20196
|
+
// (a business refusal) or a dialog the user dismisses (a technical failure) — after either one vanished, an
|
|
20197
|
+
// unsaved page looked exactly like a saved one while still displaying the value the server rejected. Held
|
|
20198
|
+
// until a save actually succeeds, NOT cleared on edit: editing does not make the shown values in force.
|
|
20199
|
+
this.saveFailure = null;
|
|
19771
20200
|
// Materialized, never a template-bound getter — a getter returning a new array here would hand the
|
|
19772
20201
|
// *ngFor a fresh reference on every change-detection pass (see the day-book/table livelock).
|
|
19773
20202
|
this.cards = [];
|
|
@@ -19847,6 +20276,14 @@ class AppConfigurationComponent {
|
|
|
19847
20276
|
onInputChange() {
|
|
19848
20277
|
this.buildVisibleCards();
|
|
19849
20278
|
}
|
|
20279
|
+
// Added: the escape hatch for the banner. Reverting the form automatically on a refusal was rejected — it
|
|
20280
|
+
// throws away every OTHER edit on a page that saves as one object, and on a business refusal it also destroys
|
|
20281
|
+
// the values the user needs in front of them to act on the message. Offered as a deliberate press instead, so
|
|
20282
|
+
// "show me what is actually saved" is one click away without being forced on anyone.
|
|
20283
|
+
discardChanges() {
|
|
20284
|
+
this.saveFailure = null;
|
|
20285
|
+
this.load();
|
|
20286
|
+
}
|
|
19850
20287
|
//---------- Save ----------
|
|
19851
20288
|
save() {
|
|
19852
20289
|
if (this.saving || !this.canEdit)
|
|
@@ -19856,6 +20293,7 @@ class AppConfigurationComponent {
|
|
|
19856
20293
|
next: (response) => {
|
|
19857
20294
|
this.saving = false;
|
|
19858
20295
|
if (response.success) {
|
|
20296
|
+
this.saveFailure = null; // Added: the values on screen are now the values in force, so the banner must go
|
|
19859
20297
|
this.messageService.toast('Configuration updated successfully');
|
|
19860
20298
|
this.load(); // re-read so computed/server-stamped values (and a first-read created row's key) come back
|
|
19861
20299
|
this.configService.refresh(); // every app-side subscriber (nav gating, unit suffixes) sees the change now, not next login
|
|
@@ -19864,18 +20302,35 @@ class AppConfigurationComponent {
|
|
|
19864
20302
|
// Changed: was toast(response.message ...) — the server's raw words for 5 seconds while the page
|
|
19865
20303
|
// still showed every edited value, so it looked saved. 'submit' is exactly right and literally
|
|
19866
20304
|
// true here: the user's configuration edits ARE still on the form and do not need re-entering.
|
|
19867
|
-
this.apiErrorService.presentAppFailure(response, 'submit', (this.config?.saveAction || { url: 'configuration/update' }).url);
|
|
20305
|
+
const failureClass = this.apiErrorService.presentAppFailure(response, 'submit', (this.config?.saveAction || { url: 'configuration/update' }).url); // Changed: capture the class — it already distinguishes a deliberate refusal from a technical failure, so the banner does not have to re-derive it
|
|
20306
|
+
// Added: this page is a PERSISTENT page, not a dialog. Everywhere else in the library a failed submit
|
|
20307
|
+
// leaves a modal open, and the open modal is itself the visible "not saved" signal. Here there is no
|
|
20308
|
+
// modal: once the toast expires the page looks settled while showing values the server refused and the
|
|
20309
|
+
// database does not hold. The banner is that missing signal, and unlike the toast it does not expire.
|
|
20310
|
+
this.saveFailure = {
|
|
20311
|
+
reason: failureClass === 'business'
|
|
20312
|
+
? this.apiErrorService.stripReference(response.message) // the server's own sentence names what to fix; stripReference drops the "(reference X)" suffix exactly as the toast does
|
|
20313
|
+
: 'A technical problem stopped the save.',
|
|
20314
|
+
technical: failureClass !== 'business'
|
|
20315
|
+
};
|
|
19868
20316
|
}
|
|
19869
20317
|
},
|
|
19870
|
-
error: () => this.saving = false
|
|
20318
|
+
// Changed: was `error: () => this.saving = false` — a TRANSPORT failure (server down, DNS, CORS, dropped
|
|
20319
|
+
// connection) never reaches the next() handler above, so this page said nothing at all and simply went
|
|
20320
|
+
// un-busy, which is the most convincing "saved" signal of the lot. The interceptor's own network dialog is
|
|
20321
|
+
// dismissible and page-agnostic; the banner is what keeps the statement attached to the values on screen.
|
|
20322
|
+
error: () => {
|
|
20323
|
+
this.saving = false;
|
|
20324
|
+
this.saveFailure = { reason: 'A technical problem stopped the save.', technical: true };
|
|
20325
|
+
}
|
|
19871
20326
|
});
|
|
19872
20327
|
}
|
|
19873
20328
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AppConfigurationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
19874
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: AppConfigurationComponent, isStandalone: false, selector: "spa-app-configuration", ngImport: i0, template: "<div class=\"ac-page\">\n\n <div class=\"ac-head\">\n <h1>{{ title }}</h1>\n <span class=\"ac-caption\" *ngIf=\"!canEdit\">Read only \u2014 you do not have permission to change these settings</span>\n <!-- Added (Setup v3): this page used to carry its own module checkboxes, which wrote the same flags as the\n Getting Started picker but skipped its capability check, its \"modules confirmed\" step and its badge\n refresh. One write path now \u2014 the page keeps the settings, the picker owns what is switched on. -->\n <a class=\"ac-modules-link\" (click)=\"goToModules()\"><mat-icon>tune</mat-icon>Modules are chosen in Getting Started</a>\n </div>\n\n <!-- One card per section. The header carries the title, the optional module switch and the collapse chevron -->\n <mat-card class=\"ac-section\" *ngFor=\"let card of visibleCards\" [id]=\"'cfg-' + card.section.key\">\n\n <div class=\"ac-section-head\" [class.clickable]=\"card.section.collapsible !== false\" (click)=\"toggle(card)\">\n <mat-icon class=\"ac-section-icon\" *ngIf=\"card.section.icon\">{{ card.section.icon }}</mat-icon>\n\n <!-- toggleField sections put the module switch IN the header; children below go readonly while it is off -->\n <spa-check *ngIf=\"card.section.toggleField\" class=\"ac-section-switch\" [display]=\"card.section.title\" [(value)]=\"configuration[card.section.toggleField]\" (valueChange)=\"onInputChange()\" [readonly]=\"!canEdit\" (click)=\"$event.stopPropagation()\"></spa-check>\n <span class=\"ac-section-title\" *ngIf=\"!card.section.toggleField\">{{ card.section.title }}</span>\n\n <mat-icon class=\"ac-section-chevron\" *ngIf=\"card.section.collapsible !== false\">{{ card.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </div>\n\n <div class=\"ac-section-hint\" *ngIf=\"card.section.hint && !card.collapsed\">{{ card.section.hint }}</div>\n\n <div class=\"ac-section-body\" *ngIf=\"!card.collapsed\">\n <spa-form [config]=\"card.form\" [data]=\"configuration\" (inputChange)=\"onInputChange()\"></spa-form>\n </div>\n\n </mat-card>\n\n <!-- One Save for the whole page \u2014 the object posts as a unit, so the sections can never drift apart -->\n <div class=\"ac-
|
|
20329
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: AppConfigurationComponent, isStandalone: false, selector: "spa-app-configuration", ngImport: i0, template: "<div class=\"ac-page\">\n\n <div class=\"ac-head\">\n <h1>{{ title }}</h1>\n <span class=\"ac-caption\" *ngIf=\"!canEdit\">Read only \u2014 you do not have permission to change these settings</span>\n <!-- Added (Setup v3): this page used to carry its own module checkboxes, which wrote the same flags as the\n Getting Started picker but skipped its capability check, its \"modules confirmed\" step and its badge\n refresh. One write path now \u2014 the page keeps the settings, the picker owns what is switched on. -->\n <a class=\"ac-modules-link\" (click)=\"goToModules()\"><mat-icon>tune</mat-icon>Modules are chosen in Getting Started</a>\n </div>\n\n <!-- One card per section. The header carries the title, the optional module switch and the collapse chevron -->\n <mat-card class=\"ac-section\" *ngFor=\"let card of visibleCards\" [id]=\"'cfg-' + card.section.key\">\n\n <div class=\"ac-section-head\" [class.clickable]=\"card.section.collapsible !== false\" (click)=\"toggle(card)\">\n <mat-icon class=\"ac-section-icon\" *ngIf=\"card.section.icon\">{{ card.section.icon }}</mat-icon>\n\n <!-- toggleField sections put the module switch IN the header; children below go readonly while it is off -->\n <spa-check *ngIf=\"card.section.toggleField\" class=\"ac-section-switch\" [display]=\"card.section.title\" [(value)]=\"configuration[card.section.toggleField]\" (valueChange)=\"onInputChange()\" [readonly]=\"!canEdit\" (click)=\"$event.stopPropagation()\"></spa-check>\n <span class=\"ac-section-title\" *ngIf=\"!card.section.toggleField\">{{ card.section.title }}</span>\n\n <mat-icon class=\"ac-section-chevron\" *ngIf=\"card.section.collapsible !== false\">{{ card.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </div>\n\n <div class=\"ac-section-hint\" *ngIf=\"card.section.hint && !card.collapsed\">{{ card.section.hint }}</div>\n\n <div class=\"ac-section-body\" *ngIf=\"!card.collapsed\">\n <spa-form [config]=\"card.form\" [data]=\"configuration\" (inputChange)=\"onInputChange()\"></spa-form>\n </div>\n\n </mat-card>\n\n <!-- One Save for the whole page \u2014 the object posts as a unit, so the sections can never drift apart.\n The sticky footer now also carries the refusal banner, so the \"not saved\" statement is on screen at every\n scroll position rather than only for the 5 seconds the toast lives. -->\n <div class=\"ac-footer\" *ngIf=\"canEdit && visibleCards.length\">\n\n <!-- Added: persists until a save succeeds. role=alert so it is announced, not just drawn -->\n <div class=\"ac-save-alert\" role=\"alert\" *ngIf=\"saveFailure\">\n <mat-icon class=\"ac-save-alert-icon\">report_problem</mat-icon>\n <div class=\"ac-save-alert-text\">\n <strong>Not saved{{ saveFailure.technical ? '' : ' \u2014 ' + saveFailure.reason }}</strong>\n <span *ngIf=\"saveFailure.technical\">{{ saveFailure.reason }}</span>\n <span>The settings shown on this page are <b>not in force</b>. Your saved configuration is unchanged.</span>\n </div>\n <button mat-stroked-button class=\"ac-save-alert-discard\" (click)=\"discardChanges()\">Discard my changes</button>\n </div>\n\n <div class=\"ac-actions\">\n <button mat-raised-button color=\"primary\" [disabled]=\"saving\" (click)=\"save()\">Save Configuration</button>\n </div>\n\n </div>\n\n</div>\n", styles: [".ac-page{max-width:1100px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.ac-head{display:flex;align-items:baseline;gap:16px;flex-wrap:wrap}.ac-head h1{margin:0;font-size:24px}.ac-caption{font-size:12px;color:#0000008c}.ac-modules-link{display:inline-flex;align-items:center;gap:4px;margin-left:auto;font-size:12px;color:#1976d2;cursor:pointer}.ac-modules-link:hover{text-decoration:underline}.ac-modules-link mat-icon{font-size:16px;width:16px;height:16px}.ac-section{padding:12px 16px}.ac-section-head{display:flex;align-items:center;gap:10px}.ac-section-head.clickable{cursor:pointer}.ac-section-icon{color:#00000073}.ac-section-title{font-size:17px;font-weight:500}.ac-section-switch{flex:0 0 auto}.ac-section-chevron{margin-left:auto;color:#00000073}.ac-section-hint{font-size:12px;color:#0000008c;margin:2px 0 0 2px}.ac-section-body{margin-top:8px}.ac-footer{position:sticky;bottom:0;background:linear-gradient(to top,var(--tin-page-bg, #fafafa) 78%,transparent)}.ac-actions{display:flex;justify-content:flex-end;padding:12px 0}.ac-save-alert{display:flex;align-items:flex-start;gap:12px;padding:12px 14px;margin-top:12px;border:1px solid #f1a9a0;border-left:4px solid #c62828;border-radius:4px;background:#fdecea}.ac-save-alert-icon{flex:0 0 auto;color:#c62828}.ac-save-alert-text{display:flex;flex-direction:column;gap:4px;font-size:13px;line-height:1.45;color:#611a15;min-width:0}.ac-save-alert-text strong{font-size:14px}.ac-save-alert-discard{flex:0 0 auto;margin-left:auto;align-self:center}@media (max-width: 599px){.ac-save-alert{flex-wrap:wrap}.ac-save-alert-discard{margin-left:0;width:100%}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage", "hint"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: FormComponent, selector: "spa-form", inputs: ["files", "data", "config"], outputs: ["buttonClick", "inputChange"] }] }); }
|
|
19875
20330
|
}
|
|
19876
20331
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AppConfigurationComponent, decorators: [{
|
|
19877
20332
|
type: Component,
|
|
19878
|
-
args: [{ selector: 'spa-app-configuration', standalone: false, template: "<div class=\"ac-page\">\n\n <div class=\"ac-head\">\n <h1>{{ title }}</h1>\n <span class=\"ac-caption\" *ngIf=\"!canEdit\">Read only \u2014 you do not have permission to change these settings</span>\n <!-- Added (Setup v3): this page used to carry its own module checkboxes, which wrote the same flags as the\n Getting Started picker but skipped its capability check, its \"modules confirmed\" step and its badge\n refresh. One write path now \u2014 the page keeps the settings, the picker owns what is switched on. -->\n <a class=\"ac-modules-link\" (click)=\"goToModules()\"><mat-icon>tune</mat-icon>Modules are chosen in Getting Started</a>\n </div>\n\n <!-- One card per section. The header carries the title, the optional module switch and the collapse chevron -->\n <mat-card class=\"ac-section\" *ngFor=\"let card of visibleCards\" [id]=\"'cfg-' + card.section.key\">\n\n <div class=\"ac-section-head\" [class.clickable]=\"card.section.collapsible !== false\" (click)=\"toggle(card)\">\n <mat-icon class=\"ac-section-icon\" *ngIf=\"card.section.icon\">{{ card.section.icon }}</mat-icon>\n\n <!-- toggleField sections put the module switch IN the header; children below go readonly while it is off -->\n <spa-check *ngIf=\"card.section.toggleField\" class=\"ac-section-switch\" [display]=\"card.section.title\" [(value)]=\"configuration[card.section.toggleField]\" (valueChange)=\"onInputChange()\" [readonly]=\"!canEdit\" (click)=\"$event.stopPropagation()\"></spa-check>\n <span class=\"ac-section-title\" *ngIf=\"!card.section.toggleField\">{{ card.section.title }}</span>\n\n <mat-icon class=\"ac-section-chevron\" *ngIf=\"card.section.collapsible !== false\">{{ card.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </div>\n\n <div class=\"ac-section-hint\" *ngIf=\"card.section.hint && !card.collapsed\">{{ card.section.hint }}</div>\n\n <div class=\"ac-section-body\" *ngIf=\"!card.collapsed\">\n <spa-form [config]=\"card.form\" [data]=\"configuration\" (inputChange)=\"onInputChange()\"></spa-form>\n </div>\n\n </mat-card>\n\n <!-- One Save for the whole page \u2014 the object posts as a unit, so the sections can never drift apart -->\n <div class=\"ac-
|
|
20333
|
+
args: [{ selector: 'spa-app-configuration', standalone: false, template: "<div class=\"ac-page\">\n\n <div class=\"ac-head\">\n <h1>{{ title }}</h1>\n <span class=\"ac-caption\" *ngIf=\"!canEdit\">Read only \u2014 you do not have permission to change these settings</span>\n <!-- Added (Setup v3): this page used to carry its own module checkboxes, which wrote the same flags as the\n Getting Started picker but skipped its capability check, its \"modules confirmed\" step and its badge\n refresh. One write path now \u2014 the page keeps the settings, the picker owns what is switched on. -->\n <a class=\"ac-modules-link\" (click)=\"goToModules()\"><mat-icon>tune</mat-icon>Modules are chosen in Getting Started</a>\n </div>\n\n <!-- One card per section. The header carries the title, the optional module switch and the collapse chevron -->\n <mat-card class=\"ac-section\" *ngFor=\"let card of visibleCards\" [id]=\"'cfg-' + card.section.key\">\n\n <div class=\"ac-section-head\" [class.clickable]=\"card.section.collapsible !== false\" (click)=\"toggle(card)\">\n <mat-icon class=\"ac-section-icon\" *ngIf=\"card.section.icon\">{{ card.section.icon }}</mat-icon>\n\n <!-- toggleField sections put the module switch IN the header; children below go readonly while it is off -->\n <spa-check *ngIf=\"card.section.toggleField\" class=\"ac-section-switch\" [display]=\"card.section.title\" [(value)]=\"configuration[card.section.toggleField]\" (valueChange)=\"onInputChange()\" [readonly]=\"!canEdit\" (click)=\"$event.stopPropagation()\"></spa-check>\n <span class=\"ac-section-title\" *ngIf=\"!card.section.toggleField\">{{ card.section.title }}</span>\n\n <mat-icon class=\"ac-section-chevron\" *ngIf=\"card.section.collapsible !== false\">{{ card.collapsed ? 'expand_more' : 'expand_less' }}</mat-icon>\n </div>\n\n <div class=\"ac-section-hint\" *ngIf=\"card.section.hint && !card.collapsed\">{{ card.section.hint }}</div>\n\n <div class=\"ac-section-body\" *ngIf=\"!card.collapsed\">\n <spa-form [config]=\"card.form\" [data]=\"configuration\" (inputChange)=\"onInputChange()\"></spa-form>\n </div>\n\n </mat-card>\n\n <!-- One Save for the whole page \u2014 the object posts as a unit, so the sections can never drift apart.\n The sticky footer now also carries the refusal banner, so the \"not saved\" statement is on screen at every\n scroll position rather than only for the 5 seconds the toast lives. -->\n <div class=\"ac-footer\" *ngIf=\"canEdit && visibleCards.length\">\n\n <!-- Added: persists until a save succeeds. role=alert so it is announced, not just drawn -->\n <div class=\"ac-save-alert\" role=\"alert\" *ngIf=\"saveFailure\">\n <mat-icon class=\"ac-save-alert-icon\">report_problem</mat-icon>\n <div class=\"ac-save-alert-text\">\n <strong>Not saved{{ saveFailure.technical ? '' : ' \u2014 ' + saveFailure.reason }}</strong>\n <span *ngIf=\"saveFailure.technical\">{{ saveFailure.reason }}</span>\n <span>The settings shown on this page are <b>not in force</b>. Your saved configuration is unchanged.</span>\n </div>\n <button mat-stroked-button class=\"ac-save-alert-discard\" (click)=\"discardChanges()\">Discard my changes</button>\n </div>\n\n <div class=\"ac-actions\">\n <button mat-raised-button color=\"primary\" [disabled]=\"saving\" (click)=\"save()\">Save Configuration</button>\n </div>\n\n </div>\n\n</div>\n", styles: [".ac-page{max-width:1100px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.ac-head{display:flex;align-items:baseline;gap:16px;flex-wrap:wrap}.ac-head h1{margin:0;font-size:24px}.ac-caption{font-size:12px;color:#0000008c}.ac-modules-link{display:inline-flex;align-items:center;gap:4px;margin-left:auto;font-size:12px;color:#1976d2;cursor:pointer}.ac-modules-link:hover{text-decoration:underline}.ac-modules-link mat-icon{font-size:16px;width:16px;height:16px}.ac-section{padding:12px 16px}.ac-section-head{display:flex;align-items:center;gap:10px}.ac-section-head.clickable{cursor:pointer}.ac-section-icon{color:#00000073}.ac-section-title{font-size:17px;font-weight:500}.ac-section-switch{flex:0 0 auto}.ac-section-chevron{margin-left:auto;color:#00000073}.ac-section-hint{font-size:12px;color:#0000008c;margin:2px 0 0 2px}.ac-section-body{margin-top:8px}.ac-footer{position:sticky;bottom:0;background:linear-gradient(to top,var(--tin-page-bg, #fafafa) 78%,transparent)}.ac-actions{display:flex;justify-content:flex-end;padding:12px 0}.ac-save-alert{display:flex;align-items:flex-start;gap:12px;padding:12px 14px;margin-top:12px;border:1px solid #f1a9a0;border-left:4px solid #c62828;border-radius:4px;background:#fdecea}.ac-save-alert-icon{flex:0 0 auto;color:#c62828}.ac-save-alert-text{display:flex;flex-direction:column;gap:4px;font-size:13px;line-height:1.45;color:#611a15;min-width:0}.ac-save-alert-text strong{font-size:14px}.ac-save-alert-discard{flex:0 0 auto;margin-left:auto;align-self:center}@media (max-width: 599px){.ac-save-alert{flex-wrap:wrap}.ac-save-alert-discard{margin-left:0;width:100%}}\n"] }]
|
|
19879
20334
|
}] });
|
|
19880
20335
|
|
|
19881
20336
|
// Renders the agent's replies as the markdown they are actually written in.
|
|
@@ -21802,11 +22257,11 @@ class NavMenuComponent {
|
|
|
21802
22257
|
return !this.isMiniSidebar || this.isMiniHovered;
|
|
21803
22258
|
}
|
|
21804
22259
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: NavMenuComponent, deps: [{ token: i1$1.Router }, { token: AuthService }, { token: StorageService }, { token: NotificationsService }, { token: i1$4.BreakpointObserver }, { token: DataServiceLib }, { token: i4.MatDialog }, { token: SubscriptionService }, { token: SetupService }, { token: LastRouteService }, { token: OfflineService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
21805
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: NavMenuComponent, isStandalone: false, selector: "spa-nav-menu", inputs: { appConfig: "appConfig", footer: "footer" }, host: { listeners: { "window:scroll": "onWindowScroll()" } }, ngImport: i0, template: "<header *ngIf=\"loggedin && dataService.appConfig.navigation == 'top'\">\n\n <!-- Changed: Removed mb-3 class to eliminate gap between toolbar and content -->\n <nav class=\"toolbar navbar navbar-expand-sm navbar-toggleable-sm navbar-light border-bottom box-shadow\" style=\"padding-right: 10px;\">\n\n\n <div class=\"container-fluid\" style=\"padding-right: 0px;\">\n\n <img *ngIf=\"appConfig.logo!=''\" [src]=\"appConfig.logo\" style=\"height: 50px; margin-right: 2em\" />\n\n <div>\n <!-- <div style=\"font-size: 20px;\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px;\">\n {{tenantName}}\n </div> -->\n\n <div *ngIf=\"!dataService.appConfig.multitenant\" style=\"font-size: 22px;\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant\" style=\"font-size: 20px; ; font-weight: 400;\" [ngStyle]=\"{'margin-top': dataService.appConfig.multitenant ? '12px' : ''}\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px; margin-bottom: 5px;\">\n {{tenantName}}\n </div>\n\n </div>\n\n\n\n <button class=\"navbar-toggler\" type=\"button\" data-toggle=\"collapse\" data-target=\".navbar-collapse\" aria-label=\"Toggle navigation\" [attr.aria-expanded]=\"isExpanded\" (click)=\"toggle()\">\n <span class=\"navbar-toggler-icon\"></span>\n </button>\n\n <div *ngIf=\"myRole\" class=\" navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse stack-top\" style=\"margin-right: 0px;\" [ngClass]=\"{ show: isExpanded, navitems: isExpanded }\" >\n\n <button mat-icon-button (click)=\"logoff()\" > <mat-icon>logout</mat-icon> </button>\n\n <div *ngIf=\"dataService.appConfig.multitenant\">\n\n <button mat-icon-button (click)=\"redirectTo('home/tenancy/settings')\" > <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon> </button>\n\n <!-- Removed: Support icon \u2014 replaced by floating assistant chat widget -->\n </div>\n\n\n <button id=\"btnUser\" mat-button [matMenuTriggerFor]=\"profileMenu\" ><mat-icon style=\"font-size: 24px;\">account_circle</mat-icon> {{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" }] }); }
|
|
22260
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: NavMenuComponent, isStandalone: false, selector: "spa-nav-menu", inputs: { appConfig: "appConfig", footer: "footer" }, host: { listeners: { "window:scroll": "onWindowScroll()" } }, ngImport: i0, template: "<header *ngIf=\"loggedin && dataService.appConfig.navigation == 'top'\">\n\n <!-- Changed: Removed mb-3 class to eliminate gap between toolbar and content -->\n <nav class=\"toolbar navbar navbar-expand-sm navbar-toggleable-sm navbar-light border-bottom box-shadow\" style=\"padding-right: 10px;\">\n\n\n <div class=\"container-fluid\" style=\"padding-right: 0px;\">\n\n <img *ngIf=\"appConfig.logo!=''\" [src]=\"appConfig.logo\" style=\"height: 50px; margin-right: 2em\" />\n\n <div>\n <!-- <div style=\"font-size: 20px;\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px;\">\n {{tenantName}}\n </div> -->\n\n <div *ngIf=\"!dataService.appConfig.multitenant\" style=\"font-size: 22px;\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant\" style=\"font-size: 20px; ; font-weight: 400;\" [ngStyle]=\"{'margin-top': dataService.appConfig.multitenant ? '12px' : ''}\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px; margin-bottom: 5px;\">\n {{tenantName}}\n </div>\n\n </div>\n\n\n\n <button class=\"navbar-toggler\" type=\"button\" data-toggle=\"collapse\" data-target=\".navbar-collapse\" aria-label=\"Toggle navigation\" [attr.aria-expanded]=\"isExpanded\" (click)=\"toggle()\">\n <span class=\"navbar-toggler-icon\"></span>\n </button>\n\n <div *ngIf=\"myRole\" class=\" navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse stack-top\" style=\"margin-right: 0px;\" [ngClass]=\"{ show: isExpanded, navitems: isExpanded }\" >\n\n <button mat-icon-button (click)=\"logoff()\" > <mat-icon>logout</mat-icon> </button>\n\n <div *ngIf=\"dataService.appConfig.multitenant\">\n\n <button mat-icon-button (click)=\"redirectTo('home/tenancy/settings')\" > <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon> </button>\n\n <!-- Removed: Support icon \u2014 replaced by floating assistant chat widget -->\n </div>\n\n\n <button id=\"btnUser\" mat-button [matMenuTriggerFor]=\"profileMenu\" ><mat-icon style=\"font-size: 24px;\">account_circle</mat-icon> {{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
22261
|
}
|
|
21807
22262
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: NavMenuComponent, decorators: [{
|
|
21808
22263
|
type: Component,
|
|
21809
|
-
args: [{ selector: 'spa-nav-menu', standalone: false, template: "<header *ngIf=\"loggedin && dataService.appConfig.navigation == 'top'\">\n\n <!-- Changed: Removed mb-3 class to eliminate gap between toolbar and content -->\n <nav class=\"toolbar navbar navbar-expand-sm navbar-toggleable-sm navbar-light border-bottom box-shadow\" style=\"padding-right: 10px;\">\n\n\n <div class=\"container-fluid\" style=\"padding-right: 0px;\">\n\n <img *ngIf=\"appConfig.logo!=''\" [src]=\"appConfig.logo\" style=\"height: 50px; margin-right: 2em\" />\n\n <div>\n <!-- <div style=\"font-size: 20px;\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px;\">\n {{tenantName}}\n </div> -->\n\n <div *ngIf=\"!dataService.appConfig.multitenant\" style=\"font-size: 22px;\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant\" style=\"font-size: 20px; ; font-weight: 400;\" [ngStyle]=\"{'margin-top': dataService.appConfig.multitenant ? '12px' : ''}\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px; margin-bottom: 5px;\">\n {{tenantName}}\n </div>\n\n </div>\n\n\n\n <button class=\"navbar-toggler\" type=\"button\" data-toggle=\"collapse\" data-target=\".navbar-collapse\" aria-label=\"Toggle navigation\" [attr.aria-expanded]=\"isExpanded\" (click)=\"toggle()\">\n <span class=\"navbar-toggler-icon\"></span>\n </button>\n\n <div *ngIf=\"myRole\" class=\" navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse stack-top\" style=\"margin-right: 0px;\" [ngClass]=\"{ show: isExpanded, navitems: isExpanded }\" >\n\n <button mat-icon-button (click)=\"logoff()\" > <mat-icon>logout</mat-icon> </button>\n\n <div *ngIf=\"dataService.appConfig.multitenant\">\n\n <button mat-icon-button (click)=\"redirectTo('home/tenancy/settings')\" > <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon> </button>\n\n <!-- Removed: Support icon \u2014 replaced by floating assistant chat widget -->\n </div>\n\n\n <button id=\"btnUser\" mat-button [matMenuTriggerFor]=\"profileMenu\" ><mat-icon style=\"font-size: 24px;\">account_circle</mat-icon> {{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"] }]
|
|
22264
|
+
args: [{ selector: 'spa-nav-menu', standalone: false, template: "<header *ngIf=\"loggedin && dataService.appConfig.navigation == 'top'\">\n\n <!-- Changed: Removed mb-3 class to eliminate gap between toolbar and content -->\n <nav class=\"toolbar navbar navbar-expand-sm navbar-toggleable-sm navbar-light border-bottom box-shadow\" style=\"padding-right: 10px;\">\n\n\n <div class=\"container-fluid\" style=\"padding-right: 0px;\">\n\n <img *ngIf=\"appConfig.logo!=''\" [src]=\"appConfig.logo\" style=\"height: 50px; margin-right: 2em\" />\n\n <div>\n <!-- <div style=\"font-size: 20px;\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px;\">\n {{tenantName}}\n </div> -->\n\n <div *ngIf=\"!dataService.appConfig.multitenant\" style=\"font-size: 22px;\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant\" style=\"font-size: 20px; ; font-weight: 400;\" [ngStyle]=\"{'margin-top': dataService.appConfig.multitenant ? '12px' : ''}\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px; margin-bottom: 5px;\">\n {{tenantName}}\n </div>\n\n </div>\n\n\n\n <button class=\"navbar-toggler\" type=\"button\" data-toggle=\"collapse\" data-target=\".navbar-collapse\" aria-label=\"Toggle navigation\" [attr.aria-expanded]=\"isExpanded\" (click)=\"toggle()\">\n <span class=\"navbar-toggler-icon\"></span>\n </button>\n\n <div *ngIf=\"myRole\" class=\" navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse stack-top\" style=\"margin-right: 0px;\" [ngClass]=\"{ show: isExpanded, navitems: isExpanded }\" >\n\n <button mat-icon-button (click)=\"logoff()\" > <mat-icon>logout</mat-icon> </button>\n\n <div *ngIf=\"dataService.appConfig.multitenant\">\n\n <button mat-icon-button (click)=\"redirectTo('home/tenancy/settings')\" > <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon> </button>\n\n <!-- Removed: Support icon \u2014 replaced by floating assistant chat widget -->\n </div>\n\n\n <button id=\"btnUser\" mat-button [matMenuTriggerFor]=\"profileMenu\" ><mat-icon style=\"font-size: 24px;\">account_circle</mat-icon> {{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
22265
|
}], ctorParameters: () => [{ type: i1$1.Router }, { type: AuthService }, { type: StorageService }, { type: NotificationsService }, { type: i1$4.BreakpointObserver }, { type: DataServiceLib }, { type: i4.MatDialog }, { type: SubscriptionService }, { type: SetupService }, { type: LastRouteService }, { type: OfflineService }], propDecorators: { onWindowScroll: [{
|
|
21811
22266
|
type: HostListener,
|
|
21812
22267
|
args: ['window:scroll']
|
|
@@ -22761,6 +23216,8 @@ class DetailsDialog {
|
|
|
22761
23216
|
openNestedDetailsDialog(button) {
|
|
22762
23217
|
// Changed: Use token pattern instead of direct DetailsDialog import, propagate nestingLevel
|
|
22763
23218
|
this.dialogService.openDefaultDetailsDialog(button, this.details, this.nestingLevel).subscribe(result => {
|
|
23219
|
+
if (!result)
|
|
23220
|
+
return; // Added: same dismissal guard as table.component — Escape/backdrop closes a nested dialog with undefined, and a dismissal must not set actionPerformed or refresh
|
|
22764
23221
|
if (result.action === 'inputChange') {
|
|
22765
23222
|
this.inputChanged(result.change);
|
|
22766
23223
|
}
|
|
@@ -22970,11 +23427,11 @@ class DetailsDialog {
|
|
|
22970
23427
|
}
|
|
22971
23428
|
}
|
|
22972
23429
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DetailsDialog, deps: [{ token: i1$4.BreakpointObserver }, { token: LoaderService }, { token: DataServiceLib }, { token: MessageService }, { token: i4.MatDialogRef }, { token: MAT_DIALOG_DATA }, { token: ButtonService }, { token: DialogService }, { token: AuthService }, { token: TableConfigService }, { token: i0.NgZone }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
22973
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: DetailsDialog, isStandalone: false, selector: "spa-detailsDialog", outputs: { inputChange: "inputChange" }, viewQueries: [{ propertyName: "formComponent", first: true, predicate: FormComponent, descendants: true }], ngImport: i0, template: "<!-- Changed: dialog-has-tables mirrors the service's hasTables (tableConfigs?.length)\n which decides height '90%' vs 'auto' \u2014 used to scope scroll behaviour per case -->\n<div class=\"dialog-container\" [class.dialog-has-tables]=\"detailsConfig.tableConfigs?.length > 0\">\n\n <div class=\"dialog-content\">\n\n <!-- Changed: was <mat-progress-bar mode=\"indeterminate\">, which looped at a constant speed forever and so\n only ever said \"busy\". This is the same markup and the same PerceivedProgress curve the table stage\n uses, so a dialog and the table behind it move identically. It tracks the FORM's record load \u2014 nested\n tab tables run their own stages. Controlled by appConfig.quietLoading. -->\n <div *ngIf=\"showProgressLine\" class=\"tin-load-line\" aria-hidden=\"true\">\n <span class=\"tin-load-line-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n\n <!-- Changed: the header's padding and the title's type were inline styles, which always beat a class and so\n made a second presentation impossible. Moved into dlg-header-classic / dlg-title-classic, which restate\n those exact values, so the default look is unchanged while `modern` can swap in the banded header. -->\n <div class=\"d-flex justify-content-between align-items-center dialog-header\"\n [class.mt-2]=\"!modern\" [class.dlg-header-classic]=\"!modern\" [class.tin-dlg-head]=\"modern\">\n\n <div class=\"dialog-header-titles\">\n <label [class.dlg-title-classic]=\"!modern\" [class.tin-dlg-title]=\"modern\">{{titleAction | titlecase}} {{formConfig?.title}}</label>\n </div>\n\n <div class=\"d-flex align-items-center\" style=\"gap: 8px;\">\n\n <!-- Changed: Auto Refresh icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.autoRefreshConfig\" mat-icon-button\n [matTooltip]=\"autoRefreshEnabled ? 'Click to disable auto refresh' : 'Click to enable auto refresh'\" matTooltipPosition=\"above\"\n [style.color]=\"autoRefreshEnabled ? '#4caf50' : '#9e9e9e'\"\n (click)=\"autoRefreshEnabled = !autoRefreshEnabled; toggleAutoRefresh()\">\n <mat-icon>autorenew</mat-icon>\n </button>\n <!-- Changed: Keep Open icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.allowUserKeepOpen\" mat-icon-button\n [matTooltip]=\"userKeepOpen ? 'Click to disable keep open' : 'Click to enable keep open'\" matTooltipPosition=\"above\"\n [style.color]=\"userKeepOpen ? '#4caf50' : '#9e9e9e'\"\n (click)=\"userKeepOpen = !userKeepOpen\">\n <mat-icon>push_pin</mat-icon>\n </button>\n\n <!-- TS-16: bind precomputed editButtonVM instead of testVisible/testDisabled (which cloned objects) per CD -->\n <div *ngIf=\"formConfig.mode=='view' && editButton && editButtonVM.visible\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Edit\" color=\"primary\" (click)=\"setMode('edit')\" [disabled]=\"editButtonVM.disabled\"><mat-icon>edit</mat-icon></button>\n </div>\n\n <!-- Changed (Quiet Loading): every [disabled] below now ORs in entityLoading. A quiet read is silent, so it\n never flips LoaderService.isLoading and isProcessing alone would leave these buttons live during the\n load \u2014 a user could submit an empty form. With the flag off entityLoading simply tracks the same\n window isProcessing already covered, so nothing changes. -->\n <!-- Changed: the icon now spins while the form's record reloads, matching the table's refresh button.\n Same .tin-spin class, so the two surfaces use one idiom rather than each inventing its own. -->\n <button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"loadByAction\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Refresh\" color=\"primary\" (click)=\"loadData(formConfig.loadAction, detailsConfig.causeTableRefresh)\"><mat-icon class=\"refreshIcon\" [class.tin-spin]=\"entityLoading\">cached</mat-icon></button>\n \n <!-- Added: Top close button when position is 'top' -->\n <button *ngIf=\"shouldShowTopClose()\" [disabled]=\"isProcessing || entityLoading\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Close\" (click)=\"close()\"><mat-icon>close</mat-icon></button>\n\n </div>\n\n </div>\n\n <div style=\"padding-left: 24px; padding-right: 24px;\">\n <spa-steps *ngIf=\"stepConfig && details && stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n </div>\n\n <mat-dialog-content class=\"mat-typography dialog-scroll-content\">\n\n <spa-steps *ngIf=\"stepConfig && details && !stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && !statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && !formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n\n <div class=\"tin-input\" style=\"font-size:14px\">\n\n <!-- Changed (Quiet Loading, FSD D6): with the flag OFF this is the original bare \"Loading...\" line, unchanged.\n With it ON the text is replaced by a skeleton of the real form \u2014 and only once showSkeleton has been\n armed past the 150ms anti-flicker delay, so a cached record renders straight into the form with no flash. -->\n <p *ngIf=\"formConfig && !details && !effQuietLoading\"><em>Loading...</em></p>\n\n <spa-form-skeleton *ngIf=\"formConfig && !details && effQuietLoading && showSkeleton\" [config]=\"formConfig\"></spa-form-skeleton>\n\n <spa-form *ngIf=\"formConfig && details\" [files]=\"files\" [data]=\"details\" [config]=\"formConfig\" (inputChange)=\"inputChanged($event)\">\n <ng-template #dynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\" let-resolveLoadAction=\"resolveLoadAction\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"resolveLoadAction ? resolveLoadAction(field) : field.loadAction\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n </spa-form>\n\n <!-- Changed: Use unified spa-tabs with nestingLevel control \u2014 tabs hidden when nestingLevel >= 2 -->\n <spa-tabs\n *ngIf=\"showTabs && tableConfigs && !(detailsConfig.hideTablesInCreateMode && formConfig?.mode === 'create')\"\n [tableConfigs]=\"tableConfigs\"\n [reload]=\"tableReload\"\n [parentDetails]=\"details\"\n [localMode]=\"formConfig?.mode === 'create'\"\n [nestingLevel]=\"nestingLevel + 1\"\n (formRefresh)=\"loadData(formConfig.loadAction, false)\"\n (actionSuccess)=\"actionPerformed = true\">\n </spa-tabs><!-- Changed: nested-tab table actions flag actionPerformed so refreshOnCloseIfActioned refreshes the parent on close -->\n\n </div>\n\n </mat-dialog-content>\n\n\n </div>\n\n <mat-dialog-actions >\n\n <div>\n\n <button mat-raised-button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"formConfig.mode=='create' && createButton\" color=\"primary\"\n (click)=\"create()\" cdkFocusInitial>{{createButton.display ?? 'Submit'}}\n </button>\n\n <button mat-raised-button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"formConfig.mode=='edit' && editButton\" color=\"primary\"\n (click)=\"edit()\" cdkFocusInitial>{{editButton.display ?? 'Submit'}}\n </button>\n\n <!-- TS-16: bind precomputed extraButtonVMs instead of testVisible/testDisabled/getButtonColor (each cloning objects) per CD -->\n <ng-container *ngFor=\"let vm of extraButtonVMs\">\n <button *ngIf=\"formConfig.mode !== 'create' && vm.visible\" mat-stroked-button [disabled]=\"isProcessing || entityLoading || vm.disabled\" [ngStyle]=\"{'color': vm.color}\" (click)=\"custom(vm.button)\" cdkFocusInitial>\n <mat-icon *ngIf=\"vm.button.icon\" [ngStyle]=\"{'color': vm.color}\">{{vm.button.icon.name}}</mat-icon>\n {{vm.button.display ?? vm.button.name | titlecase}}\n </button>\n </ng-container>\n\n <!-- Changed: Bottom close button now uses conditional display and custom text -->\n <button *ngIf=\"shouldShowBottomClose()\" mat-stroked-button color=\"primary\" (click)=\"close()\">{{getCloseText()}}</button>\n\n </div>\n\n <div class=\"col d-flex justify-content-end\" *ngIf=\"smallScreen\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Delete\" [disabled]=\"isProcessing || entityLoading\" style=\"color: red;\" (click)=\"delete()\" *ngIf=\"formConfig.mode!='create' && deleteButton\"><mat-icon>delete</mat-icon></button>\n </div>\n\n\n </mat-dialog-actions>\n\n\n</div>\n\n\n\n\n\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}.dlg-header-classic{padding-left:24px;padding-right:24px}.dlg-title-classic{font-size:20px;font-weight:500;margin-top:10px;margin-bottom:5px}.dialog-header-titles{display:flex;flex-direction:column;justify-content:center;min-width:0}.tin-dlg-head .dialog-header-titles label{margin:0}.tbl-section-header{display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:8px;border:1px solid rgba(0,0,0,.08);border-radius:10px;background:transparent;cursor:pointer;transition:border-color .15s}.tbl-section-header:hover{border-color:#90a4ae}.tbl-section-header.tbl-section-static{cursor:default}.tbl-section-icon{color:#546e7a}.tbl-section-title{font-size:14px;font-weight:600;color:#000000d1}.tbl-section-count{background:#e3f2fd;color:#1565c0;border-radius:12px;padding:2px 10px;font-size:12px}.tbl-section-chip{font-size:12px;font-weight:500;color:#0009}.tbl-section-spacer{margin-left:auto}.tbl-section-chevron{color:#90a4ae}.tbl-section-caption{margin:-4px 12px 10px;font-size:12.5px;line-height:1.45;color:#0000008c;max-width:82ch}.tbl-section-more{padding:0 12px 8px}.tbl-section-link{background:none;border:none;color:#1565c0;cursor:pointer;font-size:13px;padding:0}.tbl-section-link:hover{text-decoration:underline}.tbl-section-icon,.tbl-section-chevron,.tbl-section-count{flex:0 0 auto}.tbl-section-title{flex:1 1 auto;min-width:0}@media (max-width: 700px){.tbl-section-header{gap:8px;padding:10px}.tbl-section-chip,.tbl-section-btn-text.has-icon{display:none}.tbl-section-header button{min-width:0;padding:0 10px}}table.tin-no-col-headers tr.mat-mdc-header-row{display:none}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i4.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i4.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: StepsComponent, selector: "spa-steps", inputs: ["value", "config", "data", "activeIndex"] }, { kind: "component", type: FormComponent, selector: "spa-form", inputs: ["files", "data", "config"], outputs: ["buttonClick", "inputChange"] }, { kind: "component", type: FormSkeletonComponent, selector: "spa-form-skeleton", inputs: ["config"] }, { kind: "component", type: AlertComponent, selector: "spa-alert", inputs: ["config", "data"] }, { kind: "component", type: TabsComponent, selector: "spa-tabs", inputs: ["tableConfigs", "reload", "reloadTab", "parentDetails", "localMode", "nestingLevel"], outputs: ["formRefresh", "actionSuccess"] }, { kind: "component", type: StatusesComponent, selector: "spa-statuses", inputs: ["config", "data"] }, { kind: "pipe", type: i1$2.TitleCasePipe, name: "titlecase" }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
|
|
23430
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: DetailsDialog, isStandalone: false, selector: "spa-detailsDialog", outputs: { inputChange: "inputChange" }, viewQueries: [{ propertyName: "formComponent", first: true, predicate: FormComponent, descendants: true }], ngImport: i0, template: "<!-- Changed: dialog-has-tables mirrors the service's hasTables (tableConfigs?.length)\n which decides height '90%' vs 'auto' \u2014 used to scope scroll behaviour per case -->\n<div class=\"dialog-container\" [class.dialog-has-tables]=\"detailsConfig.tableConfigs?.length > 0\">\n\n <div class=\"dialog-content\">\n\n <!-- Changed: was <mat-progress-bar mode=\"indeterminate\">, which looped at a constant speed forever and so\n only ever said \"busy\". This is the same markup and the same PerceivedProgress curve the table stage\n uses, so a dialog and the table behind it move identically. It tracks the FORM's record load \u2014 nested\n tab tables run their own stages. Controlled by appConfig.quietLoading. -->\n <div *ngIf=\"showProgressLine\" class=\"tin-load-line\" aria-hidden=\"true\">\n <span class=\"tin-load-line-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n\n <!-- Changed: the header's padding and the title's type were inline styles, which always beat a class and so\n made a second presentation impossible. Moved into dlg-header-classic / dlg-title-classic, which restate\n those exact values, so the default look is unchanged while `modern` can swap in the banded header. -->\n <div class=\"d-flex justify-content-between align-items-center dialog-header\"\n [class.mt-2]=\"!modern\" [class.dlg-header-classic]=\"!modern\" [class.tin-dlg-head]=\"modern\">\n\n <div class=\"dialog-header-titles\">\n <label [class.dlg-title-classic]=\"!modern\" [class.tin-dlg-title]=\"modern\">{{titleAction | titlecase}} {{formConfig?.title}}</label>\n </div>\n\n <div class=\"d-flex align-items-center\" style=\"gap: 8px;\">\n\n <!-- Changed: Auto Refresh icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.autoRefreshConfig\" mat-icon-button\n [matTooltip]=\"autoRefreshEnabled ? 'Click to disable auto refresh' : 'Click to enable auto refresh'\" matTooltipPosition=\"above\"\n [style.color]=\"autoRefreshEnabled ? '#4caf50' : '#9e9e9e'\"\n (click)=\"autoRefreshEnabled = !autoRefreshEnabled; toggleAutoRefresh()\">\n <mat-icon>autorenew</mat-icon>\n </button>\n <!-- Changed: Keep Open icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.allowUserKeepOpen\" mat-icon-button\n [matTooltip]=\"userKeepOpen ? 'Click to disable keep open' : 'Click to enable keep open'\" matTooltipPosition=\"above\"\n [style.color]=\"userKeepOpen ? '#4caf50' : '#9e9e9e'\"\n (click)=\"userKeepOpen = !userKeepOpen\">\n <mat-icon>push_pin</mat-icon>\n </button>\n\n <!-- TS-16: bind precomputed editButtonVM instead of testVisible/testDisabled (which cloned objects) per CD -->\n <div *ngIf=\"formConfig.mode=='view' && editButton && editButtonVM.visible\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Edit\" color=\"primary\" (click)=\"setMode('edit')\" [disabled]=\"editButtonVM.disabled\"><mat-icon>edit</mat-icon></button>\n </div>\n\n <!-- Changed (Quiet Loading): every [disabled] below now ORs in entityLoading. A quiet read is silent, so it\n never flips LoaderService.isLoading and isProcessing alone would leave these buttons live during the\n load \u2014 a user could submit an empty form. With the flag off entityLoading simply tracks the same\n window isProcessing already covered, so nothing changes. -->\n <!-- Changed: the icon now spins while the form's record reloads, matching the table's refresh button.\n Same .tin-spin class, so the two surfaces use one idiom rather than each inventing its own. -->\n <button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"loadByAction\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Refresh\" color=\"primary\" (click)=\"loadData(formConfig.loadAction, detailsConfig.causeTableRefresh)\"><mat-icon class=\"refreshIcon\" [class.tin-spin]=\"entityLoading\">cached</mat-icon></button>\n \n <!-- Added: Top close button when position is 'top' -->\n <button *ngIf=\"shouldShowTopClose()\" [disabled]=\"isProcessing || entityLoading\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Close\" (click)=\"close()\"><mat-icon>close</mat-icon></button>\n\n </div>\n\n </div>\n\n <div style=\"padding-left: 24px; padding-right: 24px;\">\n <spa-steps *ngIf=\"stepConfig && details && stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n </div>\n\n <mat-dialog-content class=\"mat-typography dialog-scroll-content\">\n\n <spa-steps *ngIf=\"stepConfig && details && !stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && !statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && !formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n\n <div class=\"tin-input\" style=\"font-size:14px\">\n\n <!-- Changed (Quiet Loading, FSD D6): with the flag OFF this is the original bare \"Loading...\" line, unchanged.\n With it ON the text is replaced by a skeleton of the real form \u2014 and only once showSkeleton has been\n armed past the 150ms anti-flicker delay, so a cached record renders straight into the form with no flash. -->\n <p *ngIf=\"formConfig && !details && !effQuietLoading\"><em>Loading...</em></p>\n\n <spa-form-skeleton *ngIf=\"formConfig && !details && effQuietLoading && showSkeleton\" [config]=\"formConfig\"></spa-form-skeleton>\n\n <spa-form *ngIf=\"formConfig && details\" [files]=\"files\" [data]=\"details\" [config]=\"formConfig\" (inputChange)=\"inputChanged($event)\">\n <ng-template #dynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\" let-resolveLoadAction=\"resolveLoadAction\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"resolveLoadAction ? resolveLoadAction(field) : field.loadAction\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n </spa-form>\n\n <!-- Changed: Use unified spa-tabs with nestingLevel control \u2014 tabs hidden when nestingLevel >= 2 -->\n <spa-tabs\n *ngIf=\"showTabs && tableConfigs && !(detailsConfig.hideTablesInCreateMode && formConfig?.mode === 'create')\"\n [tableConfigs]=\"tableConfigs\"\n [reload]=\"tableReload\"\n [parentDetails]=\"details\"\n [localMode]=\"formConfig?.mode === 'create'\"\n [nestingLevel]=\"nestingLevel + 1\"\n (formRefresh)=\"loadData(formConfig.loadAction, false)\"\n (actionSuccess)=\"actionPerformed = true\">\n </spa-tabs><!-- Changed: nested-tab table actions flag actionPerformed so refreshOnCloseIfActioned refreshes the parent on close -->\n\n </div>\n\n </mat-dialog-content>\n\n\n </div>\n\n <mat-dialog-actions >\n\n <div>\n\n <button mat-raised-button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"formConfig.mode=='create' && createButton\" color=\"primary\"\n (click)=\"create()\" cdkFocusInitial>{{createButton.display ?? 'Submit'}}\n </button>\n\n <button mat-raised-button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"formConfig.mode=='edit' && editButton\" color=\"primary\"\n (click)=\"edit()\" cdkFocusInitial>{{editButton.display ?? 'Submit'}}\n </button>\n\n <!-- TS-16: bind precomputed extraButtonVMs instead of testVisible/testDisabled/getButtonColor (each cloning objects) per CD -->\n <ng-container *ngFor=\"let vm of extraButtonVMs\">\n <button *ngIf=\"formConfig.mode !== 'create' && vm.visible\" mat-stroked-button [disabled]=\"isProcessing || entityLoading || vm.disabled\" [ngStyle]=\"{'color': vm.color}\" (click)=\"custom(vm.button)\" cdkFocusInitial>\n <mat-icon *ngIf=\"vm.button.icon\" [ngStyle]=\"{'color': vm.color}\">{{vm.button.icon.name}}</mat-icon>\n {{vm.button.display ?? vm.button.name | titlecase}}\n </button>\n </ng-container>\n\n <!-- Changed: Bottom close button now uses conditional display and custom text -->\n <button *ngIf=\"shouldShowBottomClose()\" mat-stroked-button color=\"primary\" (click)=\"close()\">{{getCloseText()}}</button>\n\n </div>\n\n <div class=\"col d-flex justify-content-end\" *ngIf=\"smallScreen\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Delete\" [disabled]=\"isProcessing || entityLoading\" style=\"color: red;\" (click)=\"delete()\" *ngIf=\"formConfig.mode!='create' && deleteButton\"><mat-icon>delete</mat-icon></button>\n </div>\n\n\n </mat-dialog-actions>\n\n\n</div>\n\n\n\n\n\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}.dlg-header-classic{padding-left:24px;padding-right:24px}.dlg-title-classic{font-size:20px;font-weight:500;margin-top:10px;margin-bottom:5px}.dialog-header-titles{display:flex;flex-direction:column;justify-content:center;min-width:0}.tin-dlg-head .dialog-header-titles label{margin:0}.tbl-section-header{display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:8px;border:1px solid rgba(0,0,0,.08);border-radius:10px;background:transparent;cursor:pointer;transition:border-color .15s}.tbl-section-header:hover{border-color:#90a4ae}.tbl-section-header.tbl-section-static{cursor:default}.tbl-section-icon{color:#546e7a}.tbl-section-title{font-size:14px;font-weight:600;color:#000000d1}.tbl-section-count{background:#e3f2fd;color:#1565c0;border-radius:12px;padding:2px 10px;font-size:12px}.tbl-section-chip{font-size:12px;font-weight:500;color:#0009}.tbl-section-spacer{margin-left:auto}.tbl-section-chevron{color:#90a4ae}.tbl-section-caption{margin:-4px 12px 10px;font-size:12.5px;line-height:1.45;color:#0000008c;max-width:82ch}.tbl-section-more{padding:0 12px 8px}.tbl-section-link{background:none;border:none;color:#1565c0;cursor:pointer;font-size:13px;padding:0}.tbl-section-link:hover{text-decoration:underline}.tbl-section-icon,.tbl-section-chevron,.tbl-section-count{flex:0 0 auto}.tbl-section-title{flex:1 1 auto;min-width:0}@media (max-width: 700px){.tbl-section-header{gap:8px;padding:10px}.tbl-section-chip,.tbl-section-btn-text.has-icon{display:none}.tbl-section-header button{min-width:0;padding:0 10px}}table.tin-no-col-headers tr.mat-mdc-header-row{display:none}.tin-filter-empty{text-align:center;padding:24px 12px}.tin-filter-empty-title{margin:0}.tin-filter-empty-hint{margin:6px 0 0;font-size:.85em;opacity:.7;overflow-wrap:anywhere}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i4.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i4.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: StepsComponent, selector: "spa-steps", inputs: ["value", "config", "data", "activeIndex"] }, { kind: "component", type: FormComponent, selector: "spa-form", inputs: ["files", "data", "config"], outputs: ["buttonClick", "inputChange"] }, { kind: "component", type: FormSkeletonComponent, selector: "spa-form-skeleton", inputs: ["config"] }, { kind: "component", type: AlertComponent, selector: "spa-alert", inputs: ["config", "data"] }, { kind: "component", type: TabsComponent, selector: "spa-tabs", inputs: ["tableConfigs", "reload", "reloadTab", "parentDetails", "localMode", "nestingLevel"], outputs: ["formRefresh", "actionSuccess"] }, { kind: "component", type: StatusesComponent, selector: "spa-statuses", inputs: ["config", "data"] }, { kind: "pipe", type: i1$2.TitleCasePipe, name: "titlecase" }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
|
|
22974
23431
|
}
|
|
22975
23432
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DetailsDialog, decorators: [{
|
|
22976
23433
|
type: Component,
|
|
22977
|
-
args: [{ selector: 'spa-detailsDialog', standalone: false, template: "<!-- Changed: dialog-has-tables mirrors the service's hasTables (tableConfigs?.length)\n which decides height '90%' vs 'auto' \u2014 used to scope scroll behaviour per case -->\n<div class=\"dialog-container\" [class.dialog-has-tables]=\"detailsConfig.tableConfigs?.length > 0\">\n\n <div class=\"dialog-content\">\n\n <!-- Changed: was <mat-progress-bar mode=\"indeterminate\">, which looped at a constant speed forever and so\n only ever said \"busy\". This is the same markup and the same PerceivedProgress curve the table stage\n uses, so a dialog and the table behind it move identically. It tracks the FORM's record load \u2014 nested\n tab tables run their own stages. Controlled by appConfig.quietLoading. -->\n <div *ngIf=\"showProgressLine\" class=\"tin-load-line\" aria-hidden=\"true\">\n <span class=\"tin-load-line-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n\n <!-- Changed: the header's padding and the title's type were inline styles, which always beat a class and so\n made a second presentation impossible. Moved into dlg-header-classic / dlg-title-classic, which restate\n those exact values, so the default look is unchanged while `modern` can swap in the banded header. -->\n <div class=\"d-flex justify-content-between align-items-center dialog-header\"\n [class.mt-2]=\"!modern\" [class.dlg-header-classic]=\"!modern\" [class.tin-dlg-head]=\"modern\">\n\n <div class=\"dialog-header-titles\">\n <label [class.dlg-title-classic]=\"!modern\" [class.tin-dlg-title]=\"modern\">{{titleAction | titlecase}} {{formConfig?.title}}</label>\n </div>\n\n <div class=\"d-flex align-items-center\" style=\"gap: 8px;\">\n\n <!-- Changed: Auto Refresh icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.autoRefreshConfig\" mat-icon-button\n [matTooltip]=\"autoRefreshEnabled ? 'Click to disable auto refresh' : 'Click to enable auto refresh'\" matTooltipPosition=\"above\"\n [style.color]=\"autoRefreshEnabled ? '#4caf50' : '#9e9e9e'\"\n (click)=\"autoRefreshEnabled = !autoRefreshEnabled; toggleAutoRefresh()\">\n <mat-icon>autorenew</mat-icon>\n </button>\n <!-- Changed: Keep Open icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.allowUserKeepOpen\" mat-icon-button\n [matTooltip]=\"userKeepOpen ? 'Click to disable keep open' : 'Click to enable keep open'\" matTooltipPosition=\"above\"\n [style.color]=\"userKeepOpen ? '#4caf50' : '#9e9e9e'\"\n (click)=\"userKeepOpen = !userKeepOpen\">\n <mat-icon>push_pin</mat-icon>\n </button>\n\n <!-- TS-16: bind precomputed editButtonVM instead of testVisible/testDisabled (which cloned objects) per CD -->\n <div *ngIf=\"formConfig.mode=='view' && editButton && editButtonVM.visible\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Edit\" color=\"primary\" (click)=\"setMode('edit')\" [disabled]=\"editButtonVM.disabled\"><mat-icon>edit</mat-icon></button>\n </div>\n\n <!-- Changed (Quiet Loading): every [disabled] below now ORs in entityLoading. A quiet read is silent, so it\n never flips LoaderService.isLoading and isProcessing alone would leave these buttons live during the\n load \u2014 a user could submit an empty form. With the flag off entityLoading simply tracks the same\n window isProcessing already covered, so nothing changes. -->\n <!-- Changed: the icon now spins while the form's record reloads, matching the table's refresh button.\n Same .tin-spin class, so the two surfaces use one idiom rather than each inventing its own. -->\n <button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"loadByAction\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Refresh\" color=\"primary\" (click)=\"loadData(formConfig.loadAction, detailsConfig.causeTableRefresh)\"><mat-icon class=\"refreshIcon\" [class.tin-spin]=\"entityLoading\">cached</mat-icon></button>\n \n <!-- Added: Top close button when position is 'top' -->\n <button *ngIf=\"shouldShowTopClose()\" [disabled]=\"isProcessing || entityLoading\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Close\" (click)=\"close()\"><mat-icon>close</mat-icon></button>\n\n </div>\n\n </div>\n\n <div style=\"padding-left: 24px; padding-right: 24px;\">\n <spa-steps *ngIf=\"stepConfig && details && stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n </div>\n\n <mat-dialog-content class=\"mat-typography dialog-scroll-content\">\n\n <spa-steps *ngIf=\"stepConfig && details && !stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && !statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && !formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n\n <div class=\"tin-input\" style=\"font-size:14px\">\n\n <!-- Changed (Quiet Loading, FSD D6): with the flag OFF this is the original bare \"Loading...\" line, unchanged.\n With it ON the text is replaced by a skeleton of the real form \u2014 and only once showSkeleton has been\n armed past the 150ms anti-flicker delay, so a cached record renders straight into the form with no flash. -->\n <p *ngIf=\"formConfig && !details && !effQuietLoading\"><em>Loading...</em></p>\n\n <spa-form-skeleton *ngIf=\"formConfig && !details && effQuietLoading && showSkeleton\" [config]=\"formConfig\"></spa-form-skeleton>\n\n <spa-form *ngIf=\"formConfig && details\" [files]=\"files\" [data]=\"details\" [config]=\"formConfig\" (inputChange)=\"inputChanged($event)\">\n <ng-template #dynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\" let-resolveLoadAction=\"resolveLoadAction\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"resolveLoadAction ? resolveLoadAction(field) : field.loadAction\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n </spa-form>\n\n <!-- Changed: Use unified spa-tabs with nestingLevel control \u2014 tabs hidden when nestingLevel >= 2 -->\n <spa-tabs\n *ngIf=\"showTabs && tableConfigs && !(detailsConfig.hideTablesInCreateMode && formConfig?.mode === 'create')\"\n [tableConfigs]=\"tableConfigs\"\n [reload]=\"tableReload\"\n [parentDetails]=\"details\"\n [localMode]=\"formConfig?.mode === 'create'\"\n [nestingLevel]=\"nestingLevel + 1\"\n (formRefresh)=\"loadData(formConfig.loadAction, false)\"\n (actionSuccess)=\"actionPerformed = true\">\n </spa-tabs><!-- Changed: nested-tab table actions flag actionPerformed so refreshOnCloseIfActioned refreshes the parent on close -->\n\n </div>\n\n </mat-dialog-content>\n\n\n </div>\n\n <mat-dialog-actions >\n\n <div>\n\n <button mat-raised-button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"formConfig.mode=='create' && createButton\" color=\"primary\"\n (click)=\"create()\" cdkFocusInitial>{{createButton.display ?? 'Submit'}}\n </button>\n\n <button mat-raised-button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"formConfig.mode=='edit' && editButton\" color=\"primary\"\n (click)=\"edit()\" cdkFocusInitial>{{editButton.display ?? 'Submit'}}\n </button>\n\n <!-- TS-16: bind precomputed extraButtonVMs instead of testVisible/testDisabled/getButtonColor (each cloning objects) per CD -->\n <ng-container *ngFor=\"let vm of extraButtonVMs\">\n <button *ngIf=\"formConfig.mode !== 'create' && vm.visible\" mat-stroked-button [disabled]=\"isProcessing || entityLoading || vm.disabled\" [ngStyle]=\"{'color': vm.color}\" (click)=\"custom(vm.button)\" cdkFocusInitial>\n <mat-icon *ngIf=\"vm.button.icon\" [ngStyle]=\"{'color': vm.color}\">{{vm.button.icon.name}}</mat-icon>\n {{vm.button.display ?? vm.button.name | titlecase}}\n </button>\n </ng-container>\n\n <!-- Changed: Bottom close button now uses conditional display and custom text -->\n <button *ngIf=\"shouldShowBottomClose()\" mat-stroked-button color=\"primary\" (click)=\"close()\">{{getCloseText()}}</button>\n\n </div>\n\n <div class=\"col d-flex justify-content-end\" *ngIf=\"smallScreen\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Delete\" [disabled]=\"isProcessing || entityLoading\" style=\"color: red;\" (click)=\"delete()\" *ngIf=\"formConfig.mode!='create' && deleteButton\"><mat-icon>delete</mat-icon></button>\n </div>\n\n\n </mat-dialog-actions>\n\n\n</div>\n\n\n\n\n\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}.dlg-header-classic{padding-left:24px;padding-right:24px}.dlg-title-classic{font-size:20px;font-weight:500;margin-top:10px;margin-bottom:5px}.dialog-header-titles{display:flex;flex-direction:column;justify-content:center;min-width:0}.tin-dlg-head .dialog-header-titles label{margin:0}.tbl-section-header{display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:8px;border:1px solid rgba(0,0,0,.08);border-radius:10px;background:transparent;cursor:pointer;transition:border-color .15s}.tbl-section-header:hover{border-color:#90a4ae}.tbl-section-header.tbl-section-static{cursor:default}.tbl-section-icon{color:#546e7a}.tbl-section-title{font-size:14px;font-weight:600;color:#000000d1}.tbl-section-count{background:#e3f2fd;color:#1565c0;border-radius:12px;padding:2px 10px;font-size:12px}.tbl-section-chip{font-size:12px;font-weight:500;color:#0009}.tbl-section-spacer{margin-left:auto}.tbl-section-chevron{color:#90a4ae}.tbl-section-caption{margin:-4px 12px 10px;font-size:12.5px;line-height:1.45;color:#0000008c;max-width:82ch}.tbl-section-more{padding:0 12px 8px}.tbl-section-link{background:none;border:none;color:#1565c0;cursor:pointer;font-size:13px;padding:0}.tbl-section-link:hover{text-decoration:underline}.tbl-section-icon,.tbl-section-chevron,.tbl-section-count{flex:0 0 auto}.tbl-section-title{flex:1 1 auto;min-width:0}@media (max-width: 700px){.tbl-section-header{gap:8px;padding:10px}.tbl-section-chip,.tbl-section-btn-text.has-icon{display:none}.tbl-section-header button{min-width:0;padding:0 10px}}table.tin-no-col-headers tr.mat-mdc-header-row{display:none}\n"] }]
|
|
23434
|
+
args: [{ selector: 'spa-detailsDialog', standalone: false, template: "<!-- Changed: dialog-has-tables mirrors the service's hasTables (tableConfigs?.length)\n which decides height '90%' vs 'auto' \u2014 used to scope scroll behaviour per case -->\n<div class=\"dialog-container\" [class.dialog-has-tables]=\"detailsConfig.tableConfigs?.length > 0\">\n\n <div class=\"dialog-content\">\n\n <!-- Changed: was <mat-progress-bar mode=\"indeterminate\">, which looped at a constant speed forever and so\n only ever said \"busy\". This is the same markup and the same PerceivedProgress curve the table stage\n uses, so a dialog and the table behind it move identically. It tracks the FORM's record load \u2014 nested\n tab tables run their own stages. Controlled by appConfig.quietLoading. -->\n <div *ngIf=\"showProgressLine\" class=\"tin-load-line\" aria-hidden=\"true\">\n <span class=\"tin-load-line-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n\n <!-- Changed: the header's padding and the title's type were inline styles, which always beat a class and so\n made a second presentation impossible. Moved into dlg-header-classic / dlg-title-classic, which restate\n those exact values, so the default look is unchanged while `modern` can swap in the banded header. -->\n <div class=\"d-flex justify-content-between align-items-center dialog-header\"\n [class.mt-2]=\"!modern\" [class.dlg-header-classic]=\"!modern\" [class.tin-dlg-head]=\"modern\">\n\n <div class=\"dialog-header-titles\">\n <label [class.dlg-title-classic]=\"!modern\" [class.tin-dlg-title]=\"modern\">{{titleAction | titlecase}} {{formConfig?.title}}</label>\n </div>\n\n <div class=\"d-flex align-items-center\" style=\"gap: 8px;\">\n\n <!-- Changed: Auto Refresh icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.autoRefreshConfig\" mat-icon-button\n [matTooltip]=\"autoRefreshEnabled ? 'Click to disable auto refresh' : 'Click to enable auto refresh'\" matTooltipPosition=\"above\"\n [style.color]=\"autoRefreshEnabled ? '#4caf50' : '#9e9e9e'\"\n (click)=\"autoRefreshEnabled = !autoRefreshEnabled; toggleAutoRefresh()\">\n <mat-icon>autorenew</mat-icon>\n </button>\n <!-- Changed: Keep Open icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.allowUserKeepOpen\" mat-icon-button\n [matTooltip]=\"userKeepOpen ? 'Click to disable keep open' : 'Click to enable keep open'\" matTooltipPosition=\"above\"\n [style.color]=\"userKeepOpen ? '#4caf50' : '#9e9e9e'\"\n (click)=\"userKeepOpen = !userKeepOpen\">\n <mat-icon>push_pin</mat-icon>\n </button>\n\n <!-- TS-16: bind precomputed editButtonVM instead of testVisible/testDisabled (which cloned objects) per CD -->\n <div *ngIf=\"formConfig.mode=='view' && editButton && editButtonVM.visible\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Edit\" color=\"primary\" (click)=\"setMode('edit')\" [disabled]=\"editButtonVM.disabled\"><mat-icon>edit</mat-icon></button>\n </div>\n\n <!-- Changed (Quiet Loading): every [disabled] below now ORs in entityLoading. A quiet read is silent, so it\n never flips LoaderService.isLoading and isProcessing alone would leave these buttons live during the\n load \u2014 a user could submit an empty form. With the flag off entityLoading simply tracks the same\n window isProcessing already covered, so nothing changes. -->\n <!-- Changed: the icon now spins while the form's record reloads, matching the table's refresh button.\n Same .tin-spin class, so the two surfaces use one idiom rather than each inventing its own. -->\n <button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"loadByAction\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Refresh\" color=\"primary\" (click)=\"loadData(formConfig.loadAction, detailsConfig.causeTableRefresh)\"><mat-icon class=\"refreshIcon\" [class.tin-spin]=\"entityLoading\">cached</mat-icon></button>\n \n <!-- Added: Top close button when position is 'top' -->\n <button *ngIf=\"shouldShowTopClose()\" [disabled]=\"isProcessing || entityLoading\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Close\" (click)=\"close()\"><mat-icon>close</mat-icon></button>\n\n </div>\n\n </div>\n\n <div style=\"padding-left: 24px; padding-right: 24px;\">\n <spa-steps *ngIf=\"stepConfig && details && stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n </div>\n\n <mat-dialog-content class=\"mat-typography dialog-scroll-content\">\n\n <spa-steps *ngIf=\"stepConfig && details && !stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && !statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && !formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n\n <div class=\"tin-input\" style=\"font-size:14px\">\n\n <!-- Changed (Quiet Loading, FSD D6): with the flag OFF this is the original bare \"Loading...\" line, unchanged.\n With it ON the text is replaced by a skeleton of the real form \u2014 and only once showSkeleton has been\n armed past the 150ms anti-flicker delay, so a cached record renders straight into the form with no flash. -->\n <p *ngIf=\"formConfig && !details && !effQuietLoading\"><em>Loading...</em></p>\n\n <spa-form-skeleton *ngIf=\"formConfig && !details && effQuietLoading && showSkeleton\" [config]=\"formConfig\"></spa-form-skeleton>\n\n <spa-form *ngIf=\"formConfig && details\" [files]=\"files\" [data]=\"details\" [config]=\"formConfig\" (inputChange)=\"inputChanged($event)\">\n <ng-template #dynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\" let-resolveLoadAction=\"resolveLoadAction\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"resolveLoadAction ? resolveLoadAction(field) : field.loadAction\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n </spa-form>\n\n <!-- Changed: Use unified spa-tabs with nestingLevel control \u2014 tabs hidden when nestingLevel >= 2 -->\n <spa-tabs\n *ngIf=\"showTabs && tableConfigs && !(detailsConfig.hideTablesInCreateMode && formConfig?.mode === 'create')\"\n [tableConfigs]=\"tableConfigs\"\n [reload]=\"tableReload\"\n [parentDetails]=\"details\"\n [localMode]=\"formConfig?.mode === 'create'\"\n [nestingLevel]=\"nestingLevel + 1\"\n (formRefresh)=\"loadData(formConfig.loadAction, false)\"\n (actionSuccess)=\"actionPerformed = true\">\n </spa-tabs><!-- Changed: nested-tab table actions flag actionPerformed so refreshOnCloseIfActioned refreshes the parent on close -->\n\n </div>\n\n </mat-dialog-content>\n\n\n </div>\n\n <mat-dialog-actions >\n\n <div>\n\n <button mat-raised-button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"formConfig.mode=='create' && createButton\" color=\"primary\"\n (click)=\"create()\" cdkFocusInitial>{{createButton.display ?? 'Submit'}}\n </button>\n\n <button mat-raised-button [disabled]=\"isProcessing || entityLoading\" *ngIf=\"formConfig.mode=='edit' && editButton\" color=\"primary\"\n (click)=\"edit()\" cdkFocusInitial>{{editButton.display ?? 'Submit'}}\n </button>\n\n <!-- TS-16: bind precomputed extraButtonVMs instead of testVisible/testDisabled/getButtonColor (each cloning objects) per CD -->\n <ng-container *ngFor=\"let vm of extraButtonVMs\">\n <button *ngIf=\"formConfig.mode !== 'create' && vm.visible\" mat-stroked-button [disabled]=\"isProcessing || entityLoading || vm.disabled\" [ngStyle]=\"{'color': vm.color}\" (click)=\"custom(vm.button)\" cdkFocusInitial>\n <mat-icon *ngIf=\"vm.button.icon\" [ngStyle]=\"{'color': vm.color}\">{{vm.button.icon.name}}</mat-icon>\n {{vm.button.display ?? vm.button.name | titlecase}}\n </button>\n </ng-container>\n\n <!-- Changed: Bottom close button now uses conditional display and custom text -->\n <button *ngIf=\"shouldShowBottomClose()\" mat-stroked-button color=\"primary\" (click)=\"close()\">{{getCloseText()}}</button>\n\n </div>\n\n <div class=\"col d-flex justify-content-end\" *ngIf=\"smallScreen\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Delete\" [disabled]=\"isProcessing || entityLoading\" style=\"color: red;\" (click)=\"delete()\" *ngIf=\"formConfig.mode!='create' && deleteButton\"><mat-icon>delete</mat-icon></button>\n </div>\n\n\n </mat-dialog-actions>\n\n\n</div>\n\n\n\n\n\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}.dlg-header-classic{padding-left:24px;padding-right:24px}.dlg-title-classic{font-size:20px;font-weight:500;margin-top:10px;margin-bottom:5px}.dialog-header-titles{display:flex;flex-direction:column;justify-content:center;min-width:0}.tin-dlg-head .dialog-header-titles label{margin:0}.tbl-section-header{display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:8px;border:1px solid rgba(0,0,0,.08);border-radius:10px;background:transparent;cursor:pointer;transition:border-color .15s}.tbl-section-header:hover{border-color:#90a4ae}.tbl-section-header.tbl-section-static{cursor:default}.tbl-section-icon{color:#546e7a}.tbl-section-title{font-size:14px;font-weight:600;color:#000000d1}.tbl-section-count{background:#e3f2fd;color:#1565c0;border-radius:12px;padding:2px 10px;font-size:12px}.tbl-section-chip{font-size:12px;font-weight:500;color:#0009}.tbl-section-spacer{margin-left:auto}.tbl-section-chevron{color:#90a4ae}.tbl-section-caption{margin:-4px 12px 10px;font-size:12.5px;line-height:1.45;color:#0000008c;max-width:82ch}.tbl-section-more{padding:0 12px 8px}.tbl-section-link{background:none;border:none;color:#1565c0;cursor:pointer;font-size:13px;padding:0}.tbl-section-link:hover{text-decoration:underline}.tbl-section-icon,.tbl-section-chevron,.tbl-section-count{flex:0 0 auto}.tbl-section-title{flex:1 1 auto;min-width:0}@media (max-width: 700px){.tbl-section-header{gap:8px;padding:10px}.tbl-section-chip,.tbl-section-btn-text.has-icon{display:none}.tbl-section-header button{min-width:0;padding:0 10px}}table.tin-no-col-headers tr.mat-mdc-header-row{display:none}.tin-filter-empty{text-align:center;padding:24px 12px}.tin-filter-empty-title{margin:0}.tin-filter-empty-hint{margin:6px 0 0;font-size:.85em;opacity:.7;overflow-wrap:anywhere}\n"] }]
|
|
22978
23435
|
}], ctorParameters: () => [{ type: i1$4.BreakpointObserver }, { type: LoaderService }, { type: DataServiceLib }, { type: MessageService }, { type: i4.MatDialogRef }, { type: DetailsDialogConfig, decorators: [{
|
|
22979
23436
|
type: Inject,
|
|
22980
23437
|
args: [MAT_DIALOG_DATA]
|
|
@@ -23589,7 +24046,7 @@ class TitleActionsComponent {
|
|
|
23589
24046
|
this.actionValues[action.name] = value;
|
|
23590
24047
|
}
|
|
23591
24048
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TitleActionsComponent, deps: [{ token: DataServiceLib }, { token: MessageService }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
23592
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TitleActionsComponent, isStandalone: false, selector: "spa-title-actions", inputs: { titleActions: "titleActions" }, outputs: { actionChange: "actionChange" }, ngImport: i0, template: "<div class=\"title-actions-container\" *ngIf=\"titleActions && titleActions.length > 0\">\n \n <ng-container *ngFor=\"let action of titleActions\">\n \n <!-- Date Field -->\n <spa-date \n *ngIf=\"action.type === 'date' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [width]=\"action.width\"\n [(value)]=\"actionValues[action.name]\"\n [required]=\"action.required\"\n [readonly]=\"isReadonly(action)\"\n [hint]=\"action.hint\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-date>\n\n <!-- DateTime Field -->\n <spa-datetime \n *ngIf=\"action.type === 'datetime' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [width]=\"action.width\"\n [(value)]=\"actionValues[action.name]\"\n [readonly]=\"isReadonly(action)\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-datetime>\n\n <!-- Select Field -->\n <spa-select \n *ngIf=\"action.type === 'select' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [width]=\"action.width\"\n [nullable]=\"action.nullable\"\n [options]=\"action.options\"\n [optionDisplay]=\"getOptionDisplay(action)\"\n [optionValue]=\"getOptionValue(action)\"\n [(value)]=\"actionValues[action.name]\"\n [defaultFirstValue]=\"action.defaultFirstValue\"\n [required]=\"action.required\"\n [readonly]=\"isReadonly(action)\"\n [hint]=\"action.hint\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-select>\n\n <!-- Multi-Select Field -->\n <spa-select-multi \n *ngIf=\"action.type === 'select-multi' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [width]=\"action.width\"\n [options]=\"action.options\"\n [optionDisplay]=\"getOptionDisplay(action)\"\n [optionValue]=\"getOptionValue(action)\"\n [(value)]=\"actionValues[action.name]\"\n [required]=\"action.required\"\n [readonly]=\"isReadonly(action)\"\n [hint]=\"action.hint\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-select-multi>\n\n <!-- Checkbox Field -->\n <spa-check \n *ngIf=\"action.type === 'checkbox' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [(value)]=\"actionValues[action.name]\"\n [readonly]=\"isReadonly(action)\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-check>\n\n <!-- Button Field -->\n <button \n *ngIf=\"action.type === 'button' && !isHidden(action)\"\n mat-icon-button\n [disabled]=\"isReadonly(action)\"\n [matTooltip]=\"action.hint ?? (action.alias ?? action.name | camelToWords)\"\n matTooltipPosition=\"above\"\n [color]=\"action.color ?? 'primary'\"\n (click)=\"onButtonClick(action)\">\n <mat-icon *ngIf=\"action.icon\">{{action.icon.name}}</mat-icon>\n <span *ngIf=\"!action.icon\">{{action.alias ?? action.name | camelToWords}}</span>\n </button>\n\n </ng-container>\n\n</div>\n", styles: [".title-actions-container{display:flex;align-items:flex-start;gap:15px;flex-wrap:wrap}.title-actions-container spa-date,.title-actions-container spa-datetime,.title-actions-container spa-select,.title-actions-container spa-multi-select{min-width:150px}.title-actions-container button{margin:0}@media (max-width: 600px){.title-actions-container{gap:10px}.title-actions-container spa-date,.title-actions-container spa-datetime,.title-actions-container spa-select,.title-actions-container spa-multi-select{min-width:120px}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: DateComponent, selector: "spa-date", inputs: ["required", "min", "max", "readonly", "hint", "value", "display", "placeholder", "width", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: DatetimeComponent, selector: "spa-datetime", inputs: ["display", "value", "readonly", "width", "min", "max", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: SelectMultiComponent, selector: "spa-select-multi", inputs: ["display", "value", "readonly", "required", "hint", "options", "optionDisplay", "optionValue", "infoMessage", "copyContent", "clearContent", "nullable", "placeholder", "width", "suffix", "loadAction", "selectAll"], outputs: ["valueChange", "hoverChange"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
|
|
24049
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TitleActionsComponent, isStandalone: false, selector: "spa-title-actions", inputs: { titleActions: "titleActions" }, outputs: { actionChange: "actionChange" }, ngImport: i0, template: "<div class=\"title-actions-container\" *ngIf=\"titleActions && titleActions.length > 0\">\n \n <ng-container *ngFor=\"let action of titleActions\">\n \n <!-- Date Field -->\n <spa-date \n *ngIf=\"action.type === 'date' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [width]=\"action.width\"\n [(value)]=\"actionValues[action.name]\"\n [required]=\"action.required\"\n [readonly]=\"isReadonly(action)\"\n [hint]=\"action.hint\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-date>\n\n <!-- DateTime Field -->\n <spa-datetime \n *ngIf=\"action.type === 'datetime' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [width]=\"action.width\"\n [(value)]=\"actionValues[action.name]\"\n [readonly]=\"isReadonly(action)\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-datetime>\n\n <!-- Select Field -->\n <spa-select \n *ngIf=\"action.type === 'select' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [width]=\"action.width\"\n [nullable]=\"action.nullable\"\n [options]=\"action.options\"\n [optionDisplay]=\"getOptionDisplay(action)\"\n [optionValue]=\"getOptionValue(action)\"\n [(value)]=\"actionValues[action.name]\"\n [defaultFirstValue]=\"action.defaultFirstValue\"\n [required]=\"action.required\"\n [readonly]=\"isReadonly(action)\"\n [hint]=\"action.hint\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-select>\n\n <!-- Multi-Select Field -->\n <spa-select-multi \n *ngIf=\"action.type === 'select-multi' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [width]=\"action.width\"\n [options]=\"action.options\"\n [optionDisplay]=\"getOptionDisplay(action)\"\n [optionValue]=\"getOptionValue(action)\"\n [(value)]=\"actionValues[action.name]\"\n [required]=\"action.required\"\n [readonly]=\"isReadonly(action)\"\n [hint]=\"action.hint\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-select-multi>\n\n <!-- Checkbox Field -->\n <spa-check \n *ngIf=\"action.type === 'checkbox' && !isHidden(action)\"\n [display]=\"action.alias ?? action.name | camelToWords\"\n [(value)]=\"actionValues[action.name]\"\n [readonly]=\"isReadonly(action)\"\n (valueChange)=\"onActionChange(action, actionValues[action.name])\">\n </spa-check>\n\n <!-- Button Field -->\n <button \n *ngIf=\"action.type === 'button' && !isHidden(action)\"\n mat-icon-button\n [disabled]=\"isReadonly(action)\"\n [matTooltip]=\"action.hint ?? (action.alias ?? action.name | camelToWords)\"\n matTooltipPosition=\"above\"\n [color]=\"action.color ?? 'primary'\"\n (click)=\"onButtonClick(action)\">\n <mat-icon *ngIf=\"action.icon\">{{action.icon.name}}</mat-icon>\n <span *ngIf=\"!action.icon\">{{action.alias ?? action.name | camelToWords}}</span>\n </button>\n\n </ng-container>\n\n</div>\n", styles: [".title-actions-container{display:flex;align-items:flex-start;gap:15px;flex-wrap:wrap}.title-actions-container spa-date,.title-actions-container spa-datetime,.title-actions-container spa-select,.title-actions-container spa-multi-select{min-width:150px}.title-actions-container button{margin:0}@media (max-width: 600px){.title-actions-container{gap:10px}.title-actions-container spa-date,.title-actions-container spa-datetime,.title-actions-container spa-select,.title-actions-container spa-multi-select{min-width:120px}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage", "hint"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: DateComponent, selector: "spa-date", inputs: ["required", "min", "max", "readonly", "hint", "value", "display", "placeholder", "width", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: DatetimeComponent, selector: "spa-datetime", inputs: ["display", "value", "readonly", "width", "min", "max", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: SelectMultiComponent, selector: "spa-select-multi", inputs: ["display", "value", "readonly", "required", "hint", "options", "optionDisplay", "optionValue", "infoMessage", "copyContent", "clearContent", "nullable", "placeholder", "width", "suffix", "loadAction", "selectAll"], outputs: ["valueChange", "hoverChange"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
|
|
23593
24050
|
}
|
|
23594
24051
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TitleActionsComponent, decorators: [{
|
|
23595
24052
|
type: Component,
|
|
@@ -23669,7 +24126,7 @@ class PageComponent {
|
|
|
23669
24126
|
this.dataLoad.emit(x);
|
|
23670
24127
|
}
|
|
23671
24128
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PageComponent, deps: [{ token: DataServiceLib }, { token: MessageService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
23672
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PageComponent, isStandalone: false, selector: "spa-page", inputs: { config: "config" }, outputs: { searchModeActivated: "searchModeActivated", searchModeDeactivated: "searchModeDeactivated", refreshClick: "refreshClick", actionClick: "actionClick", actionResponse: "actionResponse", inputChange: "inputChange", createClick: "createClick", searchClick: "searchClick", dataLoad: "dataLoad", titleActionChange: "titleActionChange" }, ngImport: i0, template: "<div class=\"row\">\n\n <div class=\"col-auto\">\n <h4>{{config.title ?? 'Untitled'}} </h4>\n </div>\n\n <div class=\"col d-flex justify-content-end align-items-center\" style=\"font-size: 14px; gap: 15px;\">\n <!-- Added: Title actions component -->\n <spa-title-actions \n *ngIf=\"config.titleActions\" \n [titleActions]=\"config.titleActions\"\n (actionChange)=\"titleActionChanged($event)\">\n </spa-title-actions>\n \n <spa-check *ngIf=\"config.searchTableConfig\" [(value)]=\"searchMode\" display=\"Search Mode\" (valueChange)=\"toggleSearch()\" style=\"margin-right: 10px;\"></spa-check>\n </div>\n\n</div>\n\n<hr style=\"margin-top: 0px;\" />\n\n\n<div style=\" font-size: 14px;\">\n <!-- Normal -->\n <spa-table *ngIf=\"!searchMode\" [config]=\"normalTableConfig\" [reload]=\"tableReload\"\n (refreshClick)=\"refreshClicked()\" (actionClick)=\"actionClicked($event)\" (actionResponse)=\"actionResponded($event)\"\n (inputChange)=\"inputChanged($event)\" (createClick)=\"createClicked($event)\" (searchClick)=\"searchClicked($event)\" (dataLoad)=\"dataLoaded($event)\">\n </spa-table>\n\n <!-- Search -->\n <spa-table *ngIf=\"searchMode\" [config]=\"searchTableConfig\" [reload]=\"tableReload\"\n (refreshClick)=\"refreshClicked()\" (actionClick)=\"actionClicked($event)\" (actionResponse)=\"actionResponded($event)\"\n (inputChange)=\"inputChanged($event)\" (createClick)=\"createClicked($event)\" (searchClick)=\"searchClicked($event)\" (dataLoad)=\"dataLoaded($event)\">\n </spa-table>\n</div>\n\n", styles: [".mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}\n"], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }, { kind: "component", type: TitleActionsComponent, selector: "spa-title-actions", inputs: ["titleActions"], outputs: ["actionChange"] }] }); }
|
|
24129
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PageComponent, isStandalone: false, selector: "spa-page", inputs: { config: "config" }, outputs: { searchModeActivated: "searchModeActivated", searchModeDeactivated: "searchModeDeactivated", refreshClick: "refreshClick", actionClick: "actionClick", actionResponse: "actionResponse", inputChange: "inputChange", createClick: "createClick", searchClick: "searchClick", dataLoad: "dataLoad", titleActionChange: "titleActionChange" }, ngImport: i0, template: "<div class=\"row\">\n\n <div class=\"col-auto\">\n <h4>{{config.title ?? 'Untitled'}} </h4>\n </div>\n\n <div class=\"col d-flex justify-content-end align-items-center\" style=\"font-size: 14px; gap: 15px;\">\n <!-- Added: Title actions component -->\n <spa-title-actions \n *ngIf=\"config.titleActions\" \n [titleActions]=\"config.titleActions\"\n (actionChange)=\"titleActionChanged($event)\">\n </spa-title-actions>\n \n <spa-check *ngIf=\"config.searchTableConfig\" [(value)]=\"searchMode\" display=\"Search Mode\" (valueChange)=\"toggleSearch()\" style=\"margin-right: 10px;\"></spa-check>\n </div>\n\n</div>\n\n<hr style=\"margin-top: 0px;\" />\n\n\n<div style=\" font-size: 14px;\">\n <!-- Normal -->\n <spa-table *ngIf=\"!searchMode\" [config]=\"normalTableConfig\" [reload]=\"tableReload\"\n (refreshClick)=\"refreshClicked()\" (actionClick)=\"actionClicked($event)\" (actionResponse)=\"actionResponded($event)\"\n (inputChange)=\"inputChanged($event)\" (createClick)=\"createClicked($event)\" (searchClick)=\"searchClicked($event)\" (dataLoad)=\"dataLoaded($event)\">\n </spa-table>\n\n <!-- Search -->\n <spa-table *ngIf=\"searchMode\" [config]=\"searchTableConfig\" [reload]=\"tableReload\"\n (refreshClick)=\"refreshClicked()\" (actionClick)=\"actionClicked($event)\" (actionResponse)=\"actionResponded($event)\"\n (inputChange)=\"inputChanged($event)\" (createClick)=\"createClicked($event)\" (searchClick)=\"searchClicked($event)\" (dataLoad)=\"dataLoaded($event)\">\n </spa-table>\n</div>\n\n", styles: [".mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}\n"], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage", "hint"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }, { kind: "component", type: TitleActionsComponent, selector: "spa-title-actions", inputs: ["titleActions"], outputs: ["actionChange"] }] }); }
|
|
23673
24130
|
}
|
|
23674
24131
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PageComponent, decorators: [{
|
|
23675
24132
|
type: Component,
|
|
@@ -24368,7 +24825,7 @@ class TinSpaModule {
|
|
|
24368
24825
|
AttachComponent, ChipsComponent, LoaderComponent, NavMenuComponent, TableComponent, DetailsDialog, FormComponent, FormSkeletonComponent, CamelToWordsPipe, AgentMarkdownPipe, NumberComponent, SearchComponent, ViewerComponent, viewerDialog,
|
|
24369
24826
|
ListDialogComponent,
|
|
24370
24827
|
InvitationsTableComponent,
|
|
24371
|
-
TableHeaderComponent, TableRowComponent, TableActionComponent, InlineCellComponent, // Changed: added InlineCellComponent (inline row editing)
|
|
24828
|
+
TableHeaderComponent, TableRowComponent, TableActionComponent, InlineCellComponent, MonogramComponent, // Changed: added InlineCellComponent (inline row editing), MonogramComponent (monogram column type)
|
|
24372
24829
|
AlertComponent, EmailComponent, PageComponent, SelectCommonComponent, SuffixComponent, SelectContextDirective, SelectLiteComponent,
|
|
24373
24830
|
TextMultiComponent,
|
|
24374
24831
|
SelectMultiComponent, SelectBitwiseComponent,
|
|
@@ -24392,6 +24849,7 @@ class TinSpaModule {
|
|
|
24392
24849
|
DragDropModule // Changed: Added for CDK drag-drop between groups
|
|
24393
24850
|
], exports: [TinSpaComponent,
|
|
24394
24851
|
SpaMatModule,
|
|
24852
|
+
MonogramComponent, // Added: monogram column type — exported so hosts can render a person outside a table too
|
|
24395
24853
|
AlertComponent,
|
|
24396
24854
|
TextComponent,
|
|
24397
24855
|
TextMaskComponent,
|
|
@@ -24473,7 +24931,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
24473
24931
|
AttachComponent, ChipsComponent, LoaderComponent, NavMenuComponent, TableComponent, DetailsDialog, FormComponent, FormSkeletonComponent, CamelToWordsPipe, AgentMarkdownPipe, NumberComponent, SearchComponent, ViewerComponent, viewerDialog,
|
|
24474
24932
|
ListDialogComponent,
|
|
24475
24933
|
InvitationsTableComponent,
|
|
24476
|
-
TableHeaderComponent, TableRowComponent, TableActionComponent, InlineCellComponent, // Changed: added InlineCellComponent (inline row editing)
|
|
24934
|
+
TableHeaderComponent, TableRowComponent, TableActionComponent, InlineCellComponent, MonogramComponent, // Changed: added InlineCellComponent (inline row editing), MonogramComponent (monogram column type)
|
|
24477
24935
|
AlertComponent, EmailComponent, PageComponent, SelectCommonComponent, SuffixComponent, SelectContextDirective, SelectLiteComponent,
|
|
24478
24936
|
TextMultiComponent,
|
|
24479
24937
|
SelectMultiComponent, SelectBitwiseComponent,
|
|
@@ -24501,6 +24959,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
24501
24959
|
exports: [
|
|
24502
24960
|
TinSpaComponent,
|
|
24503
24961
|
SpaMatModule,
|
|
24962
|
+
MonogramComponent, // Added: monogram column type — exported so hosts can render a person outside a table too
|
|
24504
24963
|
AlertComponent,
|
|
24505
24964
|
TextComponent,
|
|
24506
24965
|
TextMaskComponent,
|
|
@@ -25653,7 +26112,7 @@ class RolesComponent {
|
|
|
25653
26112
|
const newRole = { roleName: '' };
|
|
25654
26113
|
// Open add role dialog using the same approach as rename
|
|
25655
26114
|
const dialogRef = this.dialogService.openConfiguredDetailsDialog(this.addRoleButton, newRole, DetailsDialog).subscribe(result => {
|
|
25656
|
-
if (result
|
|
26115
|
+
if (result?.message === 'success') { // Changed: a dismissed role dialog closes with undefined — read through it rather than throwing
|
|
25657
26116
|
this.loadRoles();
|
|
25658
26117
|
}
|
|
25659
26118
|
});
|
|
@@ -25694,7 +26153,7 @@ class RolesComponent {
|
|
|
25694
26153
|
const roleData = { ...role };
|
|
25695
26154
|
// Open rename dialog
|
|
25696
26155
|
const dialogRef = this.dialogService.openConfiguredDetailsDialog(this.renameButton, roleData, DetailsDialog).subscribe(result => {
|
|
25697
|
-
if (result
|
|
26156
|
+
if (result?.message === 'success') { // Changed: a dismissed role dialog closes with undefined — read through it rather than throwing
|
|
25698
26157
|
this.loadRoles();
|
|
25699
26158
|
}
|
|
25700
26159
|
});
|
|
@@ -25795,7 +26254,7 @@ class CreateAccountComponent {
|
|
|
25795
26254
|
this.router.navigate(["home/user/profile"]);
|
|
25796
26255
|
}
|
|
25797
26256
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CreateAccountComponent, deps: [{ token: HttpService }, { token: MessageService }, { token: DataServiceLib }, { token: AuthService }, { token: i1$1.Router }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
25798
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: CreateAccountComponent, isStandalone: false, selector: "spa-create-account", inputs: { appConfig: "appConfig" }, ngImport: i0, template: "<h4>Create User</h4>\n\n<hr/>\n\n<div class=\"container tin-grid\" style=\"font-size:14px; max-width: 70%;\">\n\n <spa-text id=\"txtUserName\" display=\"Username\" [(value)]=\"register.userName\"></spa-text>\n\n \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"] }] }); }
|
|
26257
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: CreateAccountComponent, isStandalone: false, selector: "spa-create-account", inputs: { appConfig: "appConfig" }, ngImport: i0, template: "<h4>Create User</h4>\n\n<hr/>\n\n<div class=\"container tin-grid\" style=\"font-size:14px; max-width: 70%;\">\n\n <spa-text id=\"txtUserName\" display=\"Username\" [(value)]=\"register.userName\"></spa-text>\n\n \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
26258
|
}
|
|
25800
26259
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CreateAccountComponent, decorators: [{
|
|
25801
26260
|
type: Component,
|
|
@@ -29271,7 +29730,7 @@ class OnboardingComponent {
|
|
|
29271
29730
|
this.dataService.Navigate('home');
|
|
29272
29731
|
}
|
|
29273
29732
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: OnboardingComponent, deps: [{ token: DataServiceLib }, { token: AuthService }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
29274
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: OnboardingComponent, isStandalone: false, selector: "spa-onboarding", ngImport: i0, template: "<label class=\"title\">Welcome, {{username}}</label>\n\n\n\n<!-- terms -->\n<div class=\"mt-3\" *ngIf=\"step=='terms'\">\n\n <label class=\"subtitle text-muted mb-2\" >We care about our users and are dedicated to protecting your data and privacy -\n thats why we want to be clear about what data we collect and how we use it to improve your experience.</label>\n\n <br>\n <spa-check display=\"I agree to the Terms and Privacy Policy\" [(value)]=\"agree\"></spa-check>\n</div>\n\n<!-- owner -->\n<div class=\"mt-3\" *ngIf=\"step=='name' && own\">\n\n <label class=\"subtitle text-muted\" style=\" margin-bottom: 20px;\">The follow steps will guide you to customise your application.</label>\n\n <div style=\"max-width: 400px;\">\n <spa-text display=\"Organisation Name\" [(value)]=\"myTenant.name\" ></spa-text>\n </div>\n\n <label class=\"text-muted\" style=\" font-size: 12px;\">You can change the Organisation's name to your team or company name.</label><br>\n <label class=\"text-muted\" style=\" font-size: 12px;margin-top: 10px;\">The name can be changed later.</label>\n\n</div>\n\n<!-- guest -->\n<div *ngIf=\"step=='hi' && !own\">\n <label class=\"subtitle text-muted\">You are now signed in to {{myTenant.name}}.</label>\n</div>\n\n\n<!-- invitations -->\n<div class=\"mt-3\" *ngIf=\"step=='invitations' && own\">\n\n <label class=\"subtitle text-muted\">You have been requested to join the following organisations. If you accept, you have the option to switch to that org now or stay in you org.</label><br>\n <label class=\"text-muted\" style=\" font-size: 12px;margin-top: 10px;\">You will be able to switch later.</label>\n <spa-invitations-table></spa-invitations-table>\n\n</div>\n\n\n<!-- Actions -->\n<div class=\"mt-3\">\n <button mat-stroked-button color=\"primary\" [disabled]=\"!agree\" (click)=\"next()\">Next <mat-icon>arrow_right_alt</mat-icon></button>\n</div>\n", styles: [".title{margin-top:1em;font-size:28px;font-weight:300}\n"], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: InvitationsTableComponent, selector: "spa-invitations-table" }] }); }
|
|
29733
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: OnboardingComponent, isStandalone: false, selector: "spa-onboarding", ngImport: i0, template: "<label class=\"title\">Welcome, {{username}}</label>\n\n\n\n<!-- terms -->\n<div class=\"mt-3\" *ngIf=\"step=='terms'\">\n\n <label class=\"subtitle text-muted mb-2\" >We care about our users and are dedicated to protecting your data and privacy -\n thats why we want to be clear about what data we collect and how we use it to improve your experience.</label>\n\n <br>\n <spa-check display=\"I agree to the Terms and Privacy Policy\" [(value)]=\"agree\"></spa-check>\n</div>\n\n<!-- owner -->\n<div class=\"mt-3\" *ngIf=\"step=='name' && own\">\n\n <label class=\"subtitle text-muted\" style=\" margin-bottom: 20px;\">The follow steps will guide you to customise your application.</label>\n\n <div style=\"max-width: 400px;\">\n <spa-text display=\"Organisation Name\" [(value)]=\"myTenant.name\" ></spa-text>\n </div>\n\n <label class=\"text-muted\" style=\" font-size: 12px;\">You can change the Organisation's name to your team or company name.</label><br>\n <label class=\"text-muted\" style=\" font-size: 12px;margin-top: 10px;\">The name can be changed later.</label>\n\n</div>\n\n<!-- guest -->\n<div *ngIf=\"step=='hi' && !own\">\n <label class=\"subtitle text-muted\">You are now signed in to {{myTenant.name}}.</label>\n</div>\n\n\n<!-- invitations -->\n<div class=\"mt-3\" *ngIf=\"step=='invitations' && own\">\n\n <label class=\"subtitle text-muted\">You have been requested to join the following organisations. If you accept, you have the option to switch to that org now or stay in you org.</label><br>\n <label class=\"text-muted\" style=\" font-size: 12px;margin-top: 10px;\">You will be able to switch later.</label>\n <spa-invitations-table></spa-invitations-table>\n\n</div>\n\n\n<!-- Actions -->\n<div class=\"mt-3\">\n <button mat-stroked-button color=\"primary\" [disabled]=\"!agree\" (click)=\"next()\">Next <mat-icon>arrow_right_alt</mat-icon></button>\n</div>\n", styles: [".title{margin-top:1em;font-size:28px;font-weight:300}\n"], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage", "hint"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: InvitationsTableComponent, selector: "spa-invitations-table" }] }); }
|
|
29275
29734
|
}
|
|
29276
29735
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: OnboardingComponent, decorators: [{
|
|
29277
29736
|
type: Component,
|
|
@@ -33079,5 +33538,5 @@ const ALSQUARE_SVG_WHITE = `<svg viewBox="0 0 80 80" fill="none" xmlns="http://w
|
|
|
33079
33538
|
* Generated bundle index. Do not edit.
|
|
33080
33539
|
*/
|
|
33081
33540
|
|
|
33082
|
-
export { ALSQUARE_SVG_DARK, ALSQUARE_SVG_WHITE, Account, AccountsComponent as AccountingAccountsComponent, AggregatesComponent as AccountingAggregatesComponent, AgingComponent as AccountingAgingComponent, CreditNotesComponent as AccountingCreditNotesComponent, CurrenciesComponent as AccountingCurrenciesComponent, AccountingDashboardComponent, InvoicesComponent as AccountingInvoicesComponent, AccountingModule, ReportsComponent as AccountingReportsComponent, AccountingService, StatementComponent as AccountingStatementComponent, TransactionTypesComponent as AccountingTransactionTypesComponent, TransactionsComponent as AccountingTransactionsComponent, VatReturnComponent as AccountingVatReturnComponent, Action, ActivityComponent, AdminModule, AgentComponent, AgentPageComponent, AgentService, AlertComponent, AlertConfig, AlertMessage, AnalyticsService, ApiErrorService, ApiResponse, AppConfig, AppConfigurationComponent, AppModelsComponent, AssetStatus, AssetsService, AttachComponent, AuthService, BankReconciliationComponent, BillingPageComponent, BottomTab, BrandsComponent, CacheConfig, CapItem, CapsulesComponent, CategoriesComponent, ChangePasswordComponent, ChangeUserPassword, ChartConfig, ChartsComponent, CheckComponent, ChecklistComponent, ChipsComponent, ConfigService, Constants, Core, CreateAccountComponent, CreditNoteStatus, CustomersComponent, DataServiceLib, DateComponent, DatetimeComponent, DayBookComponent, DepartmentsComponent, DetailsDialog, DetailsDialogConfig, DetailsDialogProcessor, DetailsSource, DialogService, EditorComponent, EmailComponent, EmployeesComponent, ExportService, FeatureDirective, FieldLoadIndicator, FilterComponent, FiscalPeriodStatus, FiscalPeriodsComponent, FormComponent, FormConfig, FormSkeletonComponent, GeneralModule, GeneralService, GradesComponent, GroupsComponent, HRModule, HtmlComponent, HttpService, ImportDialogComponent, IndexModule, InventoryDashboardComponent, InventoryModule, InventoryService, InvitationsTableComponent, InvoiceDashboardComponent, InvoiceItemType, InvoiceStatus, JournalEntryDialogComponent, LabelComponent, LastRouteService, ListDialogComponent, ListDialogConfig, LoaderComponent, LoaderService, LoanPaymentsComponent, LoanProductsComponent, LoansComponent, LoansModule, LoansService, LogLevel, LogService, LoginComponent, LogsComponent, ManufacturingModule, MembershipComponent, MessageService, MoneyComponent, MovementType, NavMenuComponent, NotesComponent, NotesConfig, NotificationsService, NumberComponent, OfflineIndicatorComponent, OfflineService, OnboardingComponent, OptionComponent, OverviewDashboardComponent, OverviewModule, PageComponent, PageConfig, PaginationConfig, PayrollDashboardComponent, PayrollModule, PayrollService, PerceivedProgress, PlansComponent, PositionsComponent, PrivacyDialogComponent, ProductionService, Profile, ProfileComponent, PurchaseStatus, PurchasingDashboardComponent, PurchasingModule, PurchasingService, PushNotificationService, QUIET_DELAY_MS, QUIET_MIN_MS, QuoteStatus, QuotesComponent, ReceiptDialogComponent, ReceiptStatus, ReceiptsComponent, ReconcileDialogComponent, RecoverAccountComponent, Register, RevenueScheduleDialogComponent, RevenueSchedulesComponent, Role, RoleAccess, RolesComponent, SalesDashboardComponent, SalesModule, SalesService, SearchComponent, SearchConfig, SecurityConfig, SelectBitwiseComponent, SelectComponent, SelectLiteComponent, SelectMultiComponent, SettingsComponent, SetupGuideComponent, SetupService, SignupComponent, SignupData, SpaAdminModule, SpaDayBookModule, SpaHomeModule, SpaIndexModule, SpaLandingComponent, SpaMatModule, SpaUserModule, StatementImportDialogComponent, StatusesComponent, Step, StepConfig, StepsComponent, StorageService, SubCategoriesComponent, SubscriptionPageComponent, SubscriptionService, SuppliersComponent, SyncComponent, TIN_SILENT_REQUEST, TIN_SPA_RUNTIME_CONFIG, TabService, TableComponent, TableConfig, TabsComponent, TasksComponent, TenancyModule, TenantsComponent, TermsDialogComponent, TextAreaComponent, TextComponent, TextMaskComponent, TextMultiComponent, TextSingleComponent, TileConfig, TilesComponent, TinSpaComponent, TinSpaModule, TinSpaService, TitleActionsComponent, TransactionTiming, UnitOfMeasure, UpdateService, User, UserModule, UsersComponent, ViewerComponent, WelcomeComponent, WorkflowModule, authGuard, dialogOptions, featureGuard, getTinSpaAppSettings, isSilentRequest, loadTinSpaAppSettings, loginConfig, messageDialog, provideTinSpaRuntime, resolveQuietLoading, silentContext, tinSpaLocationStrategyFactory, tinSpaMsalInstanceFactory, tinSpaRuntimeConfigFactory, viewerDialog };
|
|
33541
|
+
export { ALSQUARE_SVG_DARK, ALSQUARE_SVG_WHITE, Account, AccountsComponent as AccountingAccountsComponent, AggregatesComponent as AccountingAggregatesComponent, AgingComponent as AccountingAgingComponent, CreditNotesComponent as AccountingCreditNotesComponent, CurrenciesComponent as AccountingCurrenciesComponent, AccountingDashboardComponent, InvoicesComponent as AccountingInvoicesComponent, AccountingModule, ReportsComponent as AccountingReportsComponent, AccountingService, StatementComponent as AccountingStatementComponent, TransactionTypesComponent as AccountingTransactionTypesComponent, TransactionsComponent as AccountingTransactionsComponent, VatReturnComponent as AccountingVatReturnComponent, Action, ActivityComponent, AdminModule, AgentComponent, AgentPageComponent, AgentService, AlertComponent, AlertConfig, AlertMessage, AnalyticsService, ApiErrorService, ApiResponse, AppConfig, AppConfigurationComponent, AppModelsComponent, AssetStatus, AssetsService, AttachComponent, AuthService, BankReconciliationComponent, BillingPageComponent, BottomTab, BrandsComponent, CacheConfig, CapItem, CapsulesComponent, CategoriesComponent, ChangePasswordComponent, ChangeUserPassword, ChartConfig, ChartsComponent, CheckComponent, ChecklistComponent, ChipsComponent, ConfigService, Constants, Core, CreateAccountComponent, CreditNoteStatus, CustomersComponent, DataServiceLib, DateComponent, DatetimeComponent, DayBookComponent, DepartmentsComponent, DetailsDialog, DetailsDialogConfig, DetailsDialogProcessor, DetailsSource, DialogService, EditorComponent, EmailComponent, EmployeesComponent, ExportService, FeatureDirective, FieldLoadIndicator, FilterComponent, FiscalPeriodStatus, FiscalPeriodsComponent, FormComponent, FormConfig, FormSkeletonComponent, GeneralModule, GeneralService, GradesComponent, GroupsComponent, HRModule, HtmlComponent, HttpService, ImportDialogComponent, IndexModule, InventoryDashboardComponent, InventoryModule, InventoryService, InvitationsTableComponent, InvoiceDashboardComponent, InvoiceItemType, InvoiceStatus, JournalEntryDialogComponent, LabelComponent, LastRouteService, ListDialogComponent, ListDialogConfig, LoaderComponent, LoaderService, LoanPaymentsComponent, LoanProductsComponent, LoansComponent, LoansModule, LoansService, LogLevel, LogService, LoginComponent, LogsComponent, MONOGRAM_TONES, ManufacturingModule, MembershipComponent, MessageService, MoneyComponent, MonogramComponent, MovementType, NavMenuComponent, NotesComponent, NotesConfig, NotificationsService, NumberComponent, OfflineIndicatorComponent, OfflineService, OnboardingComponent, OptionComponent, OverviewDashboardComponent, OverviewModule, PageComponent, PageConfig, PaginationConfig, PayrollDashboardComponent, PayrollModule, PayrollService, PerceivedProgress, PlansComponent, PositionsComponent, PrivacyDialogComponent, ProductionService, Profile, ProfileComponent, PurchaseStatus, PurchasingDashboardComponent, PurchasingModule, PurchasingService, PushNotificationService, QUIET_DELAY_MS, QUIET_MIN_MS, QuoteStatus, QuotesComponent, ReceiptDialogComponent, ReceiptStatus, ReceiptsComponent, ReconcileDialogComponent, RecoverAccountComponent, Register, RevenueScheduleDialogComponent, RevenueSchedulesComponent, Role, RoleAccess, RolesComponent, SalesDashboardComponent, SalesModule, SalesService, SearchComponent, SearchConfig, SecurityConfig, SelectBitwiseComponent, SelectComponent, SelectLiteComponent, SelectMultiComponent, SettingsComponent, SetupGuideComponent, SetupService, SignupComponent, SignupData, SpaAdminModule, SpaDayBookModule, SpaHomeModule, SpaIndexModule, SpaLandingComponent, SpaMatModule, SpaUserModule, StatementImportDialogComponent, StatusesComponent, Step, StepConfig, StepsComponent, StorageService, SubCategoriesComponent, SubscriptionPageComponent, SubscriptionService, SuppliersComponent, SyncComponent, TIN_SILENT_REQUEST, TIN_SPA_RUNTIME_CONFIG, TabService, TableComponent, TableConfig, TabsComponent, TasksComponent, TenancyModule, TenantsComponent, TermsDialogComponent, TextAreaComponent, TextComponent, TextMaskComponent, TextMultiComponent, TextSingleComponent, TileConfig, TilesComponent, TinSpaComponent, TinSpaModule, TinSpaService, TitleActionsComponent, TransactionTiming, UnitOfMeasure, UpdateService, User, UserModule, UsersComponent, ViewerComponent, WelcomeComponent, WorkflowModule, authGuard, dialogOptions, featureGuard, getTinSpaAppSettings, isSilentRequest, loadTinSpaAppSettings, loginConfig, messageDialog, monogramInitials, monogramKind, monogramPaletteIndex, provideTinSpaRuntime, resolveQuietLoading, silentContext, tinSpaLocationStrategyFactory, tinSpaMsalInstanceFactory, tinSpaRuntimeConfigFactory, viewerDialog };
|
|
33083
33542
|
//# sourceMappingURL=tin-spa.mjs.map
|