tin-spa 20.14.32 → 20.14.40
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 +845 -123
- package/fesm2022/tin-spa.mjs.map +1 -1
- package/index.d.ts +141 -8
- package/package.json +1 -1
package/fesm2022/tin-spa.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { HttpContextToken, HttpContext, HttpHeaders, HttpClientModule, HTTP_INTE
|
|
|
8
8
|
import * as i1$1 from '@angular/router';
|
|
9
9
|
import { NavigationEnd, Router, RouterModule } from '@angular/router';
|
|
10
10
|
import Dexie, { liveQuery } from 'dexie';
|
|
11
|
-
import { map, tap, catchError, mergeMap, filter, debounceTime, startWith, finalize as finalize$1, take, switchMap as switchMap$1 } from 'rxjs/operators';
|
|
11
|
+
import { map, tap, catchError, mergeMap, filter, shareReplay, debounceTime, startWith, finalize as finalize$1, take, timeout as timeout$1, distinctUntilChanged, switchMap as switchMap$1 } from 'rxjs/operators';
|
|
12
12
|
import * as i1$2 from '@angular/common';
|
|
13
13
|
import { PathLocationStrategy, HashLocationStrategy, PlatformLocation, LocationStrategy, APP_BASE_HREF, DecimalPipe, CommonModule, CurrencyPipe, DatePipe } from '@angular/common';
|
|
14
14
|
import * as i10$1 from '@azure/msal-angular';
|
|
@@ -432,6 +432,30 @@ class Core {
|
|
|
432
432
|
const onlyNumbers = value.replace(/[^\d.-]/g, '');
|
|
433
433
|
return onlyNumbers;
|
|
434
434
|
}
|
|
435
|
+
// Added: THE multi-value contract — the one place that knows how a multi-select value is shaped.
|
|
436
|
+
// spa-select-multi and spa-text-multi publish their selection as a ';'-delimited string (updateValue does
|
|
437
|
+
// selectedValues.join(';')), and every consumer that needs the parts back — a button's onClick converting to
|
|
438
|
+
// a List<int> DTO, a component re-reading its own bound value — used to re-implement the split inline. That
|
|
439
|
+
// drifted, expensively: piglet's two sale buttons split on ',' instead of ';', so "12;13" parsed to [NaN] →
|
|
440
|
+
// JSON [null] → a 400 from model binding, and NO sale row was ever written. A single selection contains no
|
|
441
|
+
// delimiter, so the one-pig case worked and hid the bug from every test anyone thought to run. Written once.
|
|
442
|
+
//
|
|
443
|
+
// Accepts all three shapes that legitimately reach a consumer:
|
|
444
|
+
// 'a;b' the documented wire shape
|
|
445
|
+
// 12 a bare scalar — one selection that was never stringified
|
|
446
|
+
// [1, 2] an array — any form whose DTO takes a List<int> rewrites the bound property in place, and that
|
|
447
|
+
// array flows straight back down [(value)] on the next change-detection pass
|
|
448
|
+
// Every part is trimmed and empties are dropped, so 'a; ;b' and ' a ;b ' both give ['a','b'].
|
|
449
|
+
// null/undefined/''/0 give [] — the established falsy-means-nothing-selected rule, preserved deliberately.
|
|
450
|
+
// ⚠️ A value CONTAINING ';' is not representable: join(';') is lossy and no escape is defined. The shape is
|
|
451
|
+
// for ids and codes, never free text the operator can type a semicolon into.
|
|
452
|
+
static parseMultiValue(raw) {
|
|
453
|
+
if (Array.isArray(raw))
|
|
454
|
+
return raw.filter(v => v !== null && v !== undefined && String(v).trim() !== '').map(v => String(v).trim());
|
|
455
|
+
if (!raw)
|
|
456
|
+
return [];
|
|
457
|
+
return String(raw).split(';').filter(v => v.trim() !== '').map(v => v.trim()); // String() first, so a bare scalar never throws "split is not a function"
|
|
458
|
+
}
|
|
435
459
|
static getFirstDayOfMonth() {
|
|
436
460
|
const date = new Date();
|
|
437
461
|
return new Date(date.getFullYear(), date.getMonth(), 1);
|
|
@@ -454,20 +478,13 @@ class Core {
|
|
|
454
478
|
}
|
|
455
479
|
}
|
|
456
480
|
static isValidEmailList(list) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
else {
|
|
466
|
-
if (this.emailIsValid(list) == false) {
|
|
467
|
-
return false;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
return true;
|
|
481
|
+
// Changed: was an inline split(";") behind an includes(";") branch — parseMultiValue is the one owner of the
|
|
482
|
+
// delimiter now, and because it TRIMS, "a@b.com; c@d.com" no longer fails on the space after the semicolon
|
|
483
|
+
// (the previous code validated " c@d.com" verbatim, which no email pattern accepts — a real false negative).
|
|
484
|
+
const emails = this.parseMultiValue(list);
|
|
485
|
+
if (!emails.length)
|
|
486
|
+
return this.emailIsValid(list); // preserves the old behaviour for ''/null — an empty list is not silently "valid"
|
|
487
|
+
return emails.every(email => this.emailIsValid(email));
|
|
471
488
|
}
|
|
472
489
|
static { this.nullDate = "01 Jan 1969"; }
|
|
473
490
|
static isNumber(value) {
|
|
@@ -3730,6 +3747,7 @@ class DataServiceLib {
|
|
|
3730
3747
|
this.capEmployees = new CapItem;
|
|
3731
3748
|
this.capDepartments = new CapItem;
|
|
3732
3749
|
this.capPositions = new CapItem;
|
|
3750
|
+
this.capUnlock = new CapItem; // Added: cap87 — the ILock unlock gate, checked by the BASE controller rather than by any one screen
|
|
3733
3751
|
this.capGrades = new CapItem;
|
|
3734
3752
|
this.capPayroll = new CapItem; // Changed: Added Payroll module sidebar items
|
|
3735
3753
|
this.capSalaryStructures = new CapItem;
|
|
@@ -3816,6 +3834,11 @@ class DataServiceLib {
|
|
|
3816
3834
|
includeAudit: true,
|
|
3817
3835
|
};
|
|
3818
3836
|
this.editDepartmentButton = { name: 'edit', dialog: true, action: { url: 'departments?action=edit', method: 'post', successMessage: 'Edited Successfully' } };
|
|
3837
|
+
// Added: releases the lock on a locked Department row. `visible` reads the `locked` field the controller
|
|
3838
|
+
// projects (DepartmentsController:41), so the button exists only where it can do something. The capability is
|
|
3839
|
+
// passed as the OBJECT, not `.name` — datalib assigns "cap87" in the constructor, after this initializer runs,
|
|
3840
|
+
// so an eagerly-read name would be "" and gate nothing. Full mirrors the server's own bar exactly.
|
|
3841
|
+
this.unlockDepartmentButton = { name: 'unlock', display: 'Unlock', tip: 'Unlock Record', icon: { name: 'lock_open' }, visible: x => x.locked, capability: this.capUnlock, requiredAccess: RoleAccess.Full, confirm: { message: 'Unlock this record? It will become editable and deletable.', confirmLabel: 'Unlock', cancelLabel: 'Keep locked' }, action: { url: 'departments?action=unlock', method: 'post' } };
|
|
3819
3842
|
this.departmentDetailsConfig = {
|
|
3820
3843
|
formConfig: this.departmentFormConfig,
|
|
3821
3844
|
heroField: 'departmentID',
|
|
@@ -3835,6 +3858,8 @@ class DataServiceLib {
|
|
|
3835
3858
|
includeAudit: true,
|
|
3836
3859
|
};
|
|
3837
3860
|
this.editPositionButton = { name: 'edit', dialog: true, action: { url: 'positions?action=edit', method: 'post', successMessage: 'Edited Successfully' } };
|
|
3861
|
+
// Added: the Position twin of unlockDepartmentButton — same gate, same shape, its own URL. `locked` is projected at PositionsController:42
|
|
3862
|
+
this.unlockPositionButton = { name: 'unlock', display: 'Unlock', tip: 'Unlock Record', icon: { name: 'lock_open' }, visible: x => x.locked, capability: this.capUnlock, requiredAccess: RoleAccess.Full, confirm: { message: 'Unlock this record? It will become editable and deletable.', confirmLabel: 'Unlock', cancelLabel: 'Keep locked' }, action: { url: 'positions?action=unlock', method: 'post' } };
|
|
3838
3863
|
this.positionDetailsConfig = {
|
|
3839
3864
|
formConfig: this.positionFormConfig,
|
|
3840
3865
|
heroField: 'positionID',
|
|
@@ -4089,6 +4114,7 @@ class DataServiceLib {
|
|
|
4089
4114
|
},
|
|
4090
4115
|
this.editPositionButton,
|
|
4091
4116
|
{ name: 'delete', dialog: true, action: { url: 'positions?action=delete', method: 'post' } },
|
|
4117
|
+
this.unlockPositionButton, // Added: last, beside delete — hidden on every unlocked row, so the action column is unchanged for normal data
|
|
4092
4118
|
],
|
|
4093
4119
|
loadAction: { url: 'positions/all/x' },
|
|
4094
4120
|
formConfig: this.positionFormConfig,
|
|
@@ -4132,6 +4158,7 @@ class DataServiceLib {
|
|
|
4132
4158
|
},
|
|
4133
4159
|
this.editDepartmentButton,
|
|
4134
4160
|
{ name: 'delete', dialog: true, action: { url: 'departments?action=delete', method: 'post' } },
|
|
4161
|
+
this.unlockDepartmentButton, // Added: last, beside delete — hidden on every unlocked row, so the action column is unchanged for normal data
|
|
4135
4162
|
],
|
|
4136
4163
|
loadAction: { url: 'departments/all/x' },
|
|
4137
4164
|
formConfig: this.departmentFormConfig,
|
|
@@ -4735,7 +4762,17 @@ class DataServiceLib {
|
|
|
4735
4762
|
this.capHR.moduleKey = "hr"; // Added (Setup v3)
|
|
4736
4763
|
this.capHR.link = "home/hr/employees";
|
|
4737
4764
|
this.capHR.icon = "diversity_3";
|
|
4738
|
-
this.capHR.capSubItems = [this.capDepartments, this.capPositions, this.capEmployees,];
|
|
4765
|
+
this.capHR.capSubItems = [this.capDepartments, this.capPositions, this.capUnlock, this.capEmployees,]; // Changed: capUnlock sits beside the two ILock entities it releases, so the grant is found where it is used
|
|
4766
|
+
// Added: cap87 — releases the ILock protection (`POST /api/{entity}?action=unlock`, TinWeb 51f30e4). It is a
|
|
4767
|
+
// PERMISSION, not a page: no link, and showMenu = false keeps it out of every nav rendering while the Roles
|
|
4768
|
+
// screen (which does not filter on showMenu) still renders its access level. It lives under HR because the
|
|
4769
|
+
// only two ILock entities today are Department (cap19) and Position (cap20), but the server checks it on the
|
|
4770
|
+
// BASE controller, so a future ILock entity anywhere is covered by this one grant rather than a new column.
|
|
4771
|
+
// Deliberately in NO role template — Owner/Admin hold it because they hold every cap; anyone else is granted
|
|
4772
|
+
// it on purpose, which is the point of gating an irreversible action.
|
|
4773
|
+
this.capUnlock.name = "cap87";
|
|
4774
|
+
this.capUnlock.display = "Unlock Records";
|
|
4775
|
+
this.capUnlock.showMenu = false;
|
|
4739
4776
|
this.capEmployees.name = "cap18";
|
|
4740
4777
|
this.capEmployees.display = "Employees";
|
|
4741
4778
|
this.capEmployees.link = "home/hr/employees";
|
|
@@ -5085,6 +5122,13 @@ class DataServiceLib {
|
|
|
5085
5122
|
get modernDialogs() {
|
|
5086
5123
|
return this.appConfig?.dialogStyle === 'modern';
|
|
5087
5124
|
}
|
|
5125
|
+
// Added: the single place the library asks "may I name my own destructive prompts?". Read at prompt time
|
|
5126
|
+
// rather than cached, for the same reason as modernDialogs — an app assigns appConfig in its own service
|
|
5127
|
+
// constructor. Absent means false, so naming is a deliberate act and never an accident: an untracked consumer
|
|
5128
|
+
// that has never heard of this flag keeps getting "Yes" / "No" on every built-in delete confirmation.
|
|
5129
|
+
get namedDestructiveActions() {
|
|
5130
|
+
return this.appConfig?.namedDestructiveActions === true;
|
|
5131
|
+
}
|
|
5088
5132
|
// Added: panel class for the overlay pane. Dialogs apply this so surface-level styling (radius, shadow,
|
|
5089
5133
|
// header band) can live in tin-styles.css, which every consumer app already loads globally — component CSS
|
|
5090
5134
|
// cannot reach the Material surface because it is an ancestor of the component's own view.
|
|
@@ -5370,6 +5414,16 @@ class messageDialog {
|
|
|
5370
5414
|
this.data = data;
|
|
5371
5415
|
this.dataService = dataService;
|
|
5372
5416
|
this.modern = false; // Added: appConfig.dialogStyle === 'modern'; false keeps the long-standing presentation
|
|
5417
|
+
// Added: this confirmation destroys or undoes something. Drives COLOUR ONLY — never the labels, never the ids,
|
|
5418
|
+
// never whether the buttons exist. Default false, so a caller that says nothing renders exactly as before.
|
|
5419
|
+
//
|
|
5420
|
+
// Why colour has to key off this rather than off button position: the classic style hardcodes `color: green`
|
|
5421
|
+
// on the affirmative button and `color: red` on the dismissive one, an assumption that only holds while the
|
|
5422
|
+
// buttons read "Yes" and "No" and the affirmative answer is the harmless one. The moment the affirmative
|
|
5423
|
+
// button is NAMED — "Delete" — that mapping renders a green Delete beside a red Keep, i.e. it points the
|
|
5424
|
+
// user at the destructive action and warns them off the safe one. Destructiveness is a property of the
|
|
5425
|
+
// ACTION, so that is what the colour must follow.
|
|
5426
|
+
this.destructive = false;
|
|
5373
5427
|
// Added: splits the body into paragraphs on blank lines. The messages themselves already separate "what
|
|
5374
5428
|
// happened" from "what to do next" that way, but the old template rendered the whole thing as one run of
|
|
5375
5429
|
// text — which is precisely why nobody read past the first line. The first paragraph is styled as a lead,
|
|
@@ -5387,12 +5441,16 @@ class messageDialog {
|
|
|
5387
5441
|
this._confirmLabel = this.data.confirmLabel; // Added: optional caller-supplied action label for the affirmative button
|
|
5388
5442
|
this._cancelLabel = this.data.cancelLabel; // Added: optional caller-supplied label for the dismissive button
|
|
5389
5443
|
this._okLabel = this.data.okLabel; // Added: optional caller-supplied label for the acknowledge button
|
|
5444
|
+
this.destructive = this.data.destructive === true; // Added: strict true — an absent flag must never colour a prompt as dangerous
|
|
5390
5445
|
// Added: resolve the style once, at open time. The panel class carries surface-level styling (radius,
|
|
5391
5446
|
// shadow) that component CSS cannot reach, because the Material surface is an ancestor of this view.
|
|
5392
5447
|
this.modern = !!this.dataService?.modernDialogs;
|
|
5393
5448
|
if (this.modern)
|
|
5394
5449
|
this.dialogRef.addPanelClass('tin-dialog-modern');
|
|
5395
5450
|
}
|
|
5451
|
+
// Added: for a destructive confirm the icon becomes a warning rather than the neutral question mark. The
|
|
5452
|
+
// question mark is right for "Switch organisation?"; it is too quiet for "Delete this shelter?".
|
|
5453
|
+
get iconName() { return this.isConfirm && this.destructive ? 'warning_amber' : this.spec.icon; }
|
|
5396
5454
|
// Added: per-type presentation. Kept as a lookup rather than scattered *ngIf branches so a new type is one
|
|
5397
5455
|
// entry here instead of an edit in three places — the old template hardcoded its heading per type, which is
|
|
5398
5456
|
// exactly how the caller-supplied subject came to be silently dropped for errors.
|
|
@@ -5404,7 +5462,6 @@ class messageDialog {
|
|
|
5404
5462
|
confirm: { icon: 'help_outline', title: 'Just confirming' },
|
|
5405
5463
|
}; }
|
|
5406
5464
|
get spec() { return messageDialog.TYPES[this.messageType] ?? messageDialog.TYPES.info; }
|
|
5407
|
-
get iconName() { return this.spec.icon; }
|
|
5408
5465
|
// Changed: the caller's subject now wins for EVERY type. Previously the heading was hardcoded per type and
|
|
5409
5466
|
// the subject was only rendered for 'info', so a friendly title like "No connection" never appeared on an
|
|
5410
5467
|
// error — the user saw the bare word "Error" instead.
|
|
@@ -5450,11 +5507,11 @@ class messageDialog {
|
|
|
5450
5507
|
this.dialogRef.close(resp);
|
|
5451
5508
|
}
|
|
5452
5509
|
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 }); }
|
|
5453
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: messageDialog, isStandalone: false, selector: "lib-app-message", ngImport: i0, template: "<!-- Changed: two presentations behind one component. `modern` (appConfig.dialogStyle === 'modern') renders the\n structured layout; anything else renders the long-standing look, so an app that never opts in still gets a\n familiar dialog. Changed: classic is a SUPPORTED escape hatch, not frozen legacy \u2014 it is expected to render\n the same information as modern, and its three rendering bugs were repaired on 2026-08-06. Button ids\n (btnYes / btnNo / btnOK) are identical in BOTH branches \u2014 existing E2E specs select on them and must not\n care which style is active. -->\n\n<ng-container *ngIf=\"modern; else classic\">\n\n <!-- Rebuilt from a hardcoded per-type heading plus one undifferentiated paragraph into a structured layout \u2014\n icon chip, real title, lead paragraph, supporting detail. The type drives colour via a single class on\n the root, so adding a type is one entry in the component rather than three template branches. -->\n <div class=\"tin-msg\" [ngClass]=\"'tin-msg--' + messageType\">\n\n <div class=\"tin-msg__head\">\n <!-- The icon carries the type at a glance. It sits in a soft tinted disc rather than on a saturated\n banner: the intent is to inform, not to alarm someone who has usually done nothing wrong. -->\n <div class=\"tin-msg__icon\">\n <mat-icon>{{ iconName }}</mat-icon>\n </div>\n <h2 class=\"tin-msg__title\">{{ title }}</h2>\n </div>\n\n <mat-dialog-content class=\"tin-msg__body\">\n <!-- First paragraph is the lead (what happened); the rest is supporting detail (what to do next).\n Splitting on blank lines is what gives the eye somewhere to land \u2014 the old single run of text is\n why nobody read past the first line. -->\n <p *ngFor=\"let p of paragraphs; let first = first\" class=\"tin-msg__p\" [class.tin-msg__p--lead]=\"first\">{{ p }}</p>\n\n <!-- The backend's correlation code. Deliberately the quietest thing in the dialog \u2014 the user does not\n need to act on it, they only need to be able to read it out if they call support, at which point it\n turns a \"something broke this morning\" report into one log row. Hidden entirely when absent. -->\n <p class=\"tin-msg__ref\" *ngIf=\"reference\">If you contact your administrator, quote reference <span class=\"tin-msg__ref-code\">{{ reference }}</span>.</p>\n </mat-dialog-content>\n\n <mat-dialog-actions class=\"tin-msg__actions\">\n <!-- Changed: labels reverted to \"No\" / \"Yes\" (owner's call, 2026-08-06). The redesign had shipped them as\n \"Cancel\" / \"Yes, continue\"; these are the most-clicked buttons in the app and the original wording is\n what everybody here reads. Text only \u2014 the ids btnNo / btnYes are untouched and identical to the\n classic branch, so every E2E selector (#btnYes) and every caller is unaffected.\n Changed: the literal text is now {{ confirmLabel }} / {{ cancelLabel }} / {{ okLabel }}, which STILL\n render \"Yes\" / \"No\" / \"OK\" unless the caller passes a label. A caller that knows it is confirming a\n destructive action can name it (\"Delete\") so it no longer reads identically to a harmless one. -->\n <ng-container *ngIf=\"isConfirm; else modernAcknowledge\">\n <button id=\"btnNo\" mat-button class=\"tin-msg__btn-quiet\" (click)=\"response('no')\">{{ cancelLabel }}</button>\n <button id=\"btnYes\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('yes')\" cdkFocusInitial>{{ confirmLabel }}</button>\n </ng-container>\n <ng-template #modernAcknowledge>\n <button id=\"btnOK\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('ok')\" cdkFocusInitial>{{ okLabel }}</button>\n </ng-template>\n </mat-dialog-actions>\n\n </div>\n\n</ng-container>\n\n\n<!-- Classic: the original LOOK, kept deliberately as the escape hatch (owner's call, 2026-08-06 \u2014 retire was\n recommended and declined). It is the original markup plus FIXES, never style changes, because reverting a\n bug alongside a look would be a regression. It renders the same INFORMATION as the modern branch, in its\n own visual language:\n 1. the caller's subject is used as the heading when one was supplied. The old template hardcoded the\n heading per type and rendered the subject for 'info' only, which is why a friendly title like\n \"No connection\" showed up as the bare word \"Error\".\n 2. 'warning' and 'success' are handled. They did not exist when this markup was written, so they would\n otherwise fall through every branch and render an empty dialog.\n 3. 'error' now has an icon; the reference line is no longer trapped in the error-only branch; and the\n body renders real paragraphs instead of one collapsed run of text.\n \u26A0\uFE0F Anything added to the modern branch above has to be considered here too \u2014 that omission is exactly how\n the three bugs in 3. got here. -->\n\n<ng-template #classic>\n <div class=\"tin-msg-classic\">\n\n <h2>\n <div class=\"tin-between\">\n <mat-label>{{ classicHeading }}</mat-label>\n <!-- Added: the missing 'error' icon. Every other type had one; error \u2014 the type that most needs a cue \u2014\n rendered its heading beside empty flex space. Colour matches the modern branch's error accent\n (#c62828) so the two presentations agree on what \"error\" looks like. -->\n <mat-icon *ngIf=\"messageType=='error'\" style=\"color: #c62828;\">error</mat-icon>\n <mat-icon *ngIf=\"messageType=='confirm'\">question_mark</mat-icon>\n <mat-icon *ngIf=\"messageType=='info'\" style=\"color: steelblue;\">info</mat-icon>\n <mat-icon *ngIf=\"messageType=='warning'\" style=\"color: #ef6c00;\">warning</mat-icon>\n <mat-icon *ngIf=\"messageType=='success'\" style=\"color: #2e7d32;\">check_circle</mat-icon>\n </div>\n </h2>\n\n <mat-dialog-content>\n\n <!-- Changed: was `{{ _messageDetails }}` rendered raw, so the blank-line-separated paragraphs the messages\n are actually written in (api-error.service.ts) collapsed into one undifferentiated run of text. That\n collapse is the original complaint that started WS-7 and it was still live here. Now renders the same\n `paragraphs` split the modern branch uses \u2014 same information, classic's own plain markup. -->\n <div *ngIf=\"messageType=='error'\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <div *ngIf=\"messageType!='error'\" class=\"alert alert-secondary\" role=\"alert\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <!-- Changed: lifted OUT of the error-only branch above. The reference is a correlation code, not an\n error-specific field \u2014 a warning or info that carried one used to drop it silently. -->\n <div class=\"tin-msg-classic__ref\" *ngIf=\"reference\">Reference: {{ reference }}</div>\n\n </mat-dialog-content>\n\n <mat-dialog-actions>\n\n <!-- Changed: same caller-supplied labels as the modern branch, defaulting identically to \"Yes\" / \"No\" /\n \"OK\". Classic is expected to render the same INFORMATION as modern in its own visual language, so a\n caller that names its action must be honoured here too \u2014 otherwise the next person to pass a label\n would silently get \"Yes\" on every untracked consumer still running classic. Colours and ids untouched. -->\n <button id=\"btnYes\" mat-stroked-button style=\"color: green;\" *ngIf=\"isConfirm\" (click)=\"response('yes')\" cdkFocusInitial>{{ confirmLabel }}</button>\n\n <button id=\"btnNo\" mat-stroked-button style=\"color: red;\" *ngIf=\"isConfirm\" (click)=\"response('no')\">{{ cancelLabel }}</button>\n\n <button id=\"btnOK\" mat-stroked-button *ngIf=\"!isConfirm\" color=\"primary\" (click)=\"response('ok')\" cdkFocusInitial>{{ okLabel }}</button>\n\n </mat-dialog-actions>\n\n </div>\n</ng-template>\n", styles: [":host{display:block}.tin-msg-classic h2{margin:0!important;padding:16px 16px 0!important;font-size:20px!important;font-weight:500!important;line-height:normal!important}.tin-msg-classic .tin-between{display:flex;justify-content:space-between;align-items:center;margin:0!important;padding:0!important}.tin-msg-classic__p{margin:0 0 10px}.tin-msg-classic__p:last-child{margin-bottom:0}.tin-msg-classic__ref{margin-top:10px;font-size:12px;color:#0000008c;letter-spacing:.04em}.tin-msg{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1);--tin-title: rgba(0, 0, 0, .87);--tin-body: rgba(0, 0, 0, .6);position:relative;padding:4px 4px 0}.tin-msg:before{content:\"\";position:absolute;top:0;left:0;right:0;height:3px;background:var(--tin-accent);border-radius:3px 3px 0 0}.tin-msg--error{--tin-accent: #c62828;--tin-accent-tint: rgba(198, 40, 40, .1)}.tin-msg--warning{--tin-accent: #ef6c00;--tin-accent-tint: rgba(239, 108, 0, .12)}.tin-msg--info{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1)}.tin-msg--success{--tin-accent: #2e7d32;--tin-accent-tint: rgba(46, 125, 50, .12)}.tin-msg--confirm{--tin-accent: #5e35b1;--tin-accent-tint: rgba(94, 53, 177, .1)}.tin-msg__head{display:flex;align-items:center;gap:14px;padding:20px 20px 8px}.tin-msg__icon{flex:0 0 auto;width:40px;height:40px;border-radius:50%;background:var(--tin-accent-tint);display:inline-flex;align-items:center;justify-content:center}.tin-msg__icon .mat-icon,.tin-msg__icon mat-icon{color:var(--tin-accent);font-size:22px;width:22px;height:22px;line-height:22px}.tin-msg__title{margin:0!important;padding:0!important;font-size:18px!important;font-weight:600!important;line-height:1.3!important;letter-spacing:-.1px;color:var(--tin-title)}.tin-msg__body{padding:0 20px 4px 74px!important;max-height:60vh}.tin-msg__p{margin:0 0 12px;font-size:13.5px;line-height:1.6;color:var(--tin-body)}.tin-msg__p--lead{font-size:14.5px;font-weight:500;color:var(--tin-title)}.tin-msg__p:last-child{margin-bottom:0}.tin-msg__ref{margin:14px 0 0;padding-top:10px;border-top:1px solid rgba(0,0,0,.08);font-size:12px;line-height:1.5;color:var(--tin-muted, rgba(0, 0, 0, .55))}.tin-msg__ref-code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;letter-spacing:.06em;color:var(--tin-title, rgba(0, 0, 0, .8));white-space:nowrap;-webkit-user-select:all;user-select:all}.tin-msg__actions{display:flex;justify-content:flex-end;gap:8px;padding:12px 20px 16px!important;margin:0!important;min-height:0!important}.tin-msg__btn-primary{background:var(--tin-accent)!important;color:#fff!important;min-width:88px;font-weight:500;letter-spacing:.2px}.tin-msg__btn-quiet{color:var(--tin-body)!important;font-weight:500}.tin-msg__icon{animation:tin-msg-pop .22s cubic-bezier(.2,.7,.3,1) both}@keyframes tin-msg-pop{0%{transform:scale(.82);opacity:0}to{transform:scale(1);opacity:1}}@media (prefers-reduced-motion: reduce){.tin-msg__icon{animation:none}}@media (max-width: 480px){.tin-msg__head{padding:16px 16px 8px;gap:12px}.tin-msg__body{padding:0 16px 4px!important}.tin-msg__actions{padding:12px 16px 14px!important}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i4.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i4.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }] }); }
|
|
5510
|
+
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 <!-- Changed: `tin-msg--destructive` re-points the single --tin-accent variable at the warn red. Because every\n accented part (the top rule, the icon disc, the primary button) already reads from that one variable, the\n whole dialog turns to a warning in one class instead of three overrides \u2014 and the affirmative button, which\n is the thing the user is about to press, can no longer come out purple or green on a \"Delete\". -->\n <div class=\"tin-msg\" [ngClass]=\"'tin-msg--' + messageType\" [class.tin-msg--destructive]=\"isConfirm && destructive\">\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 <!-- Changed: on a DESTRUCTIVE confirm the initial focus moves to the SAFE button. The affirmative button is\n still #btnYes in the same position with the same click handler \u2014 only the keyboard default changes, so\n hitting Enter on reflex now keeps the record instead of destroying it. Every E2E locator clicks #btnYes\n explicitly (helpers/form.helper.ts acceptConfirmIfPresent uses page.locator('#btnYes')), so none of them\n depend on which button holds focus. The colour comes from tin-msg--destructive on the root, not from\n anything here \u2014 the primary button reads --tin-accent either way. -->\n <ng-container *ngIf=\"isConfirm; else modernAcknowledge\">\n <button id=\"btnNo\" mat-button class=\"tin-msg__btn-quiet\" [class.tin-msg__btn-safe]=\"destructive\" (click)=\"response('no')\" [attr.cdkFocusInitial]=\"destructive ? '' : null\">{{ cancelLabel }}</button>\n <button id=\"btnYes\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('yes')\" [attr.cdkFocusInitial]=\"destructive ? null : ''\">{{ 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 <!-- Changed: a destructive confirm shows a red warning instead of the neutral question mark, matching the\n modern branch. Same #c62828 as the error icon beside it, so the two styles agree on what danger looks\n like. A plain confirm (\"Switch organisation?\") is unchanged. -->\n <mat-icon *ngIf=\"messageType=='confirm' && destructive\" style=\"color: #c62828;\">warning</mat-icon>\n <mat-icon *ngIf=\"messageType=='confirm' && !destructive\">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 <!-- \u26A0\uFE0F Changed: THE COLOUR NOW FOLLOWS THE ACTION, NOT THE BUTTON'S POSITION. This markup hardcoded\n `color: green` on the affirmative button and `color: red` on the dismissive one. That mapping is only\n correct while the buttons read \"Yes\" and \"No\" and the affirmative answer is the harmless one \u2014 the\n green was never saying \"this is safe\", it was saying \"this is the Yes\". Name the action and the\n assumption inverts: a confirm offering \"Keep\" / \"Delete\" rendered a GREEN Delete next to a RED Keep,\n colouring the destructive action as the go-ahead and the safe one as the danger. That is the single\n reason the labels and the colours had to ship as one change rather than two.\n Destructive: affirmative red (#c62828, the same red the error icon uses), dismissive neutral \u2014 no\n colour at all, so the red is the only thing on the dialog asking for attention.\n Non-destructive: green/red exactly as before, byte for byte, for every caller that passes nothing.\n ids btnYes / btnNo and the click handlers are untouched in both cases. -->\n <button id=\"btnYes\" mat-stroked-button [style.color]=\"destructive ? '#c62828' : 'green'\" *ngIf=\"isConfirm\" (click)=\"response('yes')\" [attr.cdkFocusInitial]=\"destructive ? null : ''\">{{ confirmLabel }}</button>\n\n <button id=\"btnNo\" mat-stroked-button [style.color]=\"destructive ? null : 'red'\" *ngIf=\"isConfirm\" (click)=\"response('no')\" [attr.cdkFocusInitial]=\"destructive ? '' : null\">{{ 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--destructive{--tin-accent: #c62828;--tin-accent-tint: rgba(198, 40, 40, .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__btn-safe{color:var(--tin-title)!important;border:1px solid rgba(0,0,0,.23);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" }] }); }
|
|
5454
5511
|
}
|
|
5455
5512
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: messageDialog, decorators: [{
|
|
5456
5513
|
type: Component,
|
|
5457
|
-
args: [{ selector: 'lib-app-message', standalone: false, template: "<!-- Changed: two presentations behind one component. `modern` (appConfig.dialogStyle === 'modern') renders the\n structured layout; anything else renders the long-standing look, so an app that never opts in still gets a\n familiar dialog. Changed: classic is a SUPPORTED escape hatch, not frozen legacy \u2014 it is expected to render\n the same information as modern, and its three rendering bugs were repaired on 2026-08-06. Button ids\n (btnYes / btnNo / btnOK) are identical in BOTH branches \u2014 existing E2E specs select on them and must not\n care which style is active. -->\n\n<ng-container *ngIf=\"modern; else classic\">\n\n <!-- Rebuilt from a hardcoded per-type heading plus one undifferentiated paragraph into a structured layout \u2014\n icon chip, real title, lead paragraph, supporting detail. The type drives colour via a single class on\n the root, so adding a type is one entry in the component rather than three template branches. -->\n <div class=\"tin-msg\" [ngClass]=\"'tin-msg--' + messageType\">\n\n <div class=\"tin-msg__head\">\n <!-- The icon carries the type at a glance. It sits in a soft tinted disc rather than on a saturated\n banner: the intent is to inform, not to alarm someone who has usually done nothing wrong. -->\n <div class=\"tin-msg__icon\">\n <mat-icon>{{ iconName }}</mat-icon>\n </div>\n <h2 class=\"tin-msg__title\">{{ title }}</h2>\n </div>\n\n <mat-dialog-content class=\"tin-msg__body\">\n <!-- First paragraph is the lead (what happened); the rest is supporting detail (what to do next).\n Splitting on blank lines is what gives the eye somewhere to land \u2014 the old single run of text is\n why nobody read past the first line. -->\n <p *ngFor=\"let p of paragraphs; let first = first\" class=\"tin-msg__p\" [class.tin-msg__p--lead]=\"first\">{{ p }}</p>\n\n <!-- The backend's correlation code. Deliberately the quietest thing in the dialog \u2014 the user does not\n need to act on it, they only need to be able to read it out if they call support, at which point it\n turns a \"something broke this morning\" report into one log row. Hidden entirely when absent. -->\n <p class=\"tin-msg__ref\" *ngIf=\"reference\">If you contact your administrator, quote reference <span class=\"tin-msg__ref-code\">{{ reference }}</span>.</p>\n </mat-dialog-content>\n\n <mat-dialog-actions class=\"tin-msg__actions\">\n <!-- Changed: labels reverted to \"No\" / \"Yes\" (owner's call, 2026-08-06). The redesign had shipped them as\n \"Cancel\" / \"Yes, continue\"; these are the most-clicked buttons in the app and the original wording is\n what everybody here reads. Text only \u2014 the ids btnNo / btnYes are untouched and identical to the\n classic branch, so every E2E selector (#btnYes) and every caller is unaffected.\n Changed: the literal text is now {{ confirmLabel }} / {{ cancelLabel }} / {{ okLabel }}, which STILL\n render \"Yes\" / \"No\" / \"OK\" unless the caller passes a label. A caller that knows it is confirming a\n destructive action can name it (\"Delete\") so it no longer reads identically to a harmless one. -->\n <ng-container *ngIf=\"isConfirm; else modernAcknowledge\">\n <button id=\"btnNo\" mat-button class=\"tin-msg__btn-quiet\" (click)=\"response('no')\">{{ cancelLabel }}</button>\n <button id=\"btnYes\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('yes')\" cdkFocusInitial>{{ confirmLabel }}</button>\n </ng-container>\n <ng-template #modernAcknowledge>\n <button id=\"btnOK\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('ok')\" cdkFocusInitial>{{ okLabel }}</button>\n </ng-template>\n </mat-dialog-actions>\n\n </div>\n\n</ng-container>\n\n\n<!-- Classic: the original LOOK, kept deliberately as the escape hatch (owner's call, 2026-08-06 \u2014 retire was\n recommended and declined). It is the original markup plus FIXES, never style changes, because reverting a\n bug alongside a look would be a regression. It renders the same INFORMATION as the modern branch, in its\n own visual language:\n 1. the caller's subject is used as the heading when one was supplied. The old template hardcoded the\n heading per type and rendered the subject for 'info' only, which is why a friendly title like\n \"No connection\" showed up as the bare word \"Error\".\n 2. 'warning' and 'success' are handled. They did not exist when this markup was written, so they would\n otherwise fall through every branch and render an empty dialog.\n 3. 'error' now has an icon; the reference line is no longer trapped in the error-only branch; and the\n body renders real paragraphs instead of one collapsed run of text.\n \u26A0\uFE0F Anything added to the modern branch above has to be considered here too \u2014 that omission is exactly how\n the three bugs in 3. got here. -->\n\n<ng-template #classic>\n <div class=\"tin-msg-classic\">\n\n <h2>\n <div class=\"tin-between\">\n <mat-label>{{ classicHeading }}</mat-label>\n <!-- Added: the missing 'error' icon. Every other type had one; error \u2014 the type that most needs a cue \u2014\n rendered its heading beside empty flex space. Colour matches the modern branch's error accent\n (#c62828) so the two presentations agree on what \"error\" looks like. -->\n <mat-icon *ngIf=\"messageType=='error'\" style=\"color: #c62828;\">error</mat-icon>\n <mat-icon *ngIf=\"messageType=='confirm'\">question_mark</mat-icon>\n <mat-icon *ngIf=\"messageType=='info'\" style=\"color: steelblue;\">info</mat-icon>\n <mat-icon *ngIf=\"messageType=='warning'\" style=\"color: #ef6c00;\">warning</mat-icon>\n <mat-icon *ngIf=\"messageType=='success'\" style=\"color: #2e7d32;\">check_circle</mat-icon>\n </div>\n </h2>\n\n <mat-dialog-content>\n\n <!-- Changed: was `{{ _messageDetails }}` rendered raw, so the blank-line-separated paragraphs the messages\n are actually written in (api-error.service.ts) collapsed into one undifferentiated run of text. That\n collapse is the original complaint that started WS-7 and it was still live here. Now renders the same\n `paragraphs` split the modern branch uses \u2014 same information, classic's own plain markup. -->\n <div *ngIf=\"messageType=='error'\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <div *ngIf=\"messageType!='error'\" class=\"alert alert-secondary\" role=\"alert\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <!-- Changed: lifted OUT of the error-only branch above. The reference is a correlation code, not an\n error-specific field \u2014 a warning or info that carried one used to drop it silently. -->\n <div class=\"tin-msg-classic__ref\" *ngIf=\"reference\">Reference: {{ reference }}</div>\n\n </mat-dialog-content>\n\n <mat-dialog-actions>\n\n <!-- Changed: same caller-supplied labels as the modern branch, defaulting identically to \"Yes\" / \"No\" /\n \"OK\". Classic is expected to render the same INFORMATION as modern in its own visual language, so a\n caller that names its action must be honoured here too \u2014 otherwise the next person to pass a label\n would silently get \"Yes\" on every untracked consumer still running classic. Colours and ids untouched. -->\n <button id=\"btnYes\" mat-stroked-button style=\"color: green;\" *ngIf=\"isConfirm\" (click)=\"response('yes')\" cdkFocusInitial>{{ confirmLabel }}</button>\n\n <button id=\"btnNo\" mat-stroked-button style=\"color: red;\" *ngIf=\"isConfirm\" (click)=\"response('no')\">{{ cancelLabel }}</button>\n\n <button id=\"btnOK\" mat-stroked-button *ngIf=\"!isConfirm\" color=\"primary\" (click)=\"response('ok')\" cdkFocusInitial>{{ okLabel }}</button>\n\n </mat-dialog-actions>\n\n </div>\n</ng-template>\n", styles: [":host{display:block}.tin-msg-classic h2{margin:0!important;padding:16px 16px 0!important;font-size:20px!important;font-weight:500!important;line-height:normal!important}.tin-msg-classic .tin-between{display:flex;justify-content:space-between;align-items:center;margin:0!important;padding:0!important}.tin-msg-classic__p{margin:0 0 10px}.tin-msg-classic__p:last-child{margin-bottom:0}.tin-msg-classic__ref{margin-top:10px;font-size:12px;color:#0000008c;letter-spacing:.04em}.tin-msg{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1);--tin-title: rgba(0, 0, 0, .87);--tin-body: rgba(0, 0, 0, .6);position:relative;padding:4px 4px 0}.tin-msg:before{content:\"\";position:absolute;top:0;left:0;right:0;height:3px;background:var(--tin-accent);border-radius:3px 3px 0 0}.tin-msg--error{--tin-accent: #c62828;--tin-accent-tint: rgba(198, 40, 40, .1)}.tin-msg--warning{--tin-accent: #ef6c00;--tin-accent-tint: rgba(239, 108, 0, .12)}.tin-msg--info{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1)}.tin-msg--success{--tin-accent: #2e7d32;--tin-accent-tint: rgba(46, 125, 50, .12)}.tin-msg--confirm{--tin-accent: #5e35b1;--tin-accent-tint: rgba(94, 53, 177, .1)}.tin-msg__head{display:flex;align-items:center;gap:14px;padding:20px 20px 8px}.tin-msg__icon{flex:0 0 auto;width:40px;height:40px;border-radius:50%;background:var(--tin-accent-tint);display:inline-flex;align-items:center;justify-content:center}.tin-msg__icon .mat-icon,.tin-msg__icon mat-icon{color:var(--tin-accent);font-size:22px;width:22px;height:22px;line-height:22px}.tin-msg__title{margin:0!important;padding:0!important;font-size:18px!important;font-weight:600!important;line-height:1.3!important;letter-spacing:-.1px;color:var(--tin-title)}.tin-msg__body{padding:0 20px 4px 74px!important;max-height:60vh}.tin-msg__p{margin:0 0 12px;font-size:13.5px;line-height:1.6;color:var(--tin-body)}.tin-msg__p--lead{font-size:14.5px;font-weight:500;color:var(--tin-title)}.tin-msg__p:last-child{margin-bottom:0}.tin-msg__ref{margin:14px 0 0;padding-top:10px;border-top:1px solid rgba(0,0,0,.08);font-size:12px;line-height:1.5;color:var(--tin-muted, rgba(0, 0, 0, .55))}.tin-msg__ref-code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;letter-spacing:.06em;color:var(--tin-title, rgba(0, 0, 0, .8));white-space:nowrap;-webkit-user-select:all;user-select:all}.tin-msg__actions{display:flex;justify-content:flex-end;gap:8px;padding:12px 20px 16px!important;margin:0!important;min-height:0!important}.tin-msg__btn-primary{background:var(--tin-accent)!important;color:#fff!important;min-width:88px;font-weight:500;letter-spacing:.2px}.tin-msg__btn-quiet{color:var(--tin-body)!important;font-weight:500}.tin-msg__icon{animation:tin-msg-pop .22s cubic-bezier(.2,.7,.3,1) both}@keyframes tin-msg-pop{0%{transform:scale(.82);opacity:0}to{transform:scale(1);opacity:1}}@media (prefers-reduced-motion: reduce){.tin-msg__icon{animation:none}}@media (max-width: 480px){.tin-msg__head{padding:16px 16px 8px;gap:12px}.tin-msg__body{padding:0 16px 4px!important}.tin-msg__actions{padding:12px 16px 14px!important}}\n"] }]
|
|
5514
|
+
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 <!-- Changed: `tin-msg--destructive` re-points the single --tin-accent variable at the warn red. Because every\n accented part (the top rule, the icon disc, the primary button) already reads from that one variable, the\n whole dialog turns to a warning in one class instead of three overrides \u2014 and the affirmative button, which\n is the thing the user is about to press, can no longer come out purple or green on a \"Delete\". -->\n <div class=\"tin-msg\" [ngClass]=\"'tin-msg--' + messageType\" [class.tin-msg--destructive]=\"isConfirm && destructive\">\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 <!-- Changed: on a DESTRUCTIVE confirm the initial focus moves to the SAFE button. The affirmative button is\n still #btnYes in the same position with the same click handler \u2014 only the keyboard default changes, so\n hitting Enter on reflex now keeps the record instead of destroying it. Every E2E locator clicks #btnYes\n explicitly (helpers/form.helper.ts acceptConfirmIfPresent uses page.locator('#btnYes')), so none of them\n depend on which button holds focus. The colour comes from tin-msg--destructive on the root, not from\n anything here \u2014 the primary button reads --tin-accent either way. -->\n <ng-container *ngIf=\"isConfirm; else modernAcknowledge\">\n <button id=\"btnNo\" mat-button class=\"tin-msg__btn-quiet\" [class.tin-msg__btn-safe]=\"destructive\" (click)=\"response('no')\" [attr.cdkFocusInitial]=\"destructive ? '' : null\">{{ cancelLabel }}</button>\n <button id=\"btnYes\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('yes')\" [attr.cdkFocusInitial]=\"destructive ? null : ''\">{{ 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 <!-- Changed: a destructive confirm shows a red warning instead of the neutral question mark, matching the\n modern branch. Same #c62828 as the error icon beside it, so the two styles agree on what danger looks\n like. A plain confirm (\"Switch organisation?\") is unchanged. -->\n <mat-icon *ngIf=\"messageType=='confirm' && destructive\" style=\"color: #c62828;\">warning</mat-icon>\n <mat-icon *ngIf=\"messageType=='confirm' && !destructive\">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 <!-- \u26A0\uFE0F Changed: THE COLOUR NOW FOLLOWS THE ACTION, NOT THE BUTTON'S POSITION. This markup hardcoded\n `color: green` on the affirmative button and `color: red` on the dismissive one. That mapping is only\n correct while the buttons read \"Yes\" and \"No\" and the affirmative answer is the harmless one \u2014 the\n green was never saying \"this is safe\", it was saying \"this is the Yes\". Name the action and the\n assumption inverts: a confirm offering \"Keep\" / \"Delete\" rendered a GREEN Delete next to a RED Keep,\n colouring the destructive action as the go-ahead and the safe one as the danger. That is the single\n reason the labels and the colours had to ship as one change rather than two.\n Destructive: affirmative red (#c62828, the same red the error icon uses), dismissive neutral \u2014 no\n colour at all, so the red is the only thing on the dialog asking for attention.\n Non-destructive: green/red exactly as before, byte for byte, for every caller that passes nothing.\n ids btnYes / btnNo and the click handlers are untouched in both cases. -->\n <button id=\"btnYes\" mat-stroked-button [style.color]=\"destructive ? '#c62828' : 'green'\" *ngIf=\"isConfirm\" (click)=\"response('yes')\" [attr.cdkFocusInitial]=\"destructive ? null : ''\">{{ confirmLabel }}</button>\n\n <button id=\"btnNo\" mat-stroked-button [style.color]=\"destructive ? null : 'red'\" *ngIf=\"isConfirm\" (click)=\"response('no')\" [attr.cdkFocusInitial]=\"destructive ? '' : null\">{{ 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--destructive{--tin-accent: #c62828;--tin-accent-tint: rgba(198, 40, 40, .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__btn-safe{color:var(--tin-title)!important;border:1px solid rgba(0,0,0,.23);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"] }]
|
|
5458
5515
|
}], ctorParameters: () => [{ type: i4.MatDialogRef }, { type: undefined, decorators: [{
|
|
5459
5516
|
type: Inject,
|
|
5460
5517
|
args: [MAT_DIALOG_DATA]
|
|
@@ -5534,13 +5591,17 @@ class MessageService {
|
|
|
5534
5591
|
// ⚠️ Do NOT change the defaults to make every confirm read better: piglet-spa (38) and shift-spa (13) select
|
|
5535
5592
|
// these buttons by their literal text via getByRole('button', { name: 'Yes' }), and the owner reverted a
|
|
5536
5593
|
// hardcoded relabel on 2026-08-06. Opt in per call site.
|
|
5537
|
-
|
|
5594
|
+
// Changed: optional `destructive`, appended last so every existing call — including the three-argument ones
|
|
5595
|
+
// added with the labels — is unaffected. It drives COLOUR only: the affirmative button turns warn-red and the
|
|
5596
|
+
// dismissive one goes neutral, because the classic style hardcodes green on the affirmative button and a
|
|
5597
|
+
// GREEN "Delete" beside a RED "Keep" tells the user precisely the opposite of the truth.
|
|
5598
|
+
confirm(msg, confirmLabel, cancelLabel, destructive) {
|
|
5538
5599
|
let type = "confirm";
|
|
5539
5600
|
let subject = "";
|
|
5540
5601
|
let details = msg;
|
|
5541
5602
|
const dialogRef = this.dialog.open(messageDialog, {
|
|
5542
5603
|
width: "400px",
|
|
5543
|
-
data: { type, subject, details, confirmLabel, cancelLabel },
|
|
5604
|
+
data: { type, subject, details, confirmLabel, cancelLabel, destructive },
|
|
5544
5605
|
});
|
|
5545
5606
|
return dialogRef.afterClosed().pipe(mergeMap((result) => {
|
|
5546
5607
|
return of(result);
|
|
@@ -10743,6 +10804,47 @@ class ButtonService {
|
|
|
10743
10804
|
// Return static message
|
|
10744
10805
|
return button.confirm.message;
|
|
10745
10806
|
}
|
|
10807
|
+
// Added: the built-in action names the library is willing to recognise as destructive ON ITS OWN, and the
|
|
10808
|
+
// action word each one puts on the button. Deliberately only these two. They are the names the library
|
|
10809
|
+
// GENERATES a confirmation for without the config asking (table.deleteModel and detailsDialog.delete both
|
|
10810
|
+
// fall back to "Are you sure you want to delete ?"), so they are the only ones where naming the action cannot
|
|
10811
|
+
// be expressed at the call site. Everything else — void, cancel, discard, reverse — must pass its own label
|
|
10812
|
+
// through `confirm.confirmLabel`, because only the app knows whether its 'cancel' button cancels an order or
|
|
10813
|
+
// just closes something, and guessing wrong puts the wrong verb on the button that destroys data.
|
|
10814
|
+
static { this.DESTRUCTIVE_ACTIONS = { 'delete': 'Delete', 'remove': 'Remove' }; }
|
|
10815
|
+
// Added: resolves a confirmation's message, button labels and destructiveness in ONE place, so the four call
|
|
10816
|
+
// sites that raise a confirmation (table.deleteModel, table.doAction, detailsDialog.delete,
|
|
10817
|
+
// detailsDialog.executeAction) cannot drift apart on it — they already drifted once on the fallback message.
|
|
10818
|
+
//
|
|
10819
|
+
// `allowNamedDestructive` is the app-level opt-in (appConfig.namedDestructiveActions), passed in rather than
|
|
10820
|
+
// read here so this service keeps its single AuthService dependency and does not take one on DataServiceLib.
|
|
10821
|
+
//
|
|
10822
|
+
// Precedence, and why: an EXPLICIT label or flag in the config always wins, because it is a deliberate
|
|
10823
|
+
// statement of intent by the app. Only when the config is silent does the app-level flag allow the library to
|
|
10824
|
+
// infer destructiveness from the action name — and with the flag absent (every untracked consumer, and any app
|
|
10825
|
+
// that has not opted in) nothing is inferred at all, so the prompt renders "Yes" / "No" in green/red exactly
|
|
10826
|
+
// as it does today.
|
|
10827
|
+
getConfirmOptions(button, row, fallbackMessage, allowNamedDestructive) {
|
|
10828
|
+
const confirm = button?.confirm;
|
|
10829
|
+
const message = this.getConfirmMessage(button, row) || fallbackMessage || '';
|
|
10830
|
+
const actionWord = ButtonService.DESTRUCTIVE_ACTIONS[button?.name];
|
|
10831
|
+
// An explicit `destructive` wins either way — including an explicit false, which is how an app says "this
|
|
10832
|
+
// button is called delete but it is not destroying anything, leave it alone".
|
|
10833
|
+
const destructive = confirm?.destructive !== undefined
|
|
10834
|
+
? confirm.destructive === true
|
|
10835
|
+
: (allowNamedDestructive === true && !!actionWord);
|
|
10836
|
+
if (!destructive) {
|
|
10837
|
+
return { message, confirmLabel: confirm?.confirmLabel, cancelLabel: confirm?.cancelLabel, destructive: false };
|
|
10838
|
+
}
|
|
10839
|
+
// Naming the SAFE button matters as much as naming the destructive one: "Keep" states what happens if you
|
|
10840
|
+
// press it, where "No" only answers a question the user has to re-read the sentence to reconstruct.
|
|
10841
|
+
return {
|
|
10842
|
+
message,
|
|
10843
|
+
confirmLabel: confirm?.confirmLabel || actionWord || 'Delete',
|
|
10844
|
+
cancelLabel: confirm?.cancelLabel || 'Keep',
|
|
10845
|
+
destructive: true
|
|
10846
|
+
};
|
|
10847
|
+
}
|
|
10746
10848
|
getButtonProperties(button, row, config) {
|
|
10747
10849
|
return {
|
|
10748
10850
|
color: this.getButtonColor(button, row),
|
|
@@ -11721,6 +11823,8 @@ class SetupService {
|
|
|
11721
11823
|
// Added (v2): module catalog — empty means the app has no module concept (nothing is ever hidden)
|
|
11722
11824
|
this.modules = new BehaviorSubject([]);
|
|
11723
11825
|
this.modules$ = this.modules.asObservable();
|
|
11826
|
+
this.modulesLoaded = false; // Added (F3): a guard must be able to tell "the catalog is genuinely empty" from "it has not been fetched yet"
|
|
11827
|
+
this.modulesInFlight = null; // Added (F3): de-dupes concurrent guard activations on a deep route
|
|
11724
11828
|
} // Changed: injected for loadStatus only
|
|
11725
11829
|
get enabled() {
|
|
11726
11830
|
return !!this.dataService.appConfig?.setupConfig?.enabled;
|
|
@@ -11759,6 +11863,7 @@ class SetupService {
|
|
|
11759
11863
|
this.status.next(apiResponse.data);
|
|
11760
11864
|
this.pendingCount.next(apiResponse.data?.pending ?? 0);
|
|
11761
11865
|
this.modules.next(apiResponse.data?.modules ?? []); // Added (v2): status carries the module catalog too
|
|
11866
|
+
this.modulesLoaded = true; // Added (F3): status carries the catalog, so a Getting Started visit satisfies ensureModules() too
|
|
11762
11867
|
}
|
|
11763
11868
|
},
|
|
11764
11869
|
error: () => { }
|
|
@@ -11776,10 +11881,30 @@ class SetupService {
|
|
|
11776
11881
|
// a dialog becomes a wall.
|
|
11777
11882
|
if (apiResponse.success)
|
|
11778
11883
|
this.modules.next(apiResponse.data ?? []);
|
|
11884
|
+
this.modulesLoaded = true; // Added (F3): answered either way — a failed load leaves the catalog empty, and empty means "hide nothing"
|
|
11779
11885
|
},
|
|
11780
|
-
error: () => { } //
|
|
11886
|
+
error: () => { this.modulesLoaded = true; } // Changed (F3): a backend without module support must not leave every later ensureModules() re-requesting
|
|
11781
11887
|
});
|
|
11782
11888
|
}
|
|
11889
|
+
// Added (F3): awaitable catalog for moduleGuard. Reading the BehaviorSubject at route-resolution time is not
|
|
11890
|
+
// enough — on a deep link or an F5 the catalog has not arrived yet, so a guard would read empty and fail open on
|
|
11891
|
+
// exactly the path it exists to protect. Fails open on every unhappy path (setup disabled, no SetupController,
|
|
11892
|
+
// 401, error) because hiding working functionality is the worse error, matching isModuleEnabled below and the
|
|
11893
|
+
// server's ModuleCatalog.IsEnabled. shareReplay collapses the several child activations of one deep route into
|
|
11894
|
+
// a single setup/modules request.
|
|
11895
|
+
ensureModules() {
|
|
11896
|
+
if (!this.enabled || this.modulesLoaded)
|
|
11897
|
+
return of(true);
|
|
11898
|
+
if (!this.modulesInFlight) {
|
|
11899
|
+
this.modulesInFlight = this.dataService.CallApi({ url: 'setup/modules', skipCache: true }).pipe(map((apiResponse) => (apiResponse.success ? apiResponse.data ?? [] : [])), tap((mods) => { this.modules.next(mods); this.modulesLoaded = true; this.modulesInFlight = null; }), catchError(() => { this.modulesLoaded = true; this.modulesInFlight = null; return of([]); }), shareReplay(1));
|
|
11900
|
+
}
|
|
11901
|
+
return this.modulesInFlight.pipe(map(() => true));
|
|
11902
|
+
}
|
|
11903
|
+
// Added (F3): the refusal toast names the module, not the key — falls back to the key for an app whose route
|
|
11904
|
+
// declares a key the server's catalog does not carry
|
|
11905
|
+
moduleTitle(key) {
|
|
11906
|
+
return this.modules.value?.find(m => m.key === key)?.title ?? key;
|
|
11907
|
+
}
|
|
11783
11908
|
// Added (v2): nav-menu gate — unknown keys and apps without modules always pass (fail open)
|
|
11784
11909
|
isModuleEnabled(key) {
|
|
11785
11910
|
if (!key)
|
|
@@ -11825,6 +11950,26 @@ class SetupService {
|
|
|
11825
11950
|
applyApprovalPresets(items) {
|
|
11826
11951
|
return this.dataService.CallApi({ url: 'setup/approvals', method: 'post', skipCache: true }, { items });
|
|
11827
11952
|
}
|
|
11953
|
+
// Added (seed seam): the example-data offers this tenant's steps can make. skipCache like every other setup
|
|
11954
|
+
// call, and here it is not optional — wouldCreate, existing and applied are stamped per REQUEST, so a cached
|
|
11955
|
+
// catalogue would keep offering rows the user has just created and keep the counts wrong.
|
|
11956
|
+
loadSeedOffers() {
|
|
11957
|
+
return this.dataService.CallApi({ url: 'setup/seedoffers', skipCache: true });
|
|
11958
|
+
}
|
|
11959
|
+
// Added (seed seam): apply ONE offer, plus whatever that offer declares it needs first — the server walks the
|
|
11960
|
+
// dependency chain, so the caller sends a single key. A POST, not a GET, and the server requires Edit: the two
|
|
11961
|
+
// seed buttons this replaces are both writes reachable by a View-only user, and that is not carried forward.
|
|
11962
|
+
applySeedOffer(key) {
|
|
11963
|
+
return this.dataService.CallApi({ url: 'setup/seed', method: 'post', skipCache: true }, { key });
|
|
11964
|
+
}
|
|
11965
|
+
// Added (seed-all): fill every area that is still empty, in ONE request. The client sends no list — the server
|
|
11966
|
+
// loops the SAME offer catalogue, applies only the offers whose own area still has something to add, and skips
|
|
11967
|
+
// anything the user lacks Edit on. Deliberately NOT a client-side loop over applySeedOffer(): that would put the
|
|
11968
|
+
// judgement about what counts as "empty" in the browser, which is exactly where the demo-data card's one global
|
|
11969
|
+
// sentinel already went wrong, and it would produce one audit entry and one toast per area.
|
|
11970
|
+
applySeedAll() {
|
|
11971
|
+
return this.dataService.CallApi({ url: 'setup/seed-all', method: 'post', skipCache: true }, {});
|
|
11972
|
+
}
|
|
11828
11973
|
refresh() {
|
|
11829
11974
|
this.loadStatus();
|
|
11830
11975
|
}
|
|
@@ -12124,6 +12269,16 @@ class SetupGuideComponent {
|
|
|
12124
12269
|
this.roleTemplates = [];
|
|
12125
12270
|
this.selectedTemplates = {};
|
|
12126
12271
|
this.creatingRoles = false;
|
|
12272
|
+
// Added (seed seam): example-data offers, keyed by the step they render inside. The MAP is what the template
|
|
12273
|
+
// indexes — indexing returns the same array reference every change-detection pass, where a filter() in the
|
|
12274
|
+
// template would allocate a new one per pass (the banned pattern that livelocked this library once already).
|
|
12275
|
+
this.seedRowsByStep = {};
|
|
12276
|
+
this.seedingKey = ''; // offer key with an in-flight apply — disables its button until the server answers
|
|
12277
|
+
// Added (seed-all): the SAME offers, reported area by area for the Demo data card at the foot of the page.
|
|
12278
|
+
// One list, two readers — a second fetch with its own idea of what is empty is how the two would drift.
|
|
12279
|
+
this.demoAreas = [];
|
|
12280
|
+
this.demoPending = 0; // areas with something left to add; materialized so the template binds a number, not a filter()
|
|
12281
|
+
this.seedingAll = false;
|
|
12127
12282
|
// Added (presets): picker state. `groups` is materialized (same rule as moduleGroups above); selected/role are
|
|
12128
12283
|
// plain maps keyed by preset key so a reload never clobbers a manual choice.
|
|
12129
12284
|
// Changed (presets P4): one object per domain instead of a field per domain — see PresetPicker above.
|
|
@@ -12185,6 +12340,8 @@ class SetupGuideComponent {
|
|
|
12185
12340
|
this.buildGroups();
|
|
12186
12341
|
if (this.hasRoleTemplatesStep())
|
|
12187
12342
|
this.loadRoleTemplates();
|
|
12343
|
+
if (this.hasSeedStep() || this.showDemoData)
|
|
12344
|
+
this.loadSeedOffers(); // Changed (seed-all): the Demo data card reports per area from the same offers, so an app with the card but no seed: true step still needs them
|
|
12188
12345
|
// Added (presets): only load a picker when the backend actually serves its step. The approvals step is gated
|
|
12189
12346
|
// on the approvals module, so turning that module off makes the step — and this call — disappear with it.
|
|
12190
12347
|
this.pickers.forEach(picker => { if (this.hasPresetsStep(picker))
|
|
@@ -12373,6 +12530,122 @@ class SetupGuideComponent {
|
|
|
12373
12530
|
return;
|
|
12374
12531
|
this.selectedTemplates[tpl.key] = !this.selectedTemplates[tpl.key];
|
|
12375
12532
|
}
|
|
12533
|
+
//---------- Example data (seed seam) ----------
|
|
12534
|
+
// Seeding lives HERE and not on a working page, because getting started is exactly when someone needs a few rows
|
|
12535
|
+
// to look at. Three things make this different from the demo-data card at the foot of the page:
|
|
12536
|
+
// 1. it is SCOPED — an offer seeds its own step's area and nothing else;
|
|
12537
|
+
// 2. it is DEPENDENCY-AWARE — the server applies whatever the offer declares it needs first, and the row says
|
|
12538
|
+
// so in plain words BEFORE the click;
|
|
12539
|
+
// 3. it is EXPLICITLY example data — the notice is on the row and repeated in the confirm, so nobody can add
|
|
12540
|
+
// it without knowing that is what they are doing.
|
|
12541
|
+
// ⚠️ Accounting seeding is deliberately NOT here and must never be moved here. It is not optional and it is not
|
|
12542
|
+
// a choice — the Transaction Types button and the automatic financial seeds stay exactly as they are.
|
|
12543
|
+
hasSeed(step) {
|
|
12544
|
+
return !!this.getAction(step).seed;
|
|
12545
|
+
}
|
|
12546
|
+
hasSeedStep() {
|
|
12547
|
+
return (this.status?.steps || []).some(s => this.getAction(s).seed);
|
|
12548
|
+
}
|
|
12549
|
+
loadSeedOffers() {
|
|
12550
|
+
this.setupService.loadSeedOffers().subscribe({
|
|
12551
|
+
next: resp => {
|
|
12552
|
+
// Same reasoning as loadRoleTemplates and loadPresets: a bare return renders no offers, which reads as
|
|
12553
|
+
// "this app has no example data to give you" — a completely different statement from "we could not fetch it".
|
|
12554
|
+
if (!resp.success) {
|
|
12555
|
+
this.apiErrorService.presentAppFailure(resp, 'load', 'setup/seedoffers');
|
|
12556
|
+
return;
|
|
12557
|
+
}
|
|
12558
|
+
this.buildSeedRows(resp.data || []);
|
|
12559
|
+
},
|
|
12560
|
+
error: () => { }
|
|
12561
|
+
});
|
|
12562
|
+
}
|
|
12563
|
+
// Materialize once per load. Offers with no stepKey are PREREQUISITES — they exist so another offer can declare
|
|
12564
|
+
// them, they render nowhere, and dropping them here is what keeps them invisible.
|
|
12565
|
+
buildSeedRows(offers) {
|
|
12566
|
+
const byStep = {};
|
|
12567
|
+
offers.filter(o => !!o.stepKey).forEach(offer => {
|
|
12568
|
+
const rows = byStep[offer.stepKey] || (byStep[offer.stepKey] = []);
|
|
12569
|
+
rows.push({ offer, alsoAdds: this.seedAlsoAdds(offer), hereAlready: this.seedHereAlready(offer), actionLabel: this.seedActionLabel(offer) });
|
|
12570
|
+
});
|
|
12571
|
+
this.seedRowsByStep = byStep;
|
|
12572
|
+
// Changed (seed-all): the Demo data card gets EVERY offer, prerequisites included. A step row hides a
|
|
12573
|
+
// prerequisite on purpose (nobody should have to think about product categories to get products), but the card
|
|
12574
|
+
// is a report on the whole system, and an area quietly filled by somebody else's dependency is still an area
|
|
12575
|
+
// that changed. Order is the server's catalogue order, untouched.
|
|
12576
|
+
this.demoAreas = offers.map(offer => ({ offer, label: this.demoAreaLabel(offer), state: this.demoAreaState(offer) }));
|
|
12577
|
+
this.demoPending = offers.filter(o => !o.applied).length;
|
|
12578
|
+
}
|
|
12579
|
+
// Added (seed-all): a PREREQUISITE offer's Name is deliberately a sentence FRAGMENT server-side ("product
|
|
12580
|
+
// categories"), because on a step row it is only ever read inside "…also adds product categories". This card is
|
|
12581
|
+
// the one place a prerequisite is listed as a row of its own, where a lowercase fragment beside three capitalised
|
|
12582
|
+
// sentences reads as a rendering fault. Only the first letter is raised — the wording stays the server's, so this
|
|
12583
|
+
// does not become a second place where copy is authored. Materialized at load, never computed in a binding.
|
|
12584
|
+
demoAreaLabel(offer) {
|
|
12585
|
+
const name = offer.name || '';
|
|
12586
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
12587
|
+
}
|
|
12588
|
+
// What this ONE area says about itself: what is already in it, and what a fill would add. Numbers only — every
|
|
12589
|
+
// SENTENCE on this page is the server's (effect, sampleNotice), and this must not become a second place where
|
|
12590
|
+
// wording is authored. It always names what it counts; a bare number was removed from the Day Book on that ruling.
|
|
12591
|
+
demoAreaState(offer) {
|
|
12592
|
+
const here = offer.existing === 1 ? '1 record here' : `${offer.existing} records here`;
|
|
12593
|
+
if (offer.applied)
|
|
12594
|
+
return here;
|
|
12595
|
+
return offer.existing === 0 ? `empty — adds ${offer.wouldCreate}` : `${here} — adds ${offer.wouldCreate}`;
|
|
12596
|
+
}
|
|
12597
|
+
// The prerequisites this click would ALSO create. The server sends only the ones not already there, so the
|
|
12598
|
+
// sentence is true at the moment it is read — "it will also add X" about something already present is a lie.
|
|
12599
|
+
seedAlsoAdds(offer) {
|
|
12600
|
+
const includes = offer.includes || [];
|
|
12601
|
+
return includes.length === 0 ? '' : `Also adds ${includes.join(', ')} — these cannot exist without them.`;
|
|
12602
|
+
}
|
|
12603
|
+
// The live count of what is in this area already, real records or example ones. This is the decision-support
|
|
12604
|
+
// half: somebody with four customers already does not want three more invented ones. It always says WHAT it
|
|
12605
|
+
// counts — a bare number was removed from the Day Book on exactly that ruling.
|
|
12606
|
+
seedHereAlready(offer) {
|
|
12607
|
+
if (offer.applied || !offer.existing)
|
|
12608
|
+
return '';
|
|
12609
|
+
return offer.existing === 1 ? '1 record is already in this list' : `${offer.existing} records are already in this list`;
|
|
12610
|
+
}
|
|
12611
|
+
// The button says HOW MANY, because a button that says only "Seed" is asking for blind consent. wouldCreate is
|
|
12612
|
+
// the server's live count of rows that do not exist yet, so the number on the button is the number that lands.
|
|
12613
|
+
seedActionLabel(offer) {
|
|
12614
|
+
if (offer.wouldCreate === 1)
|
|
12615
|
+
return 'Add 1 example record';
|
|
12616
|
+
return offer.wouldCreate > 1 ? `Add ${offer.wouldCreate} example records` : 'Add example data';
|
|
12617
|
+
}
|
|
12618
|
+
// The confirm is where the example-data notice becomes unavoidable — "you don't want to just add without someone
|
|
12619
|
+
// knowing that this is what they are doing". Every sentence in it is the SERVER'S, none is composed here: the
|
|
12620
|
+
// effect, the prerequisite line and the standing notice are all authored in the catalogue, so the wording is
|
|
12621
|
+
// correctable in one place for all five apps. Blank lines are real paragraph breaks in the message dialog.
|
|
12622
|
+
applySeedOffer(offer) {
|
|
12623
|
+
if (offer.applied || this.seedingKey)
|
|
12624
|
+
return;
|
|
12625
|
+
const alsoAdds = (offer.includes || []).length > 0 ? `\n\nIt will also add: ${offer.includes.join(', ')}.` : '';
|
|
12626
|
+
this.messageService.confirm(`${offer.effect}${alsoAdds}\n\n${offer.sampleNotice}`, 'Add example data').subscribe(answer => {
|
|
12627
|
+
if (answer !== 'yes')
|
|
12628
|
+
return; // messageDialog answers with the literal 'yes'
|
|
12629
|
+
this.seedingKey = offer.key;
|
|
12630
|
+
this.setupService.applySeedOffer(offer.key).subscribe({
|
|
12631
|
+
next: resp => {
|
|
12632
|
+
this.seedingKey = '';
|
|
12633
|
+
if (!resp.success) {
|
|
12634
|
+
this.apiErrorService.presentAppFailure(resp, 'action', 'setup/seed');
|
|
12635
|
+
return;
|
|
12636
|
+
} // never the server's own words on a failure
|
|
12637
|
+
const created = resp.data || 0;
|
|
12638
|
+
// Deliberately NOT a triumphant message when nothing was written. An affordance that reports success and
|
|
12639
|
+
// creates nothing is this codebase's signature bug; the server reports that case as a failure, and this
|
|
12640
|
+
// line covers the honest "it was all already here" one.
|
|
12641
|
+
this.messageService.toast(created > 0 ? `${created} example record${created === 1 ? '' : 's'} added — look for “Example: ”` : 'Those example records are already here');
|
|
12642
|
+
this.loadSeedOffers(); // re-reads applied / wouldCreate / existing so the row comes back in its new state
|
|
12643
|
+
this.setupService.refresh(); // seeded rows move the step's own count
|
|
12644
|
+
},
|
|
12645
|
+
error: () => this.seedingKey = ''
|
|
12646
|
+
});
|
|
12647
|
+
});
|
|
12648
|
+
}
|
|
12376
12649
|
//---------- Preset pickers (notifications, sign-off) ----------
|
|
12377
12650
|
// ONE code path, driven by the PresetPicker objects above. Tick a row, pick who it goes to, apply. Both backends
|
|
12378
12651
|
// are create-only and skip anything already configured, so neither can overwrite a rule the user has since
|
|
@@ -12491,6 +12764,48 @@ class SetupGuideComponent {
|
|
|
12491
12764
|
removeDemoData() {
|
|
12492
12765
|
this.runDemoAction('demo-data/remove', 'This permanently deletes every example record created by demo data. Continue?', 'Demo data removed successfully');
|
|
12493
12766
|
}
|
|
12767
|
+
// Added (seed-all): "scroll right to the bottom where they seed demo data and it just populates everything for
|
|
12768
|
+
// them ON AREAS THAT DON'T HAVE DATA". That last clause is the whole feature and it did not exist — seedDemoData()
|
|
12769
|
+
// above asks demo-data/seed, which asks ONE global sentinel (a customer named "Example: …") and, once it is
|
|
12770
|
+
// there, seeds nothing further no matter how many other areas are still empty. This asks each area instead.
|
|
12771
|
+
//
|
|
12772
|
+
// 🔴 seedDemoData() is deliberately KEPT, not replaced. The offer catalogue covers the areas it declares; the
|
|
12773
|
+
// app-specific demo datasets (pigs, equipment, trips) still only come from demo-data/seed. Retiring that button
|
|
12774
|
+
// before an offer exists for every one of those areas would take away the only way to seed them — the same
|
|
12775
|
+
// mistake as removing a seed button before its replacement works.
|
|
12776
|
+
//
|
|
12777
|
+
// Every sentence in the confirm is the SERVER'S: each area's own effect line and the standing example-data
|
|
12778
|
+
// notice, both authored in the catalogue so all five apps read the same words and one edit corrects them all.
|
|
12779
|
+
seedAll() {
|
|
12780
|
+
if (this.seedingAll || this.demoBusy)
|
|
12781
|
+
return;
|
|
12782
|
+
const pending = this.demoAreas.filter(area => !area.offer.applied);
|
|
12783
|
+
if (pending.length === 0)
|
|
12784
|
+
return;
|
|
12785
|
+
const effects = pending.map(area => `• ${area.offer.effect}`).join('\n');
|
|
12786
|
+
this.messageService.confirm(`${effects}\n\n${pending[0].offer.sampleNotice}`, 'Fill the empty areas').subscribe(answer => {
|
|
12787
|
+
if (answer !== 'yes')
|
|
12788
|
+
return; // messageDialog answers with the literal 'yes'
|
|
12789
|
+
this.seedingAll = true;
|
|
12790
|
+
this.setupService.applySeedAll().subscribe({
|
|
12791
|
+
next: resp => {
|
|
12792
|
+
this.seedingAll = false;
|
|
12793
|
+
// The server reports "some areas advertised rows and wrote none" as a FAILURE on purpose, so this must
|
|
12794
|
+
// not toast a success over it. Never the server's own words on a failure — the shared classifier decides.
|
|
12795
|
+
if (!resp.success) {
|
|
12796
|
+
this.apiErrorService.presentAppFailure(resp, 'action', 'setup/seed-all');
|
|
12797
|
+
this.loadSeedOffers();
|
|
12798
|
+
return;
|
|
12799
|
+
}
|
|
12800
|
+
const created = resp.data || 0;
|
|
12801
|
+
this.messageService.toast(created > 0 ? `${created} example record${created === 1 ? '' : 's'} added — look for “Example: ”` : 'Every area already has data');
|
|
12802
|
+
this.loadSeedOffers(); // re-reads applied / wouldCreate / existing so every area line comes back in its new state
|
|
12803
|
+
this.setupService.refresh(); // seeded rows move the step counts
|
|
12804
|
+
},
|
|
12805
|
+
error: () => this.seedingAll = false
|
|
12806
|
+
});
|
|
12807
|
+
});
|
|
12808
|
+
}
|
|
12494
12809
|
runDemoAction(url, question, success) {
|
|
12495
12810
|
if (this.demoBusy)
|
|
12496
12811
|
return;
|
|
@@ -12536,11 +12851,11 @@ class SetupGuideComponent {
|
|
|
12536
12851
|
});
|
|
12537
12852
|
}
|
|
12538
12853
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SetupGuideComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
12539
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SetupGuideComponent, isStandalone: false, selector: "spa-setup-guide", ngImport: i0, template: "<div class=\"setup-page\" *ngIf=\"status\">\n\n <!-- Hero: overall readiness + celebration state at 100% -->\n <mat-card class=\"setup-hero\" [class.celebrate]=\"status.percent === 100\">\n <div class=\"hero-content\" *ngIf=\"status.percent < 100\">\n <div class=\"hero-text\">\n <h1>{{ title }}</h1>\n <p>Complete these steps to get your system ready for day-to-day operation.</p>\n <span class=\"hero-counter\">{{ status.completed }} of {{ status.total }} steps completed</span>\n </div>\n <div class=\"hero-progress\">\n <span class=\"hero-percent\">{{ status.percent }}%</span>\n <mat-progress-bar mode=\"determinate\" [value]=\"status.percent\"></mat-progress-bar>\n </div>\n </div>\n <div class=\"hero-content celebration\" *ngIf=\"status.percent === 100\">\n <mat-icon class=\"celebrate-icon\">celebration</mat-icon>\n <div class=\"hero-text\">\n <h1>You're all set!</h1>\n <p>All setup steps are complete \u2014 your system is ready to operate.</p>\n </div>\n </div>\n </mat-card>\n\n <!-- Module picker (v2): choose what the business uses; optional modules toggle steps + menus -->\n <mat-card class=\"setup-modules\" *ngIf=\"modules.length > 0\">\n <div class=\"group-header\">\n <h2>Your modules</h2>\n <span class=\"group-counter\">Tap a module to turn it on or off \u2014 you can change this anytime.</span>\n </div>\n <!-- Changed (v3): grouped \u2014 the picker now carries the whole library catalog, so cards sit under their\n group heading (the app's own modules first). A backend without groups yields one unnamed group,\n which renders as the original flat grid. -->\n <div class=\"module-section\" *ngFor=\"let mgroup of moduleGroups\">\n <div class=\"module-section-head\">\n <span class=\"module-section-name\">{{ mgroup.name }}</span>\n <span class=\"module-section-count\">{{ mgroup.enabled }} of {{ mgroup.total }} on</span>\n </div>\n <div class=\"module-grid\">\n <div class=\"module-card\" *ngFor=\"let mod of mgroup.modules\"\n [class.enabled]=\"mod.enabled\" [class.core]=\"mod.core\" [class.busy]=\"togglingKey === mod.key\"\n (click)=\"toggleModule(mod)\">\n <div class=\"module-head\">\n <mat-icon class=\"module-icon\">{{ mod.icon || 'extension' }}</mat-icon>\n <mat-icon class=\"module-state\" [class.on]=\"mod.enabled\">{{ mod.enabled ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n </div>\n <div class=\"module-title\">{{ mod.title }}</div>\n <div class=\"module-description\">{{ mod.description }}</div>\n <span class=\"module-chip\" *ngIf=\"mod.core\">Always on</span>\n </div>\n </div>\n </div>\n <div class=\"module-actions\" *ngIf=\"!modulesConfirmed\">\n <button mat-flat-button color=\"primary\" [disabled]=\"togglingKey !== ''\" (click)=\"confirmModules()\">Confirm selection</button>\n <span class=\"module-hint\">Happy with this selection? Confirm it to complete the step below.</span>\n </div>\n </mat-card>\n\n <!-- Category groups, top-down \u2014 Changed: flat spa-checklist replaces the raised mat-accordion -->\n <mat-card class=\"setup-group\" *ngFor=\"let group of groups\">\n <div class=\"group-header\">\n <h2>{{ group.name }}</h2>\n <span class=\"group-counter\">{{ group.completed }} of {{ group.total }}</span>\n </div>\n <spa-checklist [config]=\"group.config\" [itemTemplate]=\"stepExtrasTpl\"></spa-checklist>\n </mat-card>\n\n <!-- Demo data (opt-in via setupConfig.demoData) \u2014 moved here off the per-app configuration pages -->\n <mat-card class=\"setup-group setup-demo\" *ngIf=\"showDemoData\">\n <div class=\"group-header\">\n <h2>Demo data</h2>\n <span class=\"group-counter\">Explore with example records, then clear them out</span>\n </div>\n <p class=\"demo-description\">Seeding fills the system with example records so you can try it out before capturing anything real. Removing deletes only those example records.</p>\n <div class=\"demo-actions\">\n <button mat-flat-button color=\"primary\" [disabled]=\"demoBusy\" (click)=\"seedDemoData()\"><mat-icon>add_circle</mat-icon> Seed demo data</button>\n <button mat-stroked-button color=\"warn\" [disabled]=\"demoBusy\" (click)=\"removeDemoData()\"><mat-icon>delete</mat-icon> Remove demo data</button>\n </div>\n </mat-card>\n\n</div>\n\n<!-- Projected into the expanded checklist body: predefined roles picker on the roles step (v2) -->\n<ng-template #stepExtrasTpl let-item>\n <div class=\"role-templates\" *ngIf=\"hasRoleTemplates(item.data) && roleTemplates.length > 0\">\n <div class=\"role-template\" *ngFor=\"let tpl of roleTemplates\" [class.created]=\"tpl.exists\" [class.selected]=\"!tpl.exists && selectedTemplates[tpl.key]\" (click)=\"toggleTemplate(tpl)\">\n <div class=\"role-head\">\n <mat-icon class=\"role-state\" [class.on]=\"tpl.exists || selectedTemplates[tpl.key]\">{{ (tpl.exists || selectedTemplates[tpl.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"role-name\">{{ tpl.name }}</span>\n <span class=\"role-created\" *ngIf=\"tpl.exists\"><mat-icon>check</mat-icon>Created</span>\n </div>\n <div class=\"role-description\">{{ tpl.description }}</div>\n </div>\n </div>\n\n <!-- Added (presets): the preset picker. Same row as the role picker above \u2014 same state icon, same 8px stack,\n same colours \u2014 plus ONE sentence and ONE control.\n Changed (presets P4): rendered through ONE shared template for both the notification step and the sign-off\n step, parameterised by the picker object. The two sit one above the other on the same page, so a second\n near-identical block would drift and every difference would read as a mistake. -->\n <ng-container *ngIf=\"pickerFor(item.data) as picker\">\n <ng-container *ngTemplateOutlet=\"presetPickerTpl; context: { $implicit: picker }\"></ng-container>\n </ng-container>\n</ng-template>\n\n<!-- The one picker body. `picker` carries the domain: its rows, its roles, its wording, its apply call. -->\n<ng-template #presetPickerTpl let-picker>\n <div class=\"preset-picker\" *ngIf=\"picker.loaded && picker.presets.length > 0\">\n\n <!-- No selectable role \u2192 we ask for one instead of offering ticks that would build an inert rule. The wording\n is the picker's own, because \"no roles\" and \"roles nobody is in\" are different situations. -->\n <div class=\"preset-empty\" *ngIf=\"picker.roles.length === 0\">{{ picker.emptyText }}</div>\n\n <ng-container *ngIf=\"picker.roles.length > 0\">\n <div class=\"preset-section\" *ngFor=\"let pgroup of picker.groups\">\n <div class=\"preset-section-head\">\n <span class=\"preset-section-name\">{{ pgroup.name }}</span>\n <span class=\"preset-section-count\">{{ pgroup.on }} of {{ pgroup.total }}</span>\n </div>\n <div class=\"preset-row\" *ngFor=\"let row of pgroup.rows\" role=\"checkbox\" tabindex=\"0\"\n [class.configured]=\"row.preset.exists\" [class.selected]=\"!row.preset.exists && picker.selected[row.preset.key]\"\n [attr.aria-checked]=\"row.preset.exists || !!picker.selected[row.preset.key]\" [attr.aria-disabled]=\"row.preset.exists\"\n (click)=\"togglePreset(picker, row.preset)\"\n (keydown.enter)=\"togglePreset(picker, row.preset)\"\n (keydown.space)=\"togglePreset(picker, row.preset); $event.preventDefault()\">\n <div class=\"preset-head\">\n <mat-icon class=\"preset-state\" [class.on]=\"row.preset.exists || picker.selected[row.preset.key]\">{{ (row.preset.exists || picker.selected[row.preset.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"preset-name\">{{ row.preset.name }}</span>\n <span class=\"preset-configured\" *ngIf=\"row.preset.exists\"><mat-icon>check</mat-icon>Configured</span>\n </div>\n <div class=\"preset-description\">{{ row.preset.description }}</div>\n <!-- The effect sentence comes from the server whole; `before` and `after` are its two halves either side\n of the {role} placeholder, so reading the line IS reading the configuration. The sign-off sentences\n carry their own deferred-notification caveat, which is why there is no extra note here. -->\n <div class=\"preset-effect\" *ngIf=\"!row.preset.exists\">{{ row.before }}<select class=\"preset-role\" [attr.aria-label]=\"picker.roleAria + row.preset.name\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n (change)=\"setPresetRole(picker, row.preset, $any($event.target).value)\">\n <option *ngFor=\"let role of picker.roles\" [value]=\"role.value\" [selected]=\"role.value === picker.role[row.preset.key]\">{{ role.name }}</option>\n </select>{{ row.after }}</div>\n <div class=\"preset-effect preset-done\" *ngIf=\"row.preset.exists\">{{ picker.doneText }}</div>\n </div>\n </div>\n\n <!-- One page-level channel choice, notifications only. In-app is the only channel that always works; SMS is a\n stub and is never offered; an approval request is always in-app, so the sign-off picker has no such line. -->\n <div class=\"preset-email\" *ngIf=\"picker.showEmail\" role=\"checkbox\" tabindex=\"0\" [attr.aria-checked]=\"includeEmail\"\n (click)=\"toggleIncludeEmail()\"\n (keydown.enter)=\"toggleIncludeEmail()\"\n (keydown.space)=\"toggleIncludeEmail(); $event.preventDefault()\">\n <mat-icon class=\"preset-state\" [class.on]=\"includeEmail\">{{ includeEmail ? 'check_box' : 'check_box_outline_blank' }}</mat-icon>\n <span class=\"preset-email-label\">Also send these by email</span>\n <span class=\"preset-email-hint\">In-app always. Email needs your mail settings.</span>\n </div>\n </ng-container>\n </div>\n</ng-template>\n\n<!-- Graceful empty state (feature disabled or status unavailable) -->\n<div class=\"setup-empty\" *ngIf=\"!status\">\n <mat-icon>rocket_launch</mat-icon>\n <p>Setup status is not available yet.</p>\n</div>\n", styles: [".setup-page{max-width:860px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.setup-hero{padding:24px}.hero-content{display:flex;align-items:center;justify-content:space-between;gap:24px;flex-wrap:wrap}.hero-text h1{margin:0 0 4px;font-size:24px}.hero-text p{margin:0 0 8px;color:#0009}.hero-counter{font-size:13px;color:#0009}.hero-progress{flex:1;min-width:220px;max-width:340px}.hero-percent{display:block;font-size:28px;font-weight:600;color:#2e7d32;margin-bottom:6px;text-align:right}.hero-progress mat-progress-bar{height:10px;border-radius:5px}.setup-hero.celebrate{background:linear-gradient(135deg,#e8f5e9,#f1f8e9)}.celebration{justify-content:flex-start}.celebrate-icon{font-size:48px;width:48px;height:48px;color:#2e7d32;animation:celebrate-pop .6s ease-out}@keyframes celebrate-pop{0%{transform:scale(.3);opacity:0}70%{transform:scale(1.15)}to{transform:scale(1);opacity:1}}.setup-group{padding:16px}.group-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.group-header h2{margin:0;font-size:17px}.group-counter{font-size:12px;color:#0000008c}.setup-modules{padding:16px}.module-section+.module-section{margin-top:18px}.module-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.module-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.module-section-count{font-size:11px;color:#00000073;white-space:nowrap}.module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;margin-top:8px}.module-card{border:1px solid rgba(0,0,0,.12);border-radius:10px;padding:12px;cursor:pointer;transition:border-color .15s,background .15s,opacity .15s;opacity:.72;position:relative}.module-card:hover{border-color:#90a4ae}.module-card.enabled{border-color:#4caf50;background:#f6fbf6;opacity:1}.module-card.core{cursor:default}.module-card.busy{pointer-events:none;opacity:.5}.module-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.module-icon{color:#546e7a}.module-card.enabled .module-icon{color:#2e7d32}.module-state{color:#b0bec5}.module-state.on{color:#4caf50}.module-title{font-weight:600;font-size:14px;margin-bottom:4px}.module-description{font-size:12px;color:#0009;min-height:30px}.module-chip{display:inline-block;margin-top:8px;background:#eceff1;color:#546e7a;border-radius:12px;padding:2px 10px;font-size:11px}.module-actions{display:flex;align-items:center;gap:8px 12px;margin-top:14px;flex-wrap:wrap}.module-actions button{flex-shrink:0}.module-hint{font-size:12px;color:#0000008c}.role-templates{display:flex;flex-direction:column;gap:8px;margin:0 0 12px}.role-template{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.role-template:hover{border-color:#90a4ae}.role-template.selected{border-color:#4caf50;background:#f6fbf6}.role-template.created{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.role-head{display:flex;align-items:center;gap:8px}.role-state{color:#b0bec5;flex-shrink:0}.role-state.on{color:#4caf50}.role-name{font-weight:500;font-size:13px}.role-created{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32}.role-created mat-icon{font-size:16px;width:16px;height:16px}.role-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-picker{display:flex;flex-direction:column;gap:14px;margin:0 0 12px}.preset-empty{font-size:12px;line-height:18px;color:#0009;max-width:62ch}.preset-section{display:flex;flex-direction:column;gap:8px}.preset-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.preset-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.preset-section-count{font-size:11px;color:#00000073;white-space:nowrap}.preset-row{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.preset-row:hover{border-color:#90a4ae}.preset-row:focus-visible{outline:2px solid #4caf50;outline-offset:2px}.preset-row.selected{border-color:#4caf50;background:#f6fbf6}.preset-row.configured{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.preset-head{display:flex;align-items:center;gap:8px}.preset-state{color:#b0bec5;flex-shrink:0}.preset-state.on{color:#4caf50}.preset-name{font-weight:500;font-size:13px}.preset-configured{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32;white-space:nowrap}.preset-configured mat-icon{font-size:16px;width:16px;height:16px}.preset-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-effect{font-size:12px;line-height:22px;color:#000000c7;margin:4px 0 0 32px}.preset-done{color:#2e7d32}.preset-role{font-family:inherit;font-size:12px;font-weight:500;color:#2e7d32;background:#fff;border:1px solid #c8e6c9;border-radius:6px;padding:2px 4px;margin:0 2px;max-width:100%;cursor:pointer}.preset-role:hover{border-color:#4caf50}.preset-role:focus-visible{outline:2px solid #4caf50;outline-offset:1px}.preset-email{display:flex;align-items:center;gap:8px;flex-wrap:wrap;cursor:pointer;padding:2px 0}.preset-email:focus-visible{outline:2px solid #4caf50;outline-offset:2px;border-radius:6px}.preset-email-label{font-size:13px;font-weight:500}.preset-email-hint{font-size:12px;color:#0000008c}.setup-empty{text-align:center;padding:48px 16px;color:#00000080}.setup-empty mat-icon{font-size:40px;width:40px;height:40px}.demo-description{margin:8px 0 14px;font-size:13px;color:#0009}.demo-actions{display:flex;gap:12px;flex-wrap:wrap}@media (max-width: 700px){.setup-page{padding:10px 0;gap:12px}.setup-hero{padding:12px}.setup-modules,.setup-group{padding:10px}.hero-content{gap:10px}.hero-text h1{font-size:20px}.setup-hero:not(.celebrate) .hero-text p{display:none}.hero-counter{font-size:12px}.hero-progress{min-width:0;max-width:none}.hero-percent{font-size:22px;margin-bottom:4px}.hero-progress mat-progress-bar{height:8px;border-radius:4px}.celebrate-icon{font-size:36px;width:36px;height:36px}.group-header{margin-bottom:6px;gap:2px 8px}.group-header h2{font-size:15px}.group-counter{font-size:11px}.setup-modules>.group-header,.setup-demo>.group-header{flex-wrap:wrap}.setup-modules>.group-header .group-counter,.setup-demo>.group-header .group-counter{flex:1 0 100%}.module-section+.module-section{margin-top:12px}.module-section-head{padding-bottom:4px}.module-grid{gap:8px;margin-top:6px}.module-card{padding:10px}.module-head{margin-bottom:4px}.module-title{font-size:13.5px;margin-bottom:2px}.module-description{min-height:0}.module-chip{margin-top:6px}.module-actions{margin-top:10px}.role-templates,.preset-picker{gap:6px;margin-bottom:8px}.role-template,.preset-row{padding:8px 10px}.role-description,.preset-description,.preset-effect{margin-left:0}.preset-section{gap:6px}.preset-section-head{padding-bottom:4px}.demo-description{margin:6px 0 10px;font-size:12.5px}.demo-actions{gap:8px}.setup-empty{padding:28px 12px}.module-hint{flex:0 0 100%}.preset-role{display:block;width:100%;margin:4px 0 0;padding:6px 8px}.preset-email-hint{flex:0 0 100%}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i2$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: i6$1.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: ChecklistComponent, selector: "spa-checklist", inputs: ["config", "itemTemplate"] }] }); }
|
|
12854
|
+
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\n <!-- Added (seed-all): the per-area report. The card used to make ONE claim about the whole system, because the\n backend sentinel is a single customer named \"Example: \u2026\" \u2014 so one demo customer answered for products,\n suppliers and everything else, and none of it ever looked at REAL data. Each line here is one area\n answering for itself: what is in it now, and what a fill would add. Deliberately a compact one-line list\n and not the full seed-offer row used on the steps \u2014 a step is where you decide about ONE area, this is a\n report on all of them, and repeating the effect and notice paragraphs a dozen times would bury the button.\n The full wording is still shown, in the confirm, before anything is written. -->\n <div class=\"demo-areas\" *ngIf=\"demoAreas.length > 0\">\n <div class=\"demo-area\" *ngFor=\"let area of demoAreas\" [class.added]=\"area.offer.applied\">\n <mat-icon class=\"demo-area-state\" [class.on]=\"area.offer.applied\">{{ area.offer.applied ? 'check_circle' : (area.offer.icon || 'science') }}</mat-icon>\n <span class=\"demo-area-name\">{{ area.label }}</span>\n <span class=\"demo-area-count\">{{ area.state }}</span>\n </div>\n </div>\n\n <div class=\"demo-actions\">\n <!-- The owner's sentence, made literal: fill the areas that don't have data. It says HOW MANY areas, because\n a button that says only \"seed\" is asking for blind consent, and it disappears entirely when there is\n nothing left to fill rather than sitting there doing nothing when clicked. -->\n <button mat-flat-button color=\"primary\" *ngIf=\"demoPending > 0\" [disabled]=\"seedingAll || demoBusy\" (click)=\"seedAll()\"><mat-icon>auto_fix_high</mat-icon> Fill {{ demoPending === 1 ? '1 empty area' : demoPending + ' empty areas' }}</button>\n <span class=\"demo-full\" *ngIf=\"demoAreas.length > 0 && demoPending === 0\">Every area already has data.</span>\n <!-- KEPT, not replaced. The offer catalogue covers the areas it declares; each app's own demo dataset still\n comes only from here, so retiring this would take away the only way to seed those. -->\n <button mat-stroked-button color=\"primary\" [disabled]=\"demoBusy || seedingAll\" (click)=\"seedDemoData()\"><mat-icon>add_circle</mat-icon> Seed demo data</button>\n <button mat-stroked-button color=\"warn\" [disabled]=\"demoBusy || seedingAll\" (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 (seed seam): example-data offers for this step. Deliberately the SAME row the role picker and both\n preset pickers use \u2014 1px border, 8px radius, 10px/12px padding, the same green for \"already done\", the same\n 32px text indent \u2014 because this page is one the owner already likes and a fourth visual language inside it\n would be the change, not the feature. Three things are new, and each one is a stated requirement:\n \u2022 the effect line says exactly what lands, authored server-side;\n \u2022 \"Also adds \u2026\" names the prerequisites, because a vehicle cannot exist without its makes and models;\n \u2022 the notice band says in as many words that this is EXAMPLE data you can edit later. It is the only\n tinted band inside the row, so the eye finds it, and it is grey rather than amber, because this is a\n clarification and not a warning \u2014 nobody has done anything wrong by reading it.\n A row is a BUTTON, never a tick: unlike the presets there is no \"who\" to choose, so a checkbox idiom would\n promise a selection step that does not exist. -->\n <div class=\"seed-offers\" *ngIf=\"item.data && seedRowsByStep[item.data.key] as seedRows\">\n <div class=\"seed-offer\" *ngFor=\"let row of seedRows\" [class.added]=\"row.offer.applied\">\n <div class=\"seed-head\">\n <mat-icon class=\"seed-state\" [class.on]=\"row.offer.applied\">{{ row.offer.applied ? 'check_circle' : (row.offer.icon || 'science') }}</mat-icon>\n <span class=\"seed-name\">{{ row.offer.name }}</span>\n <span class=\"seed-added\" *ngIf=\"row.offer.applied\"><mat-icon>check</mat-icon>Added</span>\n </div>\n <div class=\"seed-description\">{{ row.offer.description }}</div>\n <div class=\"seed-effect\">{{ row.offer.effect }}</div>\n <div class=\"seed-also\" *ngIf=\"row.alsoAdds\">{{ row.alsoAdds }}</div>\n <div class=\"seed-notice\"><mat-icon>science</mat-icon><span>{{ row.offer.sampleNotice }}</span></div>\n <div class=\"seed-foot\">\n <span class=\"seed-already\" *ngIf=\"row.hereAlready\">{{ row.hereAlready }}</span>\n <button mat-stroked-button class=\"seed-action\" *ngIf=\"!row.offer.applied\" [disabled]=\"seedingKey !== ''\" (click)=\"applySeedOffer(row.offer)\"><mat-icon>add</mat-icon>{{ row.actionLabel }}</button>\n </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}.seed-offers{display:flex;flex-direction:column;gap:8px;margin:0 0 12px}.seed-offer{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;transition:border-color .15s,background .15s}.seed-offer.added{background:#f6fbf6;border-color:#c8e6c9}.seed-head{display:flex;align-items:center;gap:8px}.seed-state{color:#b0bec5;flex-shrink:0}.seed-state.on{color:#4caf50}.seed-name{font-weight:500;font-size:13px}.seed-added{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32;white-space:nowrap}.seed-added mat-icon{font-size:16px;width:16px;height:16px}.seed-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.seed-effect{font-size:12px;line-height:18px;color:#000000c7;margin:4px 0 0 32px}.seed-also{font-size:12px;line-height:18px;color:#0000008c;margin:2px 0 0 32px}.seed-notice{display:flex;align-items:flex-start;gap:6px;margin:8px 0 0 32px;padding:6px 8px;border-radius:6px;background:#00000009;font-size:11.5px;line-height:17px;color:#0009}.seed-notice mat-icon{font-size:15px;width:15px;height:15px;line-height:15px;flex-shrink:0;margin-top:1px;color:#0000006b}.seed-foot{display:flex;align-items:center;justify-content:space-between;gap:8px 12px;flex-wrap:wrap;margin:10px 0 0 32px}.seed-already{font-size:11px;color:#00000073}.seed-action{margin-left:auto;flex-shrink:0}.seed-action mat-icon{font-size:18px;width:18px;height:18px;margin-right:4px}.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;align-items:center}.demo-areas{display:flex;flex-direction:column;gap:6px;margin:0 0 14px}.demo-area{display:flex;align-items:center;gap:8px;border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:7px 12px}.demo-area.added{background:#f6fbf6;border-color:#c8e6c9}.demo-area-state{color:#b0bec5;flex-shrink:0}.demo-area-state.on{color:#4caf50}.demo-area-name{font-size:12.5px;color:#000c}.demo-area-count{margin-left:auto;font-size:11.5px;color:#00000073;white-space:nowrap;padding-left:8px}.demo-area.added .demo-area-count{color:#2e7d32}.demo-full{font-size:12.5px;color:#00000080}@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,.seed-offers{gap:6px;margin-bottom:8px}.role-template,.preset-row,.seed-offer{padding:8px 10px}.role-description,.preset-description,.preset-effect,.seed-description,.seed-effect,.seed-also,.seed-notice,.seed-foot{margin-left:0}.seed-foot{margin-top:8px}.seed-action{width:100%;margin-left:0}.seed-already{flex:1 0 100%;order:2}.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}.demo-areas{gap:5px;margin-bottom:10px}.demo-area{flex-wrap:wrap;padding:7px 10px}.demo-area-count{flex:1 0 100%;margin-left:32px;padding-left:0;white-space:normal}.demo-actions button{width:100%}.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"] }] }); }
|
|
12540
12855
|
}
|
|
12541
12856
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SetupGuideComponent, decorators: [{
|
|
12542
12857
|
type: Component,
|
|
12543
|
-
args: [{ selector: 'spa-setup-guide', standalone: false, template: "<div class=\"setup-page\" *ngIf=\"status\">\n\n <!-- Hero: overall readiness + celebration state at 100% -->\n <mat-card class=\"setup-hero\" [class.celebrate]=\"status.percent === 100\">\n <div class=\"hero-content\" *ngIf=\"status.percent < 100\">\n <div class=\"hero-text\">\n <h1>{{ title }}</h1>\n <p>Complete these steps to get your system ready for day-to-day operation.</p>\n <span class=\"hero-counter\">{{ status.completed }} of {{ status.total }} steps completed</span>\n </div>\n <div class=\"hero-progress\">\n <span class=\"hero-percent\">{{ status.percent }}%</span>\n <mat-progress-bar mode=\"determinate\" [value]=\"status.percent\"></mat-progress-bar>\n </div>\n </div>\n <div class=\"hero-content celebration\" *ngIf=\"status.percent === 100\">\n <mat-icon class=\"celebrate-icon\">celebration</mat-icon>\n <div class=\"hero-text\">\n <h1>You're all set!</h1>\n <p>All setup steps are complete \u2014 your system is ready to operate.</p>\n </div>\n </div>\n </mat-card>\n\n <!-- Module picker (v2): choose what the business uses; optional modules toggle steps + menus -->\n <mat-card class=\"setup-modules\" *ngIf=\"modules.length > 0\">\n <div class=\"group-header\">\n <h2>Your modules</h2>\n <span class=\"group-counter\">Tap a module to turn it on or off \u2014 you can change this anytime.</span>\n </div>\n <!-- Changed (v3): grouped \u2014 the picker now carries the whole library catalog, so cards sit under their\n group heading (the app's own modules first). A backend without groups yields one unnamed group,\n which renders as the original flat grid. -->\n <div class=\"module-section\" *ngFor=\"let mgroup of moduleGroups\">\n <div class=\"module-section-head\">\n <span class=\"module-section-name\">{{ mgroup.name }}</span>\n <span class=\"module-section-count\">{{ mgroup.enabled }} of {{ mgroup.total }} on</span>\n </div>\n <div class=\"module-grid\">\n <div class=\"module-card\" *ngFor=\"let mod of mgroup.modules\"\n [class.enabled]=\"mod.enabled\" [class.core]=\"mod.core\" [class.busy]=\"togglingKey === mod.key\"\n (click)=\"toggleModule(mod)\">\n <div class=\"module-head\">\n <mat-icon class=\"module-icon\">{{ mod.icon || 'extension' }}</mat-icon>\n <mat-icon class=\"module-state\" [class.on]=\"mod.enabled\">{{ mod.enabled ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n </div>\n <div class=\"module-title\">{{ mod.title }}</div>\n <div class=\"module-description\">{{ mod.description }}</div>\n <span class=\"module-chip\" *ngIf=\"mod.core\">Always on</span>\n </div>\n </div>\n </div>\n <div class=\"module-actions\" *ngIf=\"!modulesConfirmed\">\n <button mat-flat-button color=\"primary\" [disabled]=\"togglingKey !== ''\" (click)=\"confirmModules()\">Confirm selection</button>\n <span class=\"module-hint\">Happy with this selection? Confirm it to complete the step below.</span>\n </div>\n </mat-card>\n\n <!-- Category groups, top-down \u2014 Changed: flat spa-checklist replaces the raised mat-accordion -->\n <mat-card class=\"setup-group\" *ngFor=\"let group of groups\">\n <div class=\"group-header\">\n <h2>{{ group.name }}</h2>\n <span class=\"group-counter\">{{ group.completed }} of {{ group.total }}</span>\n </div>\n <spa-checklist [config]=\"group.config\" [itemTemplate]=\"stepExtrasTpl\"></spa-checklist>\n </mat-card>\n\n <!-- Demo data (opt-in via setupConfig.demoData) \u2014 moved here off the per-app configuration pages -->\n <mat-card class=\"setup-group setup-demo\" *ngIf=\"showDemoData\">\n <div class=\"group-header\">\n <h2>Demo data</h2>\n <span class=\"group-counter\">Explore with example records, then clear them out</span>\n </div>\n <p class=\"demo-description\">Seeding fills the system with example records so you can try it out before capturing anything real. Removing deletes only those example records.</p>\n <div class=\"demo-actions\">\n <button mat-flat-button color=\"primary\" [disabled]=\"demoBusy\" (click)=\"seedDemoData()\"><mat-icon>add_circle</mat-icon> Seed demo data</button>\n <button mat-stroked-button color=\"warn\" [disabled]=\"demoBusy\" (click)=\"removeDemoData()\"><mat-icon>delete</mat-icon> Remove demo data</button>\n </div>\n </mat-card>\n\n</div>\n\n<!-- Projected into the expanded checklist body: predefined roles picker on the roles step (v2) -->\n<ng-template #stepExtrasTpl let-item>\n <div class=\"role-templates\" *ngIf=\"hasRoleTemplates(item.data) && roleTemplates.length > 0\">\n <div class=\"role-template\" *ngFor=\"let tpl of roleTemplates\" [class.created]=\"tpl.exists\" [class.selected]=\"!tpl.exists && selectedTemplates[tpl.key]\" (click)=\"toggleTemplate(tpl)\">\n <div class=\"role-head\">\n <mat-icon class=\"role-state\" [class.on]=\"tpl.exists || selectedTemplates[tpl.key]\">{{ (tpl.exists || selectedTemplates[tpl.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"role-name\">{{ tpl.name }}</span>\n <span class=\"role-created\" *ngIf=\"tpl.exists\"><mat-icon>check</mat-icon>Created</span>\n </div>\n <div class=\"role-description\">{{ tpl.description }}</div>\n </div>\n </div>\n\n <!-- Added (presets): the preset picker. Same row as the role picker above \u2014 same state icon, same 8px stack,\n same colours \u2014 plus ONE sentence and ONE control.\n Changed (presets P4): rendered through ONE shared template for both the notification step and the sign-off\n step, parameterised by the picker object. The two sit one above the other on the same page, so a second\n near-identical block would drift and every difference would read as a mistake. -->\n <ng-container *ngIf=\"pickerFor(item.data) as picker\">\n <ng-container *ngTemplateOutlet=\"presetPickerTpl; context: { $implicit: picker }\"></ng-container>\n </ng-container>\n</ng-template>\n\n<!-- The one picker body. `picker` carries the domain: its rows, its roles, its wording, its apply call. -->\n<ng-template #presetPickerTpl let-picker>\n <div class=\"preset-picker\" *ngIf=\"picker.loaded && picker.presets.length > 0\">\n\n <!-- No selectable role \u2192 we ask for one instead of offering ticks that would build an inert rule. The wording\n is the picker's own, because \"no roles\" and \"roles nobody is in\" are different situations. -->\n <div class=\"preset-empty\" *ngIf=\"picker.roles.length === 0\">{{ picker.emptyText }}</div>\n\n <ng-container *ngIf=\"picker.roles.length > 0\">\n <div class=\"preset-section\" *ngFor=\"let pgroup of picker.groups\">\n <div class=\"preset-section-head\">\n <span class=\"preset-section-name\">{{ pgroup.name }}</span>\n <span class=\"preset-section-count\">{{ pgroup.on }} of {{ pgroup.total }}</span>\n </div>\n <div class=\"preset-row\" *ngFor=\"let row of pgroup.rows\" role=\"checkbox\" tabindex=\"0\"\n [class.configured]=\"row.preset.exists\" [class.selected]=\"!row.preset.exists && picker.selected[row.preset.key]\"\n [attr.aria-checked]=\"row.preset.exists || !!picker.selected[row.preset.key]\" [attr.aria-disabled]=\"row.preset.exists\"\n (click)=\"togglePreset(picker, row.preset)\"\n (keydown.enter)=\"togglePreset(picker, row.preset)\"\n (keydown.space)=\"togglePreset(picker, row.preset); $event.preventDefault()\">\n <div class=\"preset-head\">\n <mat-icon class=\"preset-state\" [class.on]=\"row.preset.exists || picker.selected[row.preset.key]\">{{ (row.preset.exists || picker.selected[row.preset.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"preset-name\">{{ row.preset.name }}</span>\n <span class=\"preset-configured\" *ngIf=\"row.preset.exists\"><mat-icon>check</mat-icon>Configured</span>\n </div>\n <div class=\"preset-description\">{{ row.preset.description }}</div>\n <!-- The effect sentence comes from the server whole; `before` and `after` are its two halves either side\n of the {role} placeholder, so reading the line IS reading the configuration. The sign-off sentences\n carry their own deferred-notification caveat, which is why there is no extra note here. -->\n <div class=\"preset-effect\" *ngIf=\"!row.preset.exists\">{{ row.before }}<select class=\"preset-role\" [attr.aria-label]=\"picker.roleAria + row.preset.name\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n (change)=\"setPresetRole(picker, row.preset, $any($event.target).value)\">\n <option *ngFor=\"let role of picker.roles\" [value]=\"role.value\" [selected]=\"role.value === picker.role[row.preset.key]\">{{ role.name }}</option>\n </select>{{ row.after }}</div>\n <div class=\"preset-effect preset-done\" *ngIf=\"row.preset.exists\">{{ picker.doneText }}</div>\n </div>\n </div>\n\n <!-- One page-level channel choice, notifications only. In-app is the only channel that always works; SMS is a\n stub and is never offered; an approval request is always in-app, so the sign-off picker has no such line. -->\n <div class=\"preset-email\" *ngIf=\"picker.showEmail\" role=\"checkbox\" tabindex=\"0\" [attr.aria-checked]=\"includeEmail\"\n (click)=\"toggleIncludeEmail()\"\n (keydown.enter)=\"toggleIncludeEmail()\"\n (keydown.space)=\"toggleIncludeEmail(); $event.preventDefault()\">\n <mat-icon class=\"preset-state\" [class.on]=\"includeEmail\">{{ includeEmail ? 'check_box' : 'check_box_outline_blank' }}</mat-icon>\n <span class=\"preset-email-label\">Also send these by email</span>\n <span class=\"preset-email-hint\">In-app always. Email needs your mail settings.</span>\n </div>\n </ng-container>\n </div>\n</ng-template>\n\n<!-- Graceful empty state (feature disabled or status unavailable) -->\n<div class=\"setup-empty\" *ngIf=\"!status\">\n <mat-icon>rocket_launch</mat-icon>\n <p>Setup status is not available yet.</p>\n</div>\n", styles: [".setup-page{max-width:860px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.setup-hero{padding:24px}.hero-content{display:flex;align-items:center;justify-content:space-between;gap:24px;flex-wrap:wrap}.hero-text h1{margin:0 0 4px;font-size:24px}.hero-text p{margin:0 0 8px;color:#0009}.hero-counter{font-size:13px;color:#0009}.hero-progress{flex:1;min-width:220px;max-width:340px}.hero-percent{display:block;font-size:28px;font-weight:600;color:#2e7d32;margin-bottom:6px;text-align:right}.hero-progress mat-progress-bar{height:10px;border-radius:5px}.setup-hero.celebrate{background:linear-gradient(135deg,#e8f5e9,#f1f8e9)}.celebration{justify-content:flex-start}.celebrate-icon{font-size:48px;width:48px;height:48px;color:#2e7d32;animation:celebrate-pop .6s ease-out}@keyframes celebrate-pop{0%{transform:scale(.3);opacity:0}70%{transform:scale(1.15)}to{transform:scale(1);opacity:1}}.setup-group{padding:16px}.group-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.group-header h2{margin:0;font-size:17px}.group-counter{font-size:12px;color:#0000008c}.setup-modules{padding:16px}.module-section+.module-section{margin-top:18px}.module-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.module-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.module-section-count{font-size:11px;color:#00000073;white-space:nowrap}.module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;margin-top:8px}.module-card{border:1px solid rgba(0,0,0,.12);border-radius:10px;padding:12px;cursor:pointer;transition:border-color .15s,background .15s,opacity .15s;opacity:.72;position:relative}.module-card:hover{border-color:#90a4ae}.module-card.enabled{border-color:#4caf50;background:#f6fbf6;opacity:1}.module-card.core{cursor:default}.module-card.busy{pointer-events:none;opacity:.5}.module-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.module-icon{color:#546e7a}.module-card.enabled .module-icon{color:#2e7d32}.module-state{color:#b0bec5}.module-state.on{color:#4caf50}.module-title{font-weight:600;font-size:14px;margin-bottom:4px}.module-description{font-size:12px;color:#0009;min-height:30px}.module-chip{display:inline-block;margin-top:8px;background:#eceff1;color:#546e7a;border-radius:12px;padding:2px 10px;font-size:11px}.module-actions{display:flex;align-items:center;gap:8px 12px;margin-top:14px;flex-wrap:wrap}.module-actions button{flex-shrink:0}.module-hint{font-size:12px;color:#0000008c}.role-templates{display:flex;flex-direction:column;gap:8px;margin:0 0 12px}.role-template{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.role-template:hover{border-color:#90a4ae}.role-template.selected{border-color:#4caf50;background:#f6fbf6}.role-template.created{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.role-head{display:flex;align-items:center;gap:8px}.role-state{color:#b0bec5;flex-shrink:0}.role-state.on{color:#4caf50}.role-name{font-weight:500;font-size:13px}.role-created{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32}.role-created mat-icon{font-size:16px;width:16px;height:16px}.role-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-picker{display:flex;flex-direction:column;gap:14px;margin:0 0 12px}.preset-empty{font-size:12px;line-height:18px;color:#0009;max-width:62ch}.preset-section{display:flex;flex-direction:column;gap:8px}.preset-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.preset-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.preset-section-count{font-size:11px;color:#00000073;white-space:nowrap}.preset-row{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.preset-row:hover{border-color:#90a4ae}.preset-row:focus-visible{outline:2px solid #4caf50;outline-offset:2px}.preset-row.selected{border-color:#4caf50;background:#f6fbf6}.preset-row.configured{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.preset-head{display:flex;align-items:center;gap:8px}.preset-state{color:#b0bec5;flex-shrink:0}.preset-state.on{color:#4caf50}.preset-name{font-weight:500;font-size:13px}.preset-configured{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32;white-space:nowrap}.preset-configured mat-icon{font-size:16px;width:16px;height:16px}.preset-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-effect{font-size:12px;line-height:22px;color:#000000c7;margin:4px 0 0 32px}.preset-done{color:#2e7d32}.preset-role{font-family:inherit;font-size:12px;font-weight:500;color:#2e7d32;background:#fff;border:1px solid #c8e6c9;border-radius:6px;padding:2px 4px;margin:0 2px;max-width:100%;cursor:pointer}.preset-role:hover{border-color:#4caf50}.preset-role:focus-visible{outline:2px solid #4caf50;outline-offset:1px}.preset-email{display:flex;align-items:center;gap:8px;flex-wrap:wrap;cursor:pointer;padding:2px 0}.preset-email:focus-visible{outline:2px solid #4caf50;outline-offset:2px;border-radius:6px}.preset-email-label{font-size:13px;font-weight:500}.preset-email-hint{font-size:12px;color:#0000008c}.setup-empty{text-align:center;padding:48px 16px;color:#00000080}.setup-empty mat-icon{font-size:40px;width:40px;height:40px}.demo-description{margin:8px 0 14px;font-size:13px;color:#0009}.demo-actions{display:flex;gap:12px;flex-wrap:wrap}@media (max-width: 700px){.setup-page{padding:10px 0;gap:12px}.setup-hero{padding:12px}.setup-modules,.setup-group{padding:10px}.hero-content{gap:10px}.hero-text h1{font-size:20px}.setup-hero:not(.celebrate) .hero-text p{display:none}.hero-counter{font-size:12px}.hero-progress{min-width:0;max-width:none}.hero-percent{font-size:22px;margin-bottom:4px}.hero-progress mat-progress-bar{height:8px;border-radius:4px}.celebrate-icon{font-size:36px;width:36px;height:36px}.group-header{margin-bottom:6px;gap:2px 8px}.group-header h2{font-size:15px}.group-counter{font-size:11px}.setup-modules>.group-header,.setup-demo>.group-header{flex-wrap:wrap}.setup-modules>.group-header .group-counter,.setup-demo>.group-header .group-counter{flex:1 0 100%}.module-section+.module-section{margin-top:12px}.module-section-head{padding-bottom:4px}.module-grid{gap:8px;margin-top:6px}.module-card{padding:10px}.module-head{margin-bottom:4px}.module-title{font-size:13.5px;margin-bottom:2px}.module-description{min-height:0}.module-chip{margin-top:6px}.module-actions{margin-top:10px}.role-templates,.preset-picker{gap:6px;margin-bottom:8px}.role-template,.preset-row{padding:8px 10px}.role-description,.preset-description,.preset-effect{margin-left:0}.preset-section{gap:6px}.preset-section-head{padding-bottom:4px}.demo-description{margin:6px 0 10px;font-size:12.5px}.demo-actions{gap:8px}.setup-empty{padding:28px 12px}.module-hint{flex:0 0 100%}.preset-role{display:block;width:100%;margin:4px 0 0;padding:6px 8px}.preset-email-hint{flex:0 0 100%}}\n"] }]
|
|
12858
|
+
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\n <!-- Added (seed-all): the per-area report. The card used to make ONE claim about the whole system, because the\n backend sentinel is a single customer named \"Example: \u2026\" \u2014 so one demo customer answered for products,\n suppliers and everything else, and none of it ever looked at REAL data. Each line here is one area\n answering for itself: what is in it now, and what a fill would add. Deliberately a compact one-line list\n and not the full seed-offer row used on the steps \u2014 a step is where you decide about ONE area, this is a\n report on all of them, and repeating the effect and notice paragraphs a dozen times would bury the button.\n The full wording is still shown, in the confirm, before anything is written. -->\n <div class=\"demo-areas\" *ngIf=\"demoAreas.length > 0\">\n <div class=\"demo-area\" *ngFor=\"let area of demoAreas\" [class.added]=\"area.offer.applied\">\n <mat-icon class=\"demo-area-state\" [class.on]=\"area.offer.applied\">{{ area.offer.applied ? 'check_circle' : (area.offer.icon || 'science') }}</mat-icon>\n <span class=\"demo-area-name\">{{ area.label }}</span>\n <span class=\"demo-area-count\">{{ area.state }}</span>\n </div>\n </div>\n\n <div class=\"demo-actions\">\n <!-- The owner's sentence, made literal: fill the areas that don't have data. It says HOW MANY areas, because\n a button that says only \"seed\" is asking for blind consent, and it disappears entirely when there is\n nothing left to fill rather than sitting there doing nothing when clicked. -->\n <button mat-flat-button color=\"primary\" *ngIf=\"demoPending > 0\" [disabled]=\"seedingAll || demoBusy\" (click)=\"seedAll()\"><mat-icon>auto_fix_high</mat-icon> Fill {{ demoPending === 1 ? '1 empty area' : demoPending + ' empty areas' }}</button>\n <span class=\"demo-full\" *ngIf=\"demoAreas.length > 0 && demoPending === 0\">Every area already has data.</span>\n <!-- KEPT, not replaced. The offer catalogue covers the areas it declares; each app's own demo dataset still\n comes only from here, so retiring this would take away the only way to seed those. -->\n <button mat-stroked-button color=\"primary\" [disabled]=\"demoBusy || seedingAll\" (click)=\"seedDemoData()\"><mat-icon>add_circle</mat-icon> Seed demo data</button>\n <button mat-stroked-button color=\"warn\" [disabled]=\"demoBusy || seedingAll\" (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 (seed seam): example-data offers for this step. Deliberately the SAME row the role picker and both\n preset pickers use \u2014 1px border, 8px radius, 10px/12px padding, the same green for \"already done\", the same\n 32px text indent \u2014 because this page is one the owner already likes and a fourth visual language inside it\n would be the change, not the feature. Three things are new, and each one is a stated requirement:\n \u2022 the effect line says exactly what lands, authored server-side;\n \u2022 \"Also adds \u2026\" names the prerequisites, because a vehicle cannot exist without its makes and models;\n \u2022 the notice band says in as many words that this is EXAMPLE data you can edit later. It is the only\n tinted band inside the row, so the eye finds it, and it is grey rather than amber, because this is a\n clarification and not a warning \u2014 nobody has done anything wrong by reading it.\n A row is a BUTTON, never a tick: unlike the presets there is no \"who\" to choose, so a checkbox idiom would\n promise a selection step that does not exist. -->\n <div class=\"seed-offers\" *ngIf=\"item.data && seedRowsByStep[item.data.key] as seedRows\">\n <div class=\"seed-offer\" *ngFor=\"let row of seedRows\" [class.added]=\"row.offer.applied\">\n <div class=\"seed-head\">\n <mat-icon class=\"seed-state\" [class.on]=\"row.offer.applied\">{{ row.offer.applied ? 'check_circle' : (row.offer.icon || 'science') }}</mat-icon>\n <span class=\"seed-name\">{{ row.offer.name }}</span>\n <span class=\"seed-added\" *ngIf=\"row.offer.applied\"><mat-icon>check</mat-icon>Added</span>\n </div>\n <div class=\"seed-description\">{{ row.offer.description }}</div>\n <div class=\"seed-effect\">{{ row.offer.effect }}</div>\n <div class=\"seed-also\" *ngIf=\"row.alsoAdds\">{{ row.alsoAdds }}</div>\n <div class=\"seed-notice\"><mat-icon>science</mat-icon><span>{{ row.offer.sampleNotice }}</span></div>\n <div class=\"seed-foot\">\n <span class=\"seed-already\" *ngIf=\"row.hereAlready\">{{ row.hereAlready }}</span>\n <button mat-stroked-button class=\"seed-action\" *ngIf=\"!row.offer.applied\" [disabled]=\"seedingKey !== ''\" (click)=\"applySeedOffer(row.offer)\"><mat-icon>add</mat-icon>{{ row.actionLabel }}</button>\n </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}.seed-offers{display:flex;flex-direction:column;gap:8px;margin:0 0 12px}.seed-offer{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;transition:border-color .15s,background .15s}.seed-offer.added{background:#f6fbf6;border-color:#c8e6c9}.seed-head{display:flex;align-items:center;gap:8px}.seed-state{color:#b0bec5;flex-shrink:0}.seed-state.on{color:#4caf50}.seed-name{font-weight:500;font-size:13px}.seed-added{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32;white-space:nowrap}.seed-added mat-icon{font-size:16px;width:16px;height:16px}.seed-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.seed-effect{font-size:12px;line-height:18px;color:#000000c7;margin:4px 0 0 32px}.seed-also{font-size:12px;line-height:18px;color:#0000008c;margin:2px 0 0 32px}.seed-notice{display:flex;align-items:flex-start;gap:6px;margin:8px 0 0 32px;padding:6px 8px;border-radius:6px;background:#00000009;font-size:11.5px;line-height:17px;color:#0009}.seed-notice mat-icon{font-size:15px;width:15px;height:15px;line-height:15px;flex-shrink:0;margin-top:1px;color:#0000006b}.seed-foot{display:flex;align-items:center;justify-content:space-between;gap:8px 12px;flex-wrap:wrap;margin:10px 0 0 32px}.seed-already{font-size:11px;color:#00000073}.seed-action{margin-left:auto;flex-shrink:0}.seed-action mat-icon{font-size:18px;width:18px;height:18px;margin-right:4px}.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;align-items:center}.demo-areas{display:flex;flex-direction:column;gap:6px;margin:0 0 14px}.demo-area{display:flex;align-items:center;gap:8px;border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:7px 12px}.demo-area.added{background:#f6fbf6;border-color:#c8e6c9}.demo-area-state{color:#b0bec5;flex-shrink:0}.demo-area-state.on{color:#4caf50}.demo-area-name{font-size:12.5px;color:#000c}.demo-area-count{margin-left:auto;font-size:11.5px;color:#00000073;white-space:nowrap;padding-left:8px}.demo-area.added .demo-area-count{color:#2e7d32}.demo-full{font-size:12.5px;color:#00000080}@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,.seed-offers{gap:6px;margin-bottom:8px}.role-template,.preset-row,.seed-offer{padding:8px 10px}.role-description,.preset-description,.preset-effect,.seed-description,.seed-effect,.seed-also,.seed-notice,.seed-foot{margin-left:0}.seed-foot{margin-top:8px}.seed-action{width:100%;margin-left:0}.seed-already{flex:1 0 100%;order:2}.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}.demo-areas{gap:5px;margin-bottom:10px}.demo-area{flex-wrap:wrap;padding:7px 10px}.demo-area-count{flex:1 0 100%;margin-left:32px;padding-left:0;white-space:normal}.demo-actions button{width:100%}.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"] }]
|
|
12544
12859
|
}] });
|
|
12545
12860
|
|
|
12546
12861
|
// Quiet Loading — perceived-progress engine (FSD D1).
|
|
@@ -13988,21 +14303,11 @@ class TextMultiComponent {
|
|
|
13988
14303
|
this.loadIndicator = new FieldLoadIndicator();
|
|
13989
14304
|
this.hoverChange = new EventEmitter();
|
|
13990
14305
|
} // Changed: injected ApiErrorService so a failed option load is no longer silent
|
|
13991
|
-
// Added: identical normalisation to SelectMultiComponent.toValueList, deliberately kept the same shape and
|
|
13992
|
-
// name so the two multi-value components cannot drift. `value` is documented as a ';'-delimited string, but
|
|
13993
|
-
// the truthiness guard below used to hand anything truthy straight to .split() — an array (any form whose
|
|
13994
|
-
// backend DTO takes a List<int> rewrites the bound property in place before posting) or a bare scalar both
|
|
13995
|
-
// threw "split is not a function". select-multi hit exactly that in piglet; nothing feeds text-multi an array
|
|
13996
|
-
// today, so this is hardening ahead of the first caller that does, not a repair.
|
|
13997
|
-
toValueList(raw) {
|
|
13998
|
-
if (Array.isArray(raw))
|
|
13999
|
-
return raw.filter(v => v !== null && v !== undefined && String(v).trim() !== '').map(v => String(v).trim()); // Added: the List<int> shape
|
|
14000
|
-
if (!raw)
|
|
14001
|
-
return []; // Added: preserves the previous falsy-means-nothing-entered rule for null/undefined/''
|
|
14002
|
-
return String(raw).split(';').filter(v => v.trim() !== '').map(v => v.trim()); // Changed: String() first, so a bare scalar no longer throws either
|
|
14003
|
-
}
|
|
14004
14306
|
ngOnInit() {
|
|
14005
|
-
|
|
14307
|
+
// Changed: the private toValueList is gone — it was a hand-kept COPY of SelectMultiComponent's, held in sync
|
|
14308
|
+
// by a comment asking the next reader not to let them drift. Core.parseMultiValue is that shared body, so
|
|
14309
|
+
// the two components cannot drift by construction rather than by request. Behaviour is unchanged.
|
|
14310
|
+
this.values = Core.parseMultiValue(this.value);
|
|
14006
14311
|
this.setupAutoComplete();
|
|
14007
14312
|
this.getData(this.loadAction);
|
|
14008
14313
|
}
|
|
@@ -14230,21 +14535,11 @@ class SelectMultiComponent {
|
|
|
14230
14535
|
}
|
|
14231
14536
|
this.initializeValues();
|
|
14232
14537
|
}
|
|
14233
|
-
// Added: `value` is documented as a ';'-delimited string, but real callers legitimately hold an array —
|
|
14234
|
-
// any form whose backend DTO takes a List<int> rewrites the bound property in place before posting
|
|
14235
|
-
// (piglet has four: wean, sell, move, treat). That array flowed straight back down [(value)] and
|
|
14236
|
-
// this.value.split(';') threw "split is not a function" on every subsequent change-detection pass.
|
|
14237
|
-
// Normalise on the way in rather than type-guarding at one call site, and keep emitting the ';' string
|
|
14238
|
-
// on the way out so no existing consumer's parsing changes.
|
|
14239
|
-
toValueList(raw) {
|
|
14240
|
-
if (Array.isArray(raw))
|
|
14241
|
-
return raw.filter(v => v !== null && v !== undefined && String(v).trim() !== '').map(v => String(v).trim()); // Added: the List<int> shape
|
|
14242
|
-
if (!raw)
|
|
14243
|
-
return []; // Added: preserves the previous falsy-means-nothing-selected rule for null/undefined/''
|
|
14244
|
-
return String(raw).split(';').filter(v => v.trim() !== '').map(v => v.trim()); // Changed: String() first, so a bare scalar (a single id) no longer throws either
|
|
14245
|
-
}
|
|
14246
14538
|
initializeValues() {
|
|
14247
|
-
|
|
14539
|
+
// Changed: the private toValueList is gone — its body moved to Core.parseMultiValue, THE multi-value
|
|
14540
|
+
// contract. It was correct here and still re-implemented by every consumer downstream, because a private
|
|
14541
|
+
// component member is not reusable; publishing it is the whole point. Behaviour is byte-for-byte identical.
|
|
14542
|
+
const requested = Core.parseMultiValue(this.value);
|
|
14248
14543
|
if (requested.length > 0) { // Changed: gate on the parsed request, not on truthiness of a raw value that may be an array
|
|
14249
14544
|
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
|
|
14250
14545
|
this.selectedValues = values;
|
|
@@ -15664,6 +15959,33 @@ class TableRowComponent {
|
|
|
15664
15959
|
showBanner(message) {
|
|
15665
15960
|
this.showBannerEvent.emit(message);
|
|
15666
15961
|
}
|
|
15962
|
+
// Added: a chip is a BADGE unless it declares a detailsConfig. 50 chip columns across the estate carried an
|
|
15963
|
+
// unconditional (click) with no detailsConfig — 39 status/enum and count badges plus 11 entity references that
|
|
15964
|
+
// were never wired — so every one of them rendered as a control, took focus, showed a pointer cursor, and did
|
|
15965
|
+
// nothing when clicked. columnClicked's no-detailsConfig branch emits actionClick under the COLUMN's name; no
|
|
15966
|
+
// handler anywhere in the six codebases listens for any of those 50 names, so the emit was a dead end, which is
|
|
15967
|
+
// why withdrawing it is safe for the untracked consumer apps too — their inert chips already did nothing.
|
|
15968
|
+
// Guarded here rather than in the template so the shape matches onMonogramTap above, which has resolved the same
|
|
15969
|
+
// question for the monogram cell since it gained a details path. The template also removes the AFFORDANCE
|
|
15970
|
+
// (.chip-inert + tabindex -1): swallowing a click a cell still advertises is the half fix that reads as a bug.
|
|
15971
|
+
onChipClick(column, row) {
|
|
15972
|
+
if (!column?.detailsConfig)
|
|
15973
|
+
return;
|
|
15974
|
+
this.onColumnClick(column, row);
|
|
15975
|
+
}
|
|
15976
|
+
// Added: a monogram cell can now DRILL IN. The circle replaces a name that may have been a click-through into
|
|
15977
|
+
// the record behind it (shift's Trips driver), and until now the monogram type had no details path at all — the
|
|
15978
|
+
// tap only ever surfaced the full name, so converting such a column silently deleted the navigation.
|
|
15979
|
+
// detailsConfig wins when the column declares one (the same field the chip/button types route through
|
|
15980
|
+
// columnClicked, so the dialog is opened by the table exactly as it always was); the banner stays the behaviour
|
|
15981
|
+
// for every monogram column that declares none, which is all of them today.
|
|
15982
|
+
onMonogramTap(label) {
|
|
15983
|
+
if (this.column?.detailsConfig) {
|
|
15984
|
+
this.onColumnClick(this.column, this.row);
|
|
15985
|
+
return;
|
|
15986
|
+
}
|
|
15987
|
+
this.showBanner(label);
|
|
15988
|
+
}
|
|
15667
15989
|
textDisplayed(row, column) {
|
|
15668
15990
|
let txt = row[column.name];
|
|
15669
15991
|
let max = !this.smallScreen ? column?.maxLength : column?.maxLength / 2;
|
|
@@ -15681,11 +16003,11 @@ class TableRowComponent {
|
|
|
15681
16003
|
return false;
|
|
15682
16004
|
}
|
|
15683
16005
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableRowComponent, deps: [{ token: ButtonService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15684
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TableRowComponent, isStandalone: false, selector: "app-table-row", inputs: { column: "column", row: "row", config: "config", smallScreen: "smallScreen" }, outputs: { actionClick: "actionClick", columnClick: "columnClick", showBannerEvent: "showBannerEvent" }, usesOnChanges: true, ngImport: i0, template: "<ng-container [ngSwitch]=\"column.type\">\n <ng-container *ngSwitchCase=\"'checkbox'\">\n <spa-check [value]=\"row[column.name]\" [readonly]=\"true\"></spa-check>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'select'\">\n <spa-select-lite [options]=\"column.options\" [optionDisplay]=\"column.optionDisplay\" [optionValue]=\"column.optionValue\" [(value)]=\"row[column.name]\" width=\"90%\"></spa-select-lite>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'chip'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'background-color': vm.color || '#eceff1', 'color': 'rgba(0, 0, 0, 0.87)', 'border': 'none'}\">{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'icon'\">\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date'\">\n {{row[column.name] | date : 'dd/MM/yyyy'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date-short'\">\n {{row[column.name] | date : 'd MMM'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetime'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetimesec'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm:ss'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'money'\">\n <label [ngStyle]=\"{'color': vm.color }\">{{row[column.name] | currency:'':''}}</label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <!-- Added: monogram \u2014 the row still holds the full name, so filter/sort keep working on the underlying value -->\n <ng-container *ngSwitchCase=\"'monogram'\">\n <spa-monogram [value]=\"row[column.name]\" [key]=\"column.keyField ? row[column.keyField] : null\" (tapped)=\"showBanner($event)\"></spa-monogram>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'button'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'color': vm.color}\" >{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchDefault>\n <label [ngStyle]=\"{'color': vm.color }\">\n <ng-container *ngIf=\"column.type === 'number'\">\n {{row[column.name] | number:'1.0-2'}}\n </ng-container>\n <ng-container *ngIf=\"column.type !== 'number'\">\n {{vm.text}}\n </ng-container>\n </label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.truncated\" matTooltip='Show more' matTooltipPosition=\"above\" (click)=\"showBanner(row[column.name])\">more_horiz</mat-icon>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n</ng-container>\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px;vertical-align:middle}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#e5e5e5}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.mat-mdc-cell .mat-mdc-outlined-button,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button){height:auto!important;min-height:36px!important;white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important;line-height:1.4!important;text-align:left!important;padding:6px 16px!important}.mat-mdc-cell .mat-mdc-outlined-button .mdc-button__label,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button) .mdc-button__label{white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i1$2.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1$2.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1$2.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage", "hint"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: MonogramComponent, selector: "spa-monogram", inputs: ["value", "key"], outputs: ["tapped"] }, { kind: "component", type: SelectLiteComponent, selector: "spa-select-lite" }, { kind: "pipe", type: i1$2.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$2.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
16006
|
+
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 [class.chip-inert]=\"!column.detailsConfig\" [tabIndex]=\"column.detailsConfig ? 0 : -1\" (click)=\"onChipClick(column, row)\" [ngStyle]=\"{'background-color': vm.color || '#eceff1', 'color': 'rgba(0, 0, 0, 0.87)', 'border': 'none'}\">{{row[column.name]}}</button> <!-- Changed: a chip only OFFERS a click when it can honour one \u2014 chip-inert and tabindex -1 withdraw the cursor/hover/focus affordance, onChipClick withdraws the behaviour -->\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)=\"onMonogramTap($event)\"></spa-monogram> <!-- Changed: tap opens the column's detailsConfig when it declares one, and still banners the full name when it does not -->\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}.mat-mdc-cell .mat-mdc-outlined-button.chip-inert{cursor:default;pointer-events: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: "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 }); }
|
|
15685
16007
|
}
|
|
15686
16008
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableRowComponent, decorators: [{
|
|
15687
16009
|
type: Component,
|
|
15688
|
-
args: [{ selector: 'app-table-row', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<ng-container [ngSwitch]=\"column.type\">\n <ng-container *ngSwitchCase=\"'checkbox'\">\n <spa-check [value]=\"row[column.name]\" [readonly]=\"true\"></spa-check>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'select'\">\n <spa-select-lite [options]=\"column.options\" [optionDisplay]=\"column.optionDisplay\" [optionValue]=\"column.optionValue\" [(value)]=\"row[column.name]\" width=\"90%\"></spa-select-lite>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'chip'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'background-color': vm.color || '#eceff1', 'color': 'rgba(0, 0, 0, 0.87)', 'border': 'none'}\">{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'icon'\">\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date'\">\n {{row[column.name] | date : 'dd/MM/yyyy'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date-short'\">\n {{row[column.name] | date : 'd MMM'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetime'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetimesec'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm:ss'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'money'\">\n <label [ngStyle]=\"{'color': vm.color }\">{{row[column.name] | currency:'':''}}</label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <!-- Added: monogram \u2014 the row still holds the full name, so filter/sort keep working on the underlying value -->\n <ng-container *ngSwitchCase=\"'monogram'\">\n <spa-monogram [value]=\"row[column.name]\" [key]=\"column.keyField ? row[column.keyField] : null\" (tapped)=\"showBanner($event)\"></spa-monogram>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'button'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'color': vm.color}\" >{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchDefault>\n <label [ngStyle]=\"{'color': vm.color }\">\n <ng-container *ngIf=\"column.type === 'number'\">\n {{row[column.name] | number:'1.0-2'}}\n </ng-container>\n <ng-container *ngIf=\"column.type !== 'number'\">\n {{vm.text}}\n </ng-container>\n </label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.truncated\" matTooltip='Show more' matTooltipPosition=\"above\" (click)=\"showBanner(row[column.name])\">more_horiz</mat-icon>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n</ng-container>\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px;vertical-align:middle}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#e5e5e5}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.mat-mdc-cell .mat-mdc-outlined-button,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button){height:auto!important;min-height:36px!important;white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important;line-height:1.4!important;text-align:left!important;padding:6px 16px!important}.mat-mdc-cell .mat-mdc-outlined-button .mdc-button__label,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button) .mdc-button__label{white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important}\n"] }]
|
|
16010
|
+
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 [class.chip-inert]=\"!column.detailsConfig\" [tabIndex]=\"column.detailsConfig ? 0 : -1\" (click)=\"onChipClick(column, row)\" [ngStyle]=\"{'background-color': vm.color || '#eceff1', 'color': 'rgba(0, 0, 0, 0.87)', 'border': 'none'}\">{{row[column.name]}}</button> <!-- Changed: a chip only OFFERS a click when it can honour one \u2014 chip-inert and tabindex -1 withdraw the cursor/hover/focus affordance, onChipClick withdraws the behaviour -->\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)=\"onMonogramTap($event)\"></spa-monogram> <!-- Changed: tap opens the column's detailsConfig when it declares one, and still banners the full name when it does not -->\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}.mat-mdc-cell .mat-mdc-outlined-button.chip-inert{cursor:default;pointer-events:none}\n"] }]
|
|
15689
16011
|
}], ctorParameters: () => [{ type: ButtonService }], propDecorators: { column: [{
|
|
15690
16012
|
type: Input
|
|
15691
16013
|
}], row: [{
|
|
@@ -16712,7 +17034,11 @@ class EmailComponent {
|
|
|
16712
17034
|
Validators.required,
|
|
16713
17035
|
Validators.pattern('^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$')
|
|
16714
17036
|
]);
|
|
16715
|
-
|
|
17037
|
+
// Changed: was this.value.split(';').filter(...) — the third inline copy of the ';' split, and the most
|
|
17038
|
+
// fragile: it threw outright on a null `value`, and it FILTERED blanks without TRIMMING the survivors, so a
|
|
17039
|
+
// pasted "a@b.com; c@d.com" kept the leading space and " c@d.com" then failed the chip's own email pattern.
|
|
17040
|
+
// Core.parseMultiValue is null-safe and trims, so both go away.
|
|
17041
|
+
this.emails = Core.parseMultiValue(this.value);
|
|
16716
17042
|
// Set up autocomplete filtering
|
|
16717
17043
|
this.filteredOptions = this.emailControl.valueChanges.pipe(startWith(''), map(value => this._filter(value)));
|
|
16718
17044
|
}
|
|
@@ -17679,7 +18005,12 @@ class TableComponent {
|
|
|
17679
18005
|
// A DERIVED name comes straight off a URL segment, which deriveEntityName capitalises and singularises for
|
|
17680
18006
|
// SignalR ("product-groups" -> "Product-group"). That is right for an entity key and wrong inside a
|
|
17681
18007
|
// sentence — the caption read "Loading Product-group…". De-hyphenate and lowercase it so it reads as prose.
|
|
17682
|
-
// deriveEntityName
|
|
18008
|
+
// Changed: this used to say deriveEntityName must be left alone because realTimeEntityName depends on its exact
|
|
18009
|
+
// output. The dependency is real but the conclusion was wrong, and it protected a bug for a release. The OTHER
|
|
18010
|
+
// side of that comparison is the server's `typeof(TEntity).Name` (TinWeb BaseController.BroadcastEntityChange),
|
|
18011
|
+
// never a second copy of this derivation — so the test is "does the corrected name match the C# class name",
|
|
18012
|
+
// not "did the string change". Every -ies segment failed that test BEFORE the correction and three now pass.
|
|
18013
|
+
// Change deriveEntityName only with that comparison re-run; see its own comment for the segment census.
|
|
17683
18014
|
const derived = this.deriveEntityName();
|
|
17684
18015
|
return derived ? derived.replace(/[-_]+/g, ' ').toLowerCase() : 'data';
|
|
17685
18016
|
}
|
|
@@ -17898,6 +18229,7 @@ class TableComponent {
|
|
|
17898
18229
|
this.displayedColumns = this.tableConfigService.setColumns(this.config, this.smallScreen);
|
|
17899
18230
|
this.displayedButtons = this.tableConfigService.getDisplayedButtons(this.config?.buttons, this.smallScreen, this.config);
|
|
17900
18231
|
this.actionsWidth = this.tableConfigService.getActionsWidth(this.displayedButtons, this.smallScreen, this.config);
|
|
18232
|
+
this.placeLockIcon(); // Added: convention — a locked row gets its lock cue here, AFTER displayedColumns is known, so the host column is one that is genuinely on screen (and it re-resolves on resize)
|
|
17901
18233
|
// Added: the same defect through the resize door. A row opened for edit on a wide screen keeps its seeded
|
|
17902
18234
|
// editingModel when the window narrows past 600px, but its cells vanish with the columns — so the tick would
|
|
17903
18235
|
// post values the operator can no longer see. Discard the edit instead; cancel has always been non-destructive.
|
|
@@ -18394,7 +18726,14 @@ class TableComponent {
|
|
|
18394
18726
|
//Dialog events
|
|
18395
18727
|
columnClicked(column, row) {
|
|
18396
18728
|
if (column.detailsConfig) {
|
|
18397
|
-
|
|
18729
|
+
// Changed: the drill-in is FLAGGED as one. Every column drill-in — chip (table-row.html:15), button (:79) and
|
|
18730
|
+
// monogram (:71, via onMonogramTap) — arrives here, and all three share one property the row-level view button
|
|
18731
|
+
// does not have: the record the dialog is about is named BY the row rather than BEING the row. shift's Trips
|
|
18732
|
+
// grid drills from a trip row into a DRIVER, so the clicked row is a trip. The dialog's failed-load path used
|
|
18733
|
+
// to adopt that row as the record, which renders a driver form with every field empty and no error — the
|
|
18734
|
+
// reported defect. Flagging it here rather than testing for it in the dialog keeps the knowledge where it is
|
|
18735
|
+
// certain: only the table knows the dialog was opened from a column.
|
|
18736
|
+
const button = { name: column.name, dialog: true, detailsConfig: { ...column.detailsConfig, drillIn: true } }; // Changed: spread, so the flag never mutates the shared config object an app declares once and every row reuses
|
|
18398
18737
|
this.open(button, row);
|
|
18399
18738
|
}
|
|
18400
18739
|
else {
|
|
@@ -18505,11 +18844,12 @@ class TableComponent {
|
|
|
18505
18844
|
}
|
|
18506
18845
|
return;
|
|
18507
18846
|
}
|
|
18508
|
-
|
|
18509
|
-
|
|
18510
|
-
|
|
18511
|
-
|
|
18512
|
-
this.
|
|
18847
|
+
// Changed: message + labels + destructiveness now resolve through the one shared helper instead of this
|
|
18848
|
+
// method assembling the message alone. This is the prompt behind 47 of the delete buttons across the four
|
|
18849
|
+
// apps — none of them declare a confirm of their own — so it is the site where naming the action pays off
|
|
18850
|
+
// most. With appConfig.namedDestructiveActions unset it still renders "Yes" / "No" in green/red, unchanged.
|
|
18851
|
+
const c = this.buttonService.getConfirmOptions(actionButton, row, `Are you sure you want to delete ?`, this.dataService.namedDestructiveActions);
|
|
18852
|
+
this.messageService.confirm(c.message, c.confirmLabel, c.cancelLabel, c.destructive).subscribe((result) => {
|
|
18513
18853
|
if (result == "yes") {
|
|
18514
18854
|
this.actionClickedEmit('delete', row);
|
|
18515
18855
|
this.doAction(actionButton.name, row);
|
|
@@ -18527,8 +18867,11 @@ class TableComponent {
|
|
|
18527
18867
|
row[this.config.heroField] = this.config.heroValue;
|
|
18528
18868
|
}
|
|
18529
18869
|
if (b.confirm && b.name != "delete") {
|
|
18530
|
-
|
|
18531
|
-
|
|
18870
|
+
// Changed: routed through getConfirmOptions so a custom button that names its action ('remove', or any
|
|
18871
|
+
// button carrying confirm.confirmLabel) is honoured here too. The `b.confirm` guard above is untouched, so
|
|
18872
|
+
// a button with no confirm config still runs straight through without a prompt.
|
|
18873
|
+
const c = this.buttonService.getConfirmOptions(b, row, undefined, this.dataService.namedDestructiveActions);
|
|
18874
|
+
this.messageService.confirm(c.message, c.confirmLabel, c.cancelLabel, c.destructive).subscribe((result) => {
|
|
18532
18875
|
if (result == "yes") {
|
|
18533
18876
|
this.execAction(b, row);
|
|
18534
18877
|
}
|
|
@@ -18655,6 +18998,55 @@ class TableComponent {
|
|
|
18655
18998
|
} // reuse the already-injected column instead of adding another
|
|
18656
18999
|
this.config.columns = [...this.config.columns, { name: '_offlineStatus', type: 'icon', alias: 'Sync', icons: statusIcons }]; // last data column -> renders just before Action
|
|
18657
19000
|
}
|
|
19001
|
+
//---------------- Locked-row cue (convention, no configuration) ----------------
|
|
19002
|
+
// Added: an ILock row (server projects `locked`) had NO visual indicator — the only sign was a kebab item
|
|
19003
|
+
// appearing on some rows and not others, which reads as a glitch rather than as state. This mirrors the
|
|
19004
|
+
// TinSync convention above: the cue rides an EXISTING column's icons array, so an author configures nothing.
|
|
19005
|
+
//
|
|
19006
|
+
// Deliberately DIFFERENT from placeStatusIcons in two ways:
|
|
19007
|
+
// 1. It never invents a column. Sync falls back to injecting '_offlineStatus'; a lock is a decoration on an
|
|
19008
|
+
// identity the operator already reads, not a fact worth a column of its own. No name/description/id on
|
|
19009
|
+
// screen -> no cue (the unlock button still carries the state on those grids).
|
|
19010
|
+
// 2. It resolves against displayedColumns, not config.columns, so a host cut by minColumns on a phone or by
|
|
19011
|
+
// a visible/hidden predicate is not chosen — an icon parked on a column nobody renders is invisible.
|
|
19012
|
+
//
|
|
19013
|
+
// Runs unconditionally on every table (the condition simply never fires on rows without `locked`) and is
|
|
19014
|
+
// IDEMPOTENT: TableConfig is often a shared singleton, and this re-runs on every resize, so the previously
|
|
19015
|
+
// injected icon is stripped from EVERY column before one is re-added — the host may have changed.
|
|
19016
|
+
placeLockIcon() {
|
|
19017
|
+
if (!this.config?.columns)
|
|
19018
|
+
return;
|
|
19019
|
+
const TAG = '_tinlockIcon';
|
|
19020
|
+
const lockIcon = { name: 'lock', color: '#90A4AE', tip: 'Locked — protected from editing and deletion', condition: (x) => !!(x?.locked ?? x?.Locked) }; // muted blue-grey, the tone the held/stale sync icon already uses: a cue, not an alarm. Both casings because a locked flag reaching the row un-camel-cased must not silently show nothing.
|
|
19021
|
+
lockIcon[TAG] = true;
|
|
19022
|
+
this.config.columns.forEach(c => { if (c.icons?.some(i => i[TAG]))
|
|
19023
|
+
c.icons = c.icons.filter(i => !i[TAG]); });
|
|
19024
|
+
const host = this.resolveLockHost();
|
|
19025
|
+
if (!host)
|
|
19026
|
+
return; // nothing to decorate — correct outcome, not a failure
|
|
19027
|
+
host.icons = [...(host.icons ?? []), lockIcon]; // appended, so any author icon and the sync icons keep their place and a row can carry both cues
|
|
19028
|
+
}
|
|
19029
|
+
// First of name -> description -> the table's id/PK among the columns ACTUALLY DISPLAYED. Name matching is
|
|
19030
|
+
// case-insensitive on name OR alias, exactly as placeStatusIcons resolves its host.
|
|
19031
|
+
resolveLockHost() {
|
|
19032
|
+
const displayed = (this.config.columns ?? []).filter(c => this.displayedColumns?.includes(c.name));
|
|
19033
|
+
if (!displayed.length)
|
|
19034
|
+
return null;
|
|
19035
|
+
const isNamed = (c, target) => (c.name?.toLowerCase() === target) || (c.alias?.toLowerCase() === target);
|
|
19036
|
+
const idNames = this.lockIdColumnNames();
|
|
19037
|
+
return displayed.find(c => isNamed(c, 'name'))
|
|
19038
|
+
?? displayed.find(c => isNamed(c, 'description'))
|
|
19039
|
+
?? displayed.find(c => idNames.includes(c.name?.toLowerCase()))
|
|
19040
|
+
?? null;
|
|
19041
|
+
}
|
|
19042
|
+
// The PK is resolved the same way OfflineService resolves idField (offline.service.ts:169) — heroField first,
|
|
19043
|
+
// then the entity-derived '<singular>ID' — plus a bare 'id'. Deliberately NOT "any column ending in id": that
|
|
19044
|
+
// matches real business columns (Paid, Valid) and would hang a lock off an unrelated cell.
|
|
19045
|
+
lockIdColumnNames() {
|
|
19046
|
+
const hero = (this.config.formConfig?.heroField ?? this.config.heroField ?? '').toString().toLowerCase();
|
|
19047
|
+
const entity = (this.config.entityName || this.deriveEntityName() || '').toLowerCase().replace(/[-_\s]/g, '');
|
|
19048
|
+
return [hero, 'id', entity ? entity.replace(/s$/, '') + 'id' : ''].filter(x => !!x);
|
|
19049
|
+
}
|
|
18658
19050
|
// Captures the raw server rows and merges pending ops: creates prepend, edits patch by id, deletes hide
|
|
18659
19051
|
applyOfflineOverlay(rows) {
|
|
18660
19052
|
if (!this.offlineEntry || !Array.isArray(rows))
|
|
@@ -18814,6 +19206,25 @@ class TableComponent {
|
|
|
18814
19206
|
if (!segment)
|
|
18815
19207
|
return '';
|
|
18816
19208
|
const name = segment.charAt(0).toUpperCase() + segment.slice(1); // Capitalize
|
|
19209
|
+
// Changed: <consonant>ies -> y BEFORE the bare trailing-'s' strip. Stripping one 's' turned 'categories' into
|
|
19210
|
+
// 'Categorie' and 'overtimeentries' into 'Overtimeentrie'.
|
|
19211
|
+
// The LIVE consequence is realTimeEntityName (line ~1567): tables on -ies urls subscribed to SignalR as
|
|
19212
|
+
// "Categorie"/"Countrie"/"Overtimeentrie" while the server broadcasts typeof(TEntity).Name — "Category",
|
|
19213
|
+
// "Country", "OvertimeEntry" — so those tables received NO live create/update/delete at all. Verified on a
|
|
19214
|
+
// live stack: the wire carries "Category" and this function now returns "Category" for 'categories'.
|
|
19215
|
+
// The locked-row cue reads the same token via lockIdColumnNames(), but that defect is LATENT, not active:
|
|
19216
|
+
// heroField is tried first and rescues the configs that have one, and the rest carry no PK column for a
|
|
19217
|
+
// correct singulariser to find. So this repairs real-time; it does not change any lock host today.
|
|
19218
|
+
// The rule is deliberately narrow. The estate's 181 url segments were enumerated before writing this and the
|
|
19219
|
+
// other "obvious" plural rules are net-negative here: a general '-es' strip would turn 'purchases' into
|
|
19220
|
+
// 'Purcha' and BREAK the SignalR match it makes CORRECTLY today, and a '-ches' strip would take 'gptcaches'
|
|
19221
|
+
// further from 'GptCache', not closer. A wider singulariser must be justified against that list, not against
|
|
19222
|
+
// English. '-sses' matches no segment today ('addresses' is not a table url yet) and is here so the next one
|
|
19223
|
+
// does not tempt anyone into the aggressive strip that breaks the two above.
|
|
19224
|
+
if (/[^aeiouAEIOU]ies$/.test(name))
|
|
19225
|
+
return name.slice(0, -3) + 'y'; // categories -> Category, countries -> Country
|
|
19226
|
+
if (/sses$/.test(name))
|
|
19227
|
+
return name.slice(0, -2); // addresses -> Address; never matches 'purchases' ('ases')
|
|
18817
19228
|
return name.endsWith('s') ? name.slice(0, -1) : name; // Remove trailing 's' for singular
|
|
18818
19229
|
}
|
|
18819
19230
|
// Changed: Find the ID property key in a row object (convention: ends with 'ID')
|
|
@@ -19082,7 +19493,7 @@ class DayBookComponent {
|
|
|
19082
19493
|
const stages = (this.config?.stages || []).filter(s => Core.isItemVisible(s, this.book));
|
|
19083
19494
|
const tiles = stages.map(s => ({
|
|
19084
19495
|
name: s.countField, alias: s.label, style: 'icon', icon: s.icon || 'checklist', info: s.hint,
|
|
19085
|
-
|
|
19496
|
+
// Changed (owner ruling, 2026-08-12): the tile no longer carries a value badge. It used to render data[valueField] as a grey pill beside the count — a raw unformatted number with no currency symbol, which read as ambiguous (money? an account number?) and earned no space on this surface. The COUNT is the only tile property the Day Book shows now. valueField itself is untouched and still resolves the lane group header's money (resolveValue / laneStageValue) — this removes the RENDERING, not the data.
|
|
19086
19497
|
color: Core.isItemVisible({ visible: s.alert }, this.book) && s.alert !== undefined ? '#c62828' : undefined,
|
|
19087
19498
|
action: { method: 'get', url: '' }, // never fired — clickable tiles emit tileClick, which we map to a scroll
|
|
19088
19499
|
hidden: (d) => !d?.[s.countField] || !this.inLane(s.lane), // Changed: lane filter lives in the predicate so pill clicks re-filter tiles live
|
|
@@ -19092,10 +19503,9 @@ class DayBookComponent {
|
|
|
19092
19503
|
// stages — more than any row can show — so one scrolling row beats either squeezing them or stacking
|
|
19093
19504
|
// them down the page above the lists. This is a Day Book decision made once in the library rather than
|
|
19094
19505
|
// repeated in five app configs; `stageTiles` still lets an app override it, including back to 'auto'.
|
|
19095
|
-
//
|
|
19096
|
-
//
|
|
19097
|
-
|
|
19098
|
-
this.tileConfig = { tiles, clickable: true, layout: 'carousel', hideValueOnMobile: true, ...(this.config?.stageTiles ?? {}) }; // a NEW object each build, so spa-tiles sees an input change
|
|
19506
|
+
// Changed: the hideValueOnMobile note goes with the badge. What it argued — the count is what the tile
|
|
19507
|
+
// is FOR and the money is a second opinion — is now the rule at EVERY width, not just on a phone.
|
|
19508
|
+
this.tileConfig = { tiles, clickable: true, layout: 'carousel', ...(this.config?.stageTiles ?? {}) }; // Changed: hideValueOnMobile dropped — it existed only to hide the money badge under 700px and there is no badge left to hide. Still a NEW object each build, so spa-tiles sees an input change
|
|
19099
19509
|
// Added: counts resolved onto a copy of the book, so a countField naming a LIST still shows a number
|
|
19100
19510
|
this.tileData = this.book ? { ...this.book, ...stages.reduce((a, s) => { a[s.countField] = this.stageCount(s); return a; }, {}) } : null;
|
|
19101
19511
|
}
|
|
@@ -20123,7 +20533,11 @@ class FormComponent {
|
|
|
20123
20533
|
return;
|
|
20124
20534
|
}
|
|
20125
20535
|
if (button.confirm) {
|
|
20126
|
-
|
|
20536
|
+
// Changed: passes the config's explicit labels and destructive flag straight through. Deliberately NOT
|
|
20537
|
+
// routed via buttonService.getConfirmOptions — this component does not inject ButtonService and a form
|
|
20538
|
+
// button never carries the built-in 'delete' name, so there is nothing here for the app-level flag to
|
|
20539
|
+
// infer. An explicit label in the config is honoured; anything absent still renders "Yes" / "No".
|
|
20540
|
+
this.messageService.confirm(`${button.confirm.message}`, button.confirm.confirmLabel, button.confirm.cancelLabel, button.confirm.destructive === true).subscribe((result) => {
|
|
20127
20541
|
if (result == "yes") {
|
|
20128
20542
|
this.processCall(button);
|
|
20129
20543
|
}
|
|
@@ -21637,6 +22051,43 @@ const featureGuard = (featureKey) => {
|
|
|
21637
22051
|
};
|
|
21638
22052
|
};
|
|
21639
22053
|
|
|
22054
|
+
// Functional guard for tenant module gating — the SetupService.isModuleEnabled sibling of featureGuard. The server
|
|
22055
|
+
// already refuses actions from a disabled module; this refuses the NAVIGATION so the user never reaches a dead
|
|
22056
|
+
// screen and fills in a form the server was always going to reject.
|
|
22057
|
+
//
|
|
22058
|
+
// NOTE for the next reader: this is SetupService.isModuleEnabled (the tenant's own module choice, fails OPEN), NOT
|
|
22059
|
+
// SubscriptionService.isModuleEnabled (plan entitlement, fails CLOSED). The names are identical and the semantics
|
|
22060
|
+
// are opposite; featureGuard next door covers the other one.
|
|
22061
|
+
// Deepest wins, so a leaf can override the area it sits in (invoicing lives under home/accounting). pathFromRoot
|
|
22062
|
+
// rather than route.data avoids depending on paramsInheritanceStrategy, which does not inherit data through a
|
|
22063
|
+
// parent that has both a path and a component.
|
|
22064
|
+
const resolveKey = (route) => {
|
|
22065
|
+
let key = '';
|
|
22066
|
+
route.pathFromRoot.forEach(r => { if (r.data && r.data['moduleKey'])
|
|
22067
|
+
key = r.data['moduleKey']; });
|
|
22068
|
+
return key;
|
|
22069
|
+
};
|
|
22070
|
+
// Optional key: moduleGuard('workshop') gates one route explicitly, moduleGuard() reads data.moduleKey off the
|
|
22071
|
+
// route tree so one canActivateChild registration can cover a whole subtree
|
|
22072
|
+
const moduleGuard = (moduleKey) => {
|
|
22073
|
+
return (route, state) => {
|
|
22074
|
+
const setupService = inject(SetupService);
|
|
22075
|
+
const messageService = inject(MessageService);
|
|
22076
|
+
const router = inject(Router);
|
|
22077
|
+
const key = moduleKey || resolveKey(route);
|
|
22078
|
+
if (!key || !setupService.enabled)
|
|
22079
|
+
return of(true); // No module restriction on this route, or an app with no setup concept — fail open
|
|
22080
|
+
return setupService.ensureModules().pipe(timeout$1(3000), // A slow or hung catalog must never wedge navigation
|
|
22081
|
+
catchError(() => of(true)), // Fail OPEN, exactly like isModuleEnabled and the server's ModuleCatalog.IsEnabled
|
|
22082
|
+
map(() => {
|
|
22083
|
+
if (setupService.isModuleEnabled(key))
|
|
22084
|
+
return true;
|
|
22085
|
+
messageService.toast(`${setupService.moduleTitle(key)} is switched off for your organisation. Turn it on in Getting Started.`);
|
|
22086
|
+
return router.parseUrl('/home'); // UrlTree, not navigate() + false — a UrlTree cannot race an in-flight navigation
|
|
22087
|
+
}));
|
|
22088
|
+
};
|
|
22089
|
+
};
|
|
22090
|
+
|
|
21640
22091
|
// Structural directive for plan-based feature gating in templates
|
|
21641
22092
|
// Usage: *spaFeature="'sales.quotes'" — hides element if feature is disabled
|
|
21642
22093
|
class FeatureDirective {
|
|
@@ -22143,8 +22594,18 @@ class NavMenuComponent {
|
|
|
22143
22594
|
if (!this.smallScreen) {
|
|
22144
22595
|
this.isExpanded = true;
|
|
22145
22596
|
}
|
|
22146
|
-
|
|
22147
|
-
|
|
22597
|
+
// Changed (F3): these two used to fire unconditionally right here. spa-nav-menu is app-root's FIRST CHILD, so
|
|
22598
|
+
// it wraps the whole app — login and signup included — and its ngOnInit runs long before any restored token is
|
|
22599
|
+
// attached. Both calls therefore answered 401 on every reload (two console errors, and on an anonymous page a
|
|
22600
|
+
// handle401() bounce to login), and after a fresh login only loadCount() ever ran again (login.component.ts:270),
|
|
22601
|
+
// so the module catalog stayed EMPTY for the whole session. Firing on the first non-empty token instead makes
|
|
22602
|
+
// the catalog real for the first route resolution on the deep-link/F5 path — which is what a module route guard
|
|
22603
|
+
// reads — and makes an anonymous visitor issue no setup calls at all. Strictly fewer requests than before.
|
|
22604
|
+
// distinctUntilChanged re-fires after a logout/login in the same shell, which the ngOnInit placement never did.
|
|
22605
|
+
this.authService.tokenObserv.pipe(filter(token => !!token), distinctUntilChanged()).subscribe(() => {
|
|
22606
|
+
this.setupService.loadCount(); // Changed (F3): setup badge, now on an authenticated session (persists across F5 — no SignalR for setup)
|
|
22607
|
+
this.setupService.loadModules(); // Changed (F3): module catalog for moduleKey menu gating, now on an authenticated session
|
|
22608
|
+
});
|
|
22148
22609
|
// Added: start recording the visited page for resumeLastRoute. The nav shell only exists while signed in,
|
|
22149
22610
|
// so this one subscription covers every in-app navigation without touching individual pages.
|
|
22150
22611
|
this.lastRouteService.start();
|
|
@@ -23131,13 +23592,20 @@ class DetailsDialog {
|
|
|
23131
23592
|
}
|
|
23132
23593
|
else {
|
|
23133
23594
|
this.settleRead(quiet, () => {
|
|
23134
|
-
// Changed: TinSync — if a by-action load fails offline, fall back to the row the table already passed in so existing records stay editable
|
|
23135
|
-
|
|
23595
|
+
// Changed: TinSync — if a by-action load fails offline, fall back to the row the table already passed in so existing records stay editable.
|
|
23596
|
+
// Changed: ...but ONLY when that row is a row of THIS entity. On a column drill-in it is not: shift's Trips
|
|
23597
|
+
// grid drills from a trip row into a driver, so the fallback wrote a TRIP into a driver form, set
|
|
23598
|
+
// isLoadComplete and rendered every field empty with nothing thrown — the dialog "opened blank". The
|
|
23599
|
+
// template never gated on isLoadComplete, so leaving details unset would have kept the same blank form;
|
|
23600
|
+
// a details dialog that cannot load its record has nothing to show, so it says why and closes.
|
|
23601
|
+
if (this.detailsConfig.details && !this.detailsConfig.drillIn) {
|
|
23136
23602
|
this.details = this.detailsConfig.details;
|
|
23137
23603
|
this.isLoadComplete = true;
|
|
23138
23604
|
}
|
|
23139
23605
|
else {
|
|
23140
23606
|
this.apiErrorService.presentAppFailure(apiResponse, 'load', action.url); // Changed: by-action details load failed with no cached row to fall back on — opaque server text gets the friendly dialog
|
|
23607
|
+
if (this.detailsConfig.drillIn)
|
|
23608
|
+
this.dialogRef.close(); // Added: closes with undefined, which TableComponent.open already treats as a dismissal — no refresh, no emit, no onSuccessButton
|
|
23141
23609
|
}
|
|
23142
23610
|
if (this.autoRefreshEnabled) {
|
|
23143
23611
|
this.autoRefreshEnabled = false;
|
|
@@ -23233,11 +23701,10 @@ class DetailsDialog {
|
|
|
23233
23701
|
const button = this.buttonService.getButton(this.buttons, 'delete');
|
|
23234
23702
|
if (!button)
|
|
23235
23703
|
return;
|
|
23236
|
-
|
|
23237
|
-
|
|
23238
|
-
|
|
23239
|
-
|
|
23240
|
-
this.messageService.confirm(confirmMessage).subscribe((result) => {
|
|
23704
|
+
// Changed: same shared resolver as table.deleteModel — this is the details-dialog copy of that prompt and
|
|
23705
|
+
// the two must not disagree about what a delete confirmation looks like.
|
|
23706
|
+
const c = this.buttonService.getConfirmOptions(button, this.details, `Are you sure you want to delete ?`, this.dataService.namedDestructiveActions);
|
|
23707
|
+
this.messageService.confirm(c.message, c.confirmLabel, c.cancelLabel, c.destructive).subscribe((result) => {
|
|
23241
23708
|
if (result == "yes") {
|
|
23242
23709
|
this.handleButtonAction('delete');
|
|
23243
23710
|
}
|
|
@@ -23346,11 +23813,12 @@ class DetailsDialog {
|
|
|
23346
23813
|
executeAction(button, data) {
|
|
23347
23814
|
const actionData = this.prepareActionData(button, data);
|
|
23348
23815
|
if (button.confirm) {
|
|
23349
|
-
|
|
23350
|
-
|
|
23351
|
-
|
|
23352
|
-
|
|
23353
|
-
this.
|
|
23816
|
+
// Changed: routed through the shared resolver. The delete-only fallback message is preserved by passing it
|
|
23817
|
+
// as the fallback for that button name alone, so a non-delete button with an empty confirm message still
|
|
23818
|
+
// behaves exactly as it did.
|
|
23819
|
+
const fallback = button.name == 'delete' ? `Are you sure you want to delete ?` : undefined;
|
|
23820
|
+
const c = this.buttonService.getConfirmOptions(button, this.details, fallback, this.dataService.namedDestructiveActions);
|
|
23821
|
+
this.messageService.confirm(c.message, c.confirmLabel, c.cancelLabel, c.destructive).subscribe((result) => {
|
|
23354
23822
|
if (result == "yes") {
|
|
23355
23823
|
this.performApiCall(button, actionData);
|
|
23356
23824
|
}
|
|
@@ -23546,6 +24014,10 @@ class InvitationsTableComponent {
|
|
|
23546
24014
|
this.messageService = messageService;
|
|
23547
24015
|
this.authService = authService;
|
|
23548
24016
|
this.apiErrorService = apiErrorService;
|
|
24017
|
+
// Added (2026-08-12): this component wraps a spa-table, so its host had no way to learn how many
|
|
24018
|
+
// invitations were loaded. Tenant settings shows that count on the collapsed card. Purely additive —
|
|
24019
|
+
// the other host (pages/onboarding) simply does not bind it and is unaffected.
|
|
24020
|
+
this.totalChange = new EventEmitter();
|
|
23549
24021
|
this.tableReload = new Subject();
|
|
23550
24022
|
this.invitationsTableConfig = {
|
|
23551
24023
|
greyOut: (value) => value.accepted == false,
|
|
@@ -23604,12 +24076,14 @@ class InvitationsTableComponent {
|
|
|
23604
24076
|
});
|
|
23605
24077
|
}
|
|
23606
24078
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: InvitationsTableComponent, deps: [{ token: DataServiceLib }, { token: MessageService }, { token: AuthService }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
23607
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: InvitationsTableComponent, isStandalone: false, selector: "spa-invitations-table", ngImport: i0, template: "<spa-table [config]=\"invitationsTableConfig\" (actionClick)=\"invActionClicked($event)\" [reload]=\"tableReload\"></spa-table>\n", styles: [""], dependencies: [{ 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"] }] }); }
|
|
24079
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: InvitationsTableComponent, isStandalone: false, selector: "spa-invitations-table", outputs: { totalChange: "totalChange" }, ngImport: i0, template: "<spa-table [config]=\"invitationsTableConfig\" (actionClick)=\"invActionClicked($event)\" (totalChange)=\"totalChange.emit($event)\" [reload]=\"tableReload\"></spa-table>\n", styles: [""], dependencies: [{ 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"] }] }); }
|
|
23608
24080
|
}
|
|
23609
24081
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: InvitationsTableComponent, decorators: [{
|
|
23610
24082
|
type: Component,
|
|
23611
|
-
args: [{ selector: 'spa-invitations-table', standalone: false, template: "<spa-table [config]=\"invitationsTableConfig\" (actionClick)=\"invActionClicked($event)\" [reload]=\"tableReload\"></spa-table>\n" }]
|
|
23612
|
-
}], ctorParameters: () => [{ type: DataServiceLib }, { type: MessageService }, { type: AuthService }, { type: ApiErrorService }]
|
|
24083
|
+
args: [{ selector: 'spa-invitations-table', standalone: false, template: "<spa-table [config]=\"invitationsTableConfig\" (actionClick)=\"invActionClicked($event)\" (totalChange)=\"totalChange.emit($event)\" [reload]=\"tableReload\"></spa-table>\n" }]
|
|
24084
|
+
}], ctorParameters: () => [{ type: DataServiceLib }, { type: MessageService }, { type: AuthService }, { type: ApiErrorService }], propDecorators: { totalChange: [{
|
|
24085
|
+
type: Output
|
|
24086
|
+
}] } });
|
|
23613
24087
|
|
|
23614
24088
|
// Generic AI-assisted import wizard: Upload -> Map -> Review -> Done. Config-driven via ImportConfig.
|
|
23615
24089
|
class ImportDialogComponent {
|
|
@@ -26076,6 +26550,15 @@ class RolesComponent {
|
|
|
26076
26550
|
{ value: RoleAccess.Create, name: 'Create' },
|
|
26077
26551
|
{ value: RoleAccess.Full, name: 'Full' }
|
|
26078
26552
|
];
|
|
26553
|
+
// Added: the two shapes a TOP-LEVEL capability can take, split once when config arrives rather than with a
|
|
26554
|
+
// template getter. `topPages` are top-level entries with nothing underneath them (Home, Day Book, Demo …);
|
|
26555
|
+
// `topGroups` are the ones that own sub-capabilities and become the page's sections.
|
|
26556
|
+
this.topPages = [];
|
|
26557
|
+
this.topGroups = [];
|
|
26558
|
+
this.views = []; // Added: one entry per role, in load order
|
|
26559
|
+
this.loaded = false; // Added: distinguishes "still loading" from "this tenant genuinely has no roles"
|
|
26560
|
+
this.filter = ''; // Added: live client-side search over capability names
|
|
26561
|
+
this.enabledOnly = false; // Added: "show only what is switched on" — the at-a-glance answer to "what does this role actually have"
|
|
26079
26562
|
this.renameDialogConfig = {
|
|
26080
26563
|
formConfig: {
|
|
26081
26564
|
security: { allow: [this.dataService.capRoles] }, // Added: gate rename role form by roles cap
|
|
@@ -26111,16 +26594,184 @@ class RolesComponent {
|
|
|
26111
26594
|
ngOnInit() {
|
|
26112
26595
|
this.authService.isAuthorised(this.dataService.capRoles.name);
|
|
26113
26596
|
this.loadRoles();
|
|
26114
|
-
this.dataService.appConfigObserv.subscribe(x => this.appConfig = x);
|
|
26597
|
+
this.dataService.appConfigObserv.subscribe(x => { this.appConfig = x; this.splitCaps(); this.buildViews(); }); // Changed: capItems can arrive after the roles do, so the view model is rebuilt on either
|
|
26115
26598
|
}
|
|
26116
26599
|
loadRoles() {
|
|
26117
26600
|
this.dataService.GetRole("all", "").subscribe((apiResponse) => {
|
|
26118
26601
|
this.roles = apiResponse.data;
|
|
26602
|
+
this.loaded = true;
|
|
26603
|
+
this.buildViews(); // Changed: the view model is derived from the roles, so it is rebuilt with them
|
|
26119
26604
|
});
|
|
26120
26605
|
}
|
|
26121
26606
|
refresh() {
|
|
26122
26607
|
this.loadRoles();
|
|
26123
26608
|
}
|
|
26609
|
+
// Added: splits top-level capabilities into "pages" and "groups". Called once per config change.
|
|
26610
|
+
splitCaps() {
|
|
26611
|
+
const caps = this.appConfig?.capItems || [];
|
|
26612
|
+
this.topPages = caps.filter(c => !c.capSubItems || c.capSubItems.length === 0);
|
|
26613
|
+
this.topGroups = caps.filter(c => c.capSubItems && c.capSubItems.length > 0);
|
|
26614
|
+
}
|
|
26615
|
+
// Added: a capability is "on" whether it is stored as a boolean (parents / bool caps) or as a RoleAccess
|
|
26616
|
+
// level (0 = None). `true > 0` is true in JS, so one comparison covers both — the same test the old
|
|
26617
|
+
// hasSubItemsAccess() used.
|
|
26618
|
+
isOn(value) {
|
|
26619
|
+
return value > 0;
|
|
26620
|
+
}
|
|
26621
|
+
// Added: rebuilds every role's view model. Cheap enough to run whole (200 capabilities x a handful of
|
|
26622
|
+
// roles) and only ever runs on load, not per change-detection pass.
|
|
26623
|
+
buildViews() {
|
|
26624
|
+
if (!this.roles) {
|
|
26625
|
+
this.views = [];
|
|
26626
|
+
return;
|
|
26627
|
+
}
|
|
26628
|
+
const previous = {};
|
|
26629
|
+
this.views.forEach(v => previous[v.role.roleID] = v.expanded); // keep open/shut state across a refresh
|
|
26630
|
+
this.views = this.roles.map((role, i) => {
|
|
26631
|
+
const view = { role: role, expanded: false, on: 0, total: 0, issues: 0, stats: {}, visible: {}, joined: {}, anyPages: true, anyVisible: true };
|
|
26632
|
+
this.recount(view);
|
|
26633
|
+
// The first role opens so the page never lands looking empty; everything else stays shut, which is
|
|
26634
|
+
// what makes 200 capabilities per role survivable. A refresh restores whatever was open before it.
|
|
26635
|
+
view.expanded = previous[role.roleID] !== undefined ? previous[role.roleID] : i === 0;
|
|
26636
|
+
return view;
|
|
26637
|
+
});
|
|
26638
|
+
}
|
|
26639
|
+
// Added: recomputes one role's counts. `inconsistent` is the old red asterisk's meaning, kept exactly —
|
|
26640
|
+
// this capability is off while something underneath it is on. It is REPORTED, never resolved: nothing here
|
|
26641
|
+
// writes to the role, because a parent being off with children on is a deliberate configuration this page
|
|
26642
|
+
// has supported since onCapItemChange() was disabled.
|
|
26643
|
+
recount(view) {
|
|
26644
|
+
const stats = {};
|
|
26645
|
+
let on = 0, total = 0, issues = 0;
|
|
26646
|
+
const term = (this.filter || '').trim().toLowerCase();
|
|
26647
|
+
const visit = (cap) => {
|
|
26648
|
+
let subOn = 0, subTotal = 0, subMatches = 0;
|
|
26649
|
+
(cap.capSubItems || []).forEach(sub => {
|
|
26650
|
+
const r = visit(sub);
|
|
26651
|
+
subOn += r.on;
|
|
26652
|
+
subTotal += r.total;
|
|
26653
|
+
subMatches += r.matches;
|
|
26654
|
+
});
|
|
26655
|
+
const self = this.isOn(view.role[cap.name]);
|
|
26656
|
+
const selfMatch = !term || (cap.display || '').toLowerCase().indexOf(term) >= 0;
|
|
26657
|
+
const stat = { self: self, on: subOn, total: subTotal, matches: subMatches, inconsistent: !self && subOn > 0 };
|
|
26658
|
+
stats[cap.name] = stat;
|
|
26659
|
+
if (stat.inconsistent)
|
|
26660
|
+
issues++;
|
|
26661
|
+
return { on: subOn + (self ? 1 : 0), total: subTotal + 1, matches: subMatches + (selfMatch ? 1 : 0) };
|
|
26662
|
+
};
|
|
26663
|
+
(this.appConfig?.capItems || []).forEach(cap => visit(cap));
|
|
26664
|
+
// The role-level tally counts each capability NAME once, not each position in the menu tree. Several
|
|
26665
|
+
// menu entries deliberately share one cap number — capAccountingDashboard is declared as "cap27", the
|
|
26666
|
+
// same field as capAccounting itself ("Reuses module cap number for dashboard visibility"), and eight
|
|
26667
|
+
// more do the same. Counting positions would report one capability twice and make the headline number
|
|
26668
|
+
// on the collapsed card wrong, which is the one number that has to be trustworthy.
|
|
26669
|
+
const seen = {};
|
|
26670
|
+
const tally = (cap) => {
|
|
26671
|
+
if (!seen[cap.name]) {
|
|
26672
|
+
seen[cap.name] = true;
|
|
26673
|
+
total++;
|
|
26674
|
+
if (this.isOn(view.role[cap.name]))
|
|
26675
|
+
on++;
|
|
26676
|
+
}
|
|
26677
|
+
(cap.capSubItems || []).forEach(sub => tally(sub));
|
|
26678
|
+
};
|
|
26679
|
+
(this.appConfig?.capItems || []).forEach(cap => tally(cap));
|
|
26680
|
+
view.stats = stats;
|
|
26681
|
+
view.on = on;
|
|
26682
|
+
view.total = total;
|
|
26683
|
+
view.issues = issues;
|
|
26684
|
+
this.recomputeVisibility(view);
|
|
26685
|
+
}
|
|
26686
|
+
// Added: works out what the search box and the "enabled only" switch leave on screen. Two passes, because
|
|
26687
|
+
// a capability shows when its own name matches, when a DESCENDANT matches (so the group survives), or when
|
|
26688
|
+
// an ANCESTOR matches (so searching "Inventory" shows all of Inventory).
|
|
26689
|
+
recomputeVisibility(view) {
|
|
26690
|
+
const term = (this.filter || '').trim().toLowerCase();
|
|
26691
|
+
const self = {}; // this capability's OWN name matches
|
|
26692
|
+
const subtree = {}; // it or anything below it matches
|
|
26693
|
+
const visible = {};
|
|
26694
|
+
const mark = (cap) => {
|
|
26695
|
+
const own = !term || (cap.display || '').toLowerCase().indexOf(term) >= 0;
|
|
26696
|
+
// OR-accumulated for the same reason `visible` is: nine cap names occupy two menu positions each, and a
|
|
26697
|
+
// plain assignment let the second position erase the first. Searching "payroll" set self[cap72]=true on
|
|
26698
|
+
// the Payroll section and its own "Dashboard" child then reset it to false, so the section stopped
|
|
26699
|
+
// passing "an ancestor matched" down and showed 2 of its 9 rows. Two labels for one field: if either
|
|
26700
|
+
// matches, the field matches.
|
|
26701
|
+
self[cap.name] = self[cap.name] || own;
|
|
26702
|
+
let hit = own;
|
|
26703
|
+
(cap.capSubItems || []).forEach(sub => { if (mark(sub))
|
|
26704
|
+
hit = true; });
|
|
26705
|
+
subtree[cap.name] = subtree[cap.name] || hit;
|
|
26706
|
+
return hit;
|
|
26707
|
+
};
|
|
26708
|
+
// A capability stays on screen when it matches, when something BELOW it matches (so its section survives
|
|
26709
|
+
// and you can still get to the match), or when an ancestor matched BY NAME (so searching "Payroll" shows
|
|
26710
|
+
// all of Payroll). The ancestor test deliberately uses self[], not subtree[] — passing subtree down meant
|
|
26711
|
+
// one matching child pulled its 8 non-matching siblings back onto the screen with it, which is a section
|
|
26712
|
+
// filter, not a search.
|
|
26713
|
+
const show = (cap, ancestorMatched) => {
|
|
26714
|
+
const hasChildren = !!(cap.capSubItems && cap.capSubItems.length);
|
|
26715
|
+
// Only a SECTION is kept alive by a match below it; a leaf has to match on its own name (or sit under
|
|
26716
|
+
// an ancestor that matched). Without the hasChildren guard, a leaf that shares its cap number with its
|
|
26717
|
+
// own parent inherits the parent's subtree result and appears in searches it does not match — Payroll
|
|
26718
|
+
// and its "Dashboard" child are both cap72, and nine such pairs exist across the library.
|
|
26719
|
+
const matched = self[cap.name] || (hasChildren && subtree[cap.name]) || ancestorMatched;
|
|
26720
|
+
const stat = view.stats[cap.name];
|
|
26721
|
+
// "Enabled only" hides what is off — EXCEPT anything flagged inconsistent. Filtering an inconsistency
|
|
26722
|
+
// off the screen would quietly defeat the one marker this page exists to show.
|
|
26723
|
+
const enabledOk = !this.enabledOnly || stat.self || stat.on > 0 || stat.inconsistent;
|
|
26724
|
+
// OR-accumulated, never assigned. These maps are keyed by cap NAME and nine names occupy two menu
|
|
26725
|
+
// positions each (Payroll and its own "Dashboard" child are both cap72). A plain assignment let the
|
|
26726
|
+
// second position overwrite the first, which silently hid the whole Payroll section from a search for
|
|
26727
|
+
// "commission" — the section was correctly resolved to visible, then its child reset the same key to
|
|
26728
|
+
// false. A capability is shown if ANY of its positions should show it.
|
|
26729
|
+
visible[cap.name] = visible[cap.name] || (matched && enabledOk);
|
|
26730
|
+
(cap.capSubItems || []).forEach(sub => show(sub, self[cap.name] || ancestorMatched));
|
|
26731
|
+
};
|
|
26732
|
+
(this.appConfig?.capItems || []).forEach(cap => mark(cap));
|
|
26733
|
+
(this.appConfig?.capItems || []).forEach(cap => show(cap, false));
|
|
26734
|
+
view.visible = visible;
|
|
26735
|
+
view.anyVisible = (this.appConfig?.capItems || []).some(cap => visible[cap.name]);
|
|
26736
|
+
view.anyPages = this.topPages.some(cap => visible[cap.name]); // a band header must not outlive its contents
|
|
26737
|
+
// Added: does this section have anything attached under its header (a body, an inconsistency line, or a
|
|
26738
|
+
// "matches are inside" line)? Purely cosmetic — it squares the header's bottom corners. Precomputed
|
|
26739
|
+
// rather than expressed as :has() so the look does not depend on selector support, and rather than as a
|
|
26740
|
+
// template expression so it costs nothing per change-detection pass.
|
|
26741
|
+
const joined = {};
|
|
26742
|
+
this.topGroups.forEach(cap => {
|
|
26743
|
+
const stat = view.stats[cap.name];
|
|
26744
|
+
const open = this.isOn(view.role[cap.name]);
|
|
26745
|
+
joined[cap.name] = open || (stat && stat.inconsistent) || (!!term && !open && !!stat && stat.matches > 0);
|
|
26746
|
+
});
|
|
26747
|
+
view.joined = joined;
|
|
26748
|
+
}
|
|
26749
|
+
// Added: search / filter changed — every role's visibility has to be reworked, not just the open one,
|
|
26750
|
+
// because the collapsed summaries are counted from the same maps.
|
|
26751
|
+
filterChanged() {
|
|
26752
|
+
this.views.forEach(v => this.recomputeVisibility(v));
|
|
26753
|
+
}
|
|
26754
|
+
clearFilter() {
|
|
26755
|
+
this.filter = '';
|
|
26756
|
+
this.filterChanged();
|
|
26757
|
+
}
|
|
26758
|
+
toggleEnabledOnly() {
|
|
26759
|
+
this.enabledOnly = !this.enabledOnly;
|
|
26760
|
+
this.filterChanged();
|
|
26761
|
+
}
|
|
26762
|
+
toggleRole(view) {
|
|
26763
|
+
view.expanded = !view.expanded;
|
|
26764
|
+
}
|
|
26765
|
+
// Added: single write path for every control on the page, so the counts and the inconsistency markers can
|
|
26766
|
+
// never drift away from what the checkboxes actually say.
|
|
26767
|
+
setValue(view, cap, value, topLevel = false) {
|
|
26768
|
+
view.role[cap.name] = value;
|
|
26769
|
+
if (topLevel)
|
|
26770
|
+
this.onCapItemChange(cap, value, view.role); // preserved: the top-level seam below
|
|
26771
|
+
this.recount(view);
|
|
26772
|
+
}
|
|
26773
|
+
trackByRole(index, view) { return view.role.roleID; }
|
|
26774
|
+
trackByCap(index, cap) { return cap.name; }
|
|
26124
26775
|
onCapItemChange(capItem, checked, role) {
|
|
26125
26776
|
return; //disabled to allow sub component access without granting whole menu access
|
|
26126
26777
|
if (!checked && capItem.capSubItems) {
|
|
@@ -26197,11 +26848,11 @@ class RolesComponent {
|
|
|
26197
26848
|
});
|
|
26198
26849
|
}
|
|
26199
26850
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: RolesComponent, deps: [{ token: HttpService }, { token: i1$1.Router }, { token: AuthService }, { token: DataServiceLib }, { token: DialogService }, { token: i4.MatDialog }, { token: MessageService }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
26200
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: RolesComponent, isStandalone: false, selector: "spa-roles", ngImport: i0, template: "<h4> Roles </h4>\n<hr />\n\n<div class=\"container-fluid mb-5\">\n\n <div class=\"d-flex justify-content-between mb-2\">\n\n <div >\n <button id=\"btnNewRole\" mat-raised-button color=\"primary\" (click)=\"addRole()\">New Role</button>\n </div>\n\n <div class=\"d-flex justify-content-end\">\n <button id=\"btnRefresh\" mat-icon-button color=\"primary\" (click)=\"refresh()\" matTooltip=\"refresh data\" matTooltipPosition=\"right\"><mat-icon >refresh</mat-icon></button>\n </div>\n\n </div>\n\n\n <div class=\"row mt-2 mb-1\" *ngFor=\"let role of roles\">\n\n <mat-card class=\"mat-elevation-z8\" style=\"width:100%\">\n\n <div class=\"d-flex justify-content-between align-items-center\">\n\n <label style=\"font-size: 16px;\">{{role.roleName}}</label>\n\n <button mat-icon-button color=\"primary\" matTooltip=\"Rename Role\" (click)=\"renameRole(role)\">\n <mat-icon>edit</mat-icon>\n </button>\n </div>\n\n <hr style=\"margin-top: 0px;\">\n\n <div class=\"tin-row\" style=\" font-size:12px;\">\n\n\n <div class=\"tin-row\" *ngFor=\"let capItem of appConfig.capItems\">\n\n <!-- Main item-->\n <mat-checkbox *ngIf=\"capItem.isBool || capItem.capSubItems\"\n color=\"primary\" style=\"min-width: 100px;\" [(ngModel)]=\"role[capItem.name]\" (ngModelChange)=\"onCapItemChange(capItem, $event, role)\">\n {{capItem.display}}\n <span *ngIf=\"!role[capItem.name] && hasSubItemsAccess(capItem, role)\" class=\"asterisk\" style=\"color: red;\">*</span>\n </mat-checkbox>\n\n <spa-select\n *ngIf=\"!capItem.isBool && !capItem.capSubItems\"\n [options]=\"roleAccessOptions\"\n optionDisplay=\"name\"\n optionValue=\"value\"\n [display]=\"capItem.display\"\n [(value)]=\"role[capItem.name]\"\n width=\"150px\" \n style=\"font-size: 12px;\">\n </spa-select>\n\n\n <ng-container *ngIf=\"capItem.capSubItems && role[capItem.name]\">\n\n <div class=\"tin-row\" *ngFor=\"let capSubItem of capItem.capSubItems\">\n\n\n <!-- Sub Item -->\n <mat-checkbox *ngIf=\"capSubItem.isBool\"\n color=\"primary\" style=\"min-width: 100px;\" [(ngModel)]=\"role[capSubItem.name]\">\n {{capSubItem.display}}\n </mat-checkbox>\n\n <spa-select\n *ngIf=\"!capSubItem.isBool\"\n [options]=\"roleAccessOptions\"\n optionDisplay=\"name\"\n optionValue=\"value\"\n [display]=\"capSubItem.display\"\n [(value)]=\"role[capSubItem.name]\"\n width=\"150px\"\n style=\"font-size: 12px;\">\n </spa-select>\n\n <ng-container *ngIf=\"capSubItem.capSubItems\">\n\n <div class=\"tin-row\" *ngFor=\"let capSubSubItem of capSubItem.capSubItems\">\n\n <!-- Sub Sub Items -->\n <mat-checkbox *ngIf=\"capSubSubItem.isBool\"\n color=\"primary\" style=\"min-width: 100px;\" [(ngModel)]=\"role[capSubSubItem.name]\">\n {{capSubSubItem.display}}\n </mat-checkbox>\n\n <spa-select\n *ngIf=\"!capSubSubItem.isBool\"\n [options]=\"roleAccessOptions\"\n optionDisplay=\"name\"\n optionValue=\"value\"\n [display]=\"capSubSubItem.display\"\n [(value)]=\"role[capSubSubItem.name]\"\n width=\"150px\" \n style=\"font-size: 12px;\">\n </spa-select>\n\n </div>\n\n </ng-container>\n\n\n\n </div>\n\n </ng-container>\n\n </div>\n\n </div>\n\n\n <mat-card-actions>\n\n <button mat-raised-button color=\"primary\" (click)=\"updateRole(role)\" style=\"margin-right:10px;\">\n <mat-icon>done_all</mat-icon>\n Update\n </button>\n\n <button mat-raised-button (click)=\"deleteRole(role)\" style=\"margin-right:10px\">\n <mat-icon>delete</mat-icon>\n Delete\n </button>\n\n </mat-card-actions>\n\n </mat-card>\n\n </div>\n\n <hr style=\"margin-top: 50px;\" />\n\n\n</div>\n\n", styles: [""], dependencies: [{ kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$2.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"] }, { 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: i19.MatCardActions, selector: "mat-card-actions", inputs: ["align"], exportAs: ["matCardActions"] }, { 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"] }] }); }
|
|
26851
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: RolesComponent, isStandalone: false, selector: "spa-roles", ngImport: i0, template: "<!-- Changed: the page was one wrapping run of 100+ controls per role with no grouping, no hierarchy and no way\n to find anything. Same controls, same rules, rearranged: roles collapse to a summary line, capabilities are\n grouped under the section they belong to, and there is a search. Two rules are load-bearing and unchanged \u2014\n a section's sub-items exist only while that section is switched on, and a section that is OFF while\n something underneath it is ON is FLAGGED, never corrected. -->\n\n<div class=\"rp-page\">\n\n <div class=\"rp-head\">\n\n <div class=\"rp-head-text\">\n <h1>Roles</h1>\n <p class=\"rp-lead\">What each role can reach. Switch a section on to see and set the pages inside it.</p>\n </div>\n\n <div class=\"rp-head-actions\">\n <button id=\"btnNewRole\" mat-raised-button color=\"primary\" (click)=\"addRole()\">New Role</button>\n <button id=\"btnRefresh\" mat-icon-button color=\"primary\" (click)=\"refresh()\" matTooltip=\"Refresh roles\"><mat-icon>refresh</mat-icon></button>\n </div>\n\n </div>\n\n <!-- Added: the two ways of finding something in 200 capabilities \u2014 search it, or show only what is on. -->\n <div class=\"rp-tools\" *ngIf=\"views.length\">\n\n <div class=\"rp-search\">\n <mat-icon>search</mat-icon>\n <input type=\"text\" name=\"capFilter\" placeholder=\"Search capabilities\" autocomplete=\"off\" [(ngModel)]=\"filter\" (ngModelChange)=\"filterChanged()\" />\n <button mat-icon-button *ngIf=\"filter\" (click)=\"clearFilter()\" matTooltip=\"Clear search\"><mat-icon>close</mat-icon></button>\n </div>\n\n <button type=\"button\" class=\"rp-toggle\" [class.active]=\"enabledOnly\" (click)=\"toggleEnabledOnly()\">\n <mat-icon>{{ enabledOnly ? 'check_box' : 'check_box_outline_blank' }}</mat-icon>\n Only what is switched on\n </button>\n\n </div>\n\n <div class=\"rp-empty\" *ngIf=\"loaded && !views.length\">\n <mat-icon>work_outline</mat-icon>\n <p>No roles yet. Create one to start granting access.</p>\n </div>\n\n <mat-card class=\"rp-role\" *ngFor=\"let view of views; trackBy: trackByRole\" [class.open]=\"view.expanded\">\n\n <!-- The whole strip is the control that opens the role, so a collapsed role still says everything it has\n to say: its name, how much is on, and whether anything inside it needs looking at. -->\n <div class=\"rp-rhead\">\n\n <button type=\"button\" class=\"rp-rtoggle\" (click)=\"toggleRole(view)\" [attr.aria-expanded]=\"view.expanded\">\n <mat-icon class=\"rp-chev\">{{ view.expanded ? 'expand_more' : 'chevron_right' }}</mat-icon>\n <span class=\"rp-name\">{{ view.role.roleName }}</span>\n <span class=\"rp-summary\">{{ view.on }} of {{ view.total }} capabilities on</span>\n <span class=\"rp-issues\" *ngIf=\"view.issues\" matTooltip=\"A section is off while capabilities inside it are on\"><mat-icon>error_outline</mat-icon>{{ view.issues }} {{ view.issues === 1 ? 'inconsistency' : 'inconsistencies' }}</span>\n </button>\n\n <button mat-icon-button color=\"primary\" matTooltip=\"Rename Role\" (click)=\"renameRole(view.role)\"><mat-icon>edit</mat-icon></button>\n\n </div>\n\n <div class=\"rp-rbody\" *ngIf=\"view.expanded\">\n\n <p class=\"rp-none\" *ngIf=\"!view.anyVisible\">Nothing matches the current search or filter.</p>\n\n <!-- Top-level entries that own nothing underneath them. -->\n <div class=\"rp-band\" *ngIf=\"topPages.length && view.anyPages\">\n\n <div class=\"rp-band-head\">\n <span class=\"rp-band-name\">Pages</span>\n <span class=\"rp-band-note\">top-level menu items with no sub-items</span>\n </div>\n\n <div class=\"rp-grid\">\n <ng-container *ngFor=\"let cap of topPages; trackBy: trackByCap\">\n <div class=\"rp-item\" *ngIf=\"view.visible[cap.name]\">\n <mat-checkbox *ngIf=\"cap.isBool\" color=\"primary\" [ngModel]=\"view.role[cap.name]\" (ngModelChange)=\"setValue(view, cap, $event, true)\">{{ cap.display }}</mat-checkbox>\n <spa-select *ngIf=\"!cap.isBool\" [options]=\"roleAccessOptions\" optionDisplay=\"name\" optionValue=\"value\" [required]=\"false\" [display]=\"cap.display\" [value]=\"view.role[cap.name]\" (valueChange)=\"setValue(view, cap, $event, true)\" width=\"100%\"></spa-select>\n </div>\n </ng-container>\n </div>\n\n </div>\n\n <!-- One section per top-level capability that owns sub-items. -->\n <div class=\"rp-groups\">\n\n <ng-container *ngFor=\"let cap of topGroups; trackBy: trackByCap\">\n\n <section class=\"rp-group\" *ngIf=\"view.visible[cap.name]\">\n\n <div class=\"rp-ghead\" [class.on]=\"view.stats[cap.name].self\" [class.flag]=\"view.stats[cap.name].inconsistent\" [class.joined]=\"view.joined[cap.name]\">\n <mat-checkbox class=\"rp-gcheck\" color=\"primary\" [ngModel]=\"view.role[cap.name]\" (ngModelChange)=\"setValue(view, cap, $event, true)\"><mat-icon class=\"rp-gicon\">{{ cap.icon }}</mat-icon><span class=\"rp-gname\">{{ cap.display }}</span></mat-checkbox>\n <span class=\"rp-gcount\">{{ view.stats[cap.name].on }} of {{ view.stats[cap.name].total }} inside are on</span>\n <mat-icon class=\"rp-gflag\" *ngIf=\"view.stats[cap.name].inconsistent\">error_outline</mat-icon>\n </div>\n\n <!-- The inconsistency, said in words. The section is off, so its body is hidden and this line has\n the space; it reports and does nothing else \u2014 the values underneath are left exactly as set. -->\n <div class=\"rp-note flag\" *ngIf=\"!view.role[cap.name] && view.stats[cap.name].inconsistent\">\n <mat-icon>error_outline</mat-icon>\n <span><strong>{{ view.stats[cap.name].on }}</strong> of {{ view.stats[cap.name].total }} capabilities inside {{ cap.display }} are switched on while {{ cap.display }} itself is off, so they are probably unreachable. Nothing has been changed \u2014 switch {{ cap.display }} on to review them.</span>\n </div>\n\n <div class=\"rp-note\" *ngIf=\"!view.role[cap.name] && !view.stats[cap.name].inconsistent && filter && view.stats[cap.name].matches\">\n <mat-icon>search</mat-icon>\n <span>{{ view.stats[cap.name].matches }} capabilities inside {{ cap.display }} match this search. Switch {{ cap.display }} on to set them.</span>\n </div>\n\n <!-- PARENT-DRIVEN HIDING, unchanged: sub-items render only while the parent is on. -->\n <div class=\"rp-gbody\" *ngIf=\"cap.capSubItems && view.role[cap.name]\">\n\n <div class=\"rp-grid\">\n\n <ng-container *ngFor=\"let sub of cap.capSubItems; trackBy: trackByCap\">\n\n <ng-container *ngIf=\"view.visible[sub.name]\">\n\n <!-- A sub-item with its own sub-items gets its own block. Its children are NOT gated on its\n value \u2014 that is how the page has always behaved at this level and changing it would put\n capabilities out of reach. -->\n <div class=\"rp-sub\" *ngIf=\"sub.capSubItems && sub.capSubItems.length\" [class.flag]=\"view.stats[sub.name].inconsistent\">\n\n <div class=\"rp-subhead\">\n <mat-checkbox *ngIf=\"sub.isBool\" color=\"primary\" [ngModel]=\"view.role[sub.name]\" (ngModelChange)=\"setValue(view, sub, $event)\">{{ sub.display }}</mat-checkbox>\n <spa-select *ngIf=\"!sub.isBool\" [options]=\"roleAccessOptions\" optionDisplay=\"name\" optionValue=\"value\" [required]=\"false\" [display]=\"sub.display\" [value]=\"view.role[sub.name]\" (valueChange)=\"setValue(view, sub, $event)\" width=\"100%\"></spa-select>\n <mat-icon class=\"rp-gflag\" *ngIf=\"view.stats[sub.name].inconsistent\" matTooltip=\"{{ view.stats[sub.name].on }} capabilities below this are on while it is off\">error_outline</mat-icon>\n </div>\n\n <div class=\"rp-grid inner\">\n <ng-container *ngFor=\"let leaf of sub.capSubItems; trackBy: trackByCap\">\n <div class=\"rp-item\" *ngIf=\"view.visible[leaf.name]\">\n <mat-checkbox *ngIf=\"leaf.isBool\" color=\"primary\" [ngModel]=\"view.role[leaf.name]\" (ngModelChange)=\"setValue(view, leaf, $event)\">{{ leaf.display }}</mat-checkbox>\n <spa-select *ngIf=\"!leaf.isBool\" [options]=\"roleAccessOptions\" optionDisplay=\"name\" optionValue=\"value\" [required]=\"false\" [display]=\"leaf.display\" [value]=\"view.role[leaf.name]\" (valueChange)=\"setValue(view, leaf, $event)\" width=\"100%\"></spa-select>\n </div>\n </ng-container>\n </div>\n\n </div>\n\n <div class=\"rp-item\" *ngIf=\"!sub.capSubItems || !sub.capSubItems.length\">\n <mat-checkbox *ngIf=\"sub.isBool\" color=\"primary\" [ngModel]=\"view.role[sub.name]\" (ngModelChange)=\"setValue(view, sub, $event)\">{{ sub.display }}</mat-checkbox>\n <spa-select *ngIf=\"!sub.isBool\" [options]=\"roleAccessOptions\" optionDisplay=\"name\" optionValue=\"value\" [required]=\"false\" [display]=\"sub.display\" [value]=\"view.role[sub.name]\" (valueChange)=\"setValue(view, sub, $event)\" width=\"100%\"></spa-select>\n </div>\n\n </ng-container>\n\n </ng-container>\n\n </div>\n\n </div>\n\n </section>\n\n </ng-container>\n\n </div>\n\n <div class=\"rp-actions\">\n <button mat-raised-button color=\"primary\" (click)=\"updateRole(view.role)\"><mat-icon>done_all</mat-icon> Update</button>\n <button mat-raised-button (click)=\"deleteRole(view.role)\"><mat-icon>delete</mat-icon> Delete</button>\n </div>\n\n </div>\n\n </mat-card>\n\n</div>\n", styles: [":host{--rp-accent: #1565c0;--rp-on: #2e7d32;--rp-on-edge: #c8e6c9;--rp-on-tint: #f6fbf6;--rp-flag: #ef6c00;--rp-flag-tint: #fff8e1;--rp-quiet: rgba(0, 0, 0, .55)}.rp-page{max-width:1400px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.rp-head{display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap}.rp-head-text h1{margin:0;font-size:24px}.rp-head-text .rp-lead{margin:4px 0 0;font-size:14px;color:var(--rp-quiet);max-width:72ch}.rp-head-actions{display:flex;align-items:center;gap:8px;margin-left:auto}.rp-tools{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.rp-search{display:flex;align-items:center;gap:6px;flex:1 1 260px;max-width:380px;min-height:38px;border:1px solid rgba(0,0,0,.12);border-radius:8px;background:#fff;padding:0 4px 0 10px;transition:border-color .15s}.rp-search:focus-within{border-color:var(--rp-accent)}.rp-search>mat-icon{font-size:19px;width:19px;height:19px;color:#00000073;flex-shrink:0}.rp-search input{flex:1;min-width:0;border:0;outline:0;background:transparent;font:inherit;font-size:13.5px;padding:8px 0}.rp-search input::placeholder{color:#0006}.rp-toggle{display:inline-flex;align-items:center;gap:6px;min-height:38px;border:1px solid rgba(0,0,0,.12);border-radius:19px;background:transparent;padding:4px 14px;font:inherit;font-size:13px;color:#000000b3;cursor:pointer;transition:border-color .15s,background .15s,color .15s}.rp-toggle:hover{border-color:#90a4ae}.rp-toggle.active{border-color:var(--rp-accent);background:#e3f2fd;color:var(--rp-accent);font-weight:500}.rp-toggle mat-icon{font-size:18px;width:18px;height:18px}.rp-role{padding:0;border:1px solid #e0e0e0;border-radius:10px;box-shadow:none!important;overflow:hidden}.rp-rhead{display:flex;align-items:center;gap:4px;padding:4px 8px 4px 4px}.rp-role.open .rp-rhead{border-bottom:1px solid #eceff1}.rp-rtoggle{display:flex;align-items:center;gap:10px;flex:1;min-width:0;min-height:44px;border:0;border-radius:8px;background:transparent;padding:6px 8px;font:inherit;text-align:left;cursor:pointer;transition:background .15s}.rp-rtoggle:hover{background:#f5f7f9}.rp-rtoggle:focus-visible{outline:2px solid var(--rp-accent);outline-offset:-2px}.rp-chev{color:#00000073;flex-shrink:0}.rp-name{font-size:16px;font-weight:600;letter-spacing:.2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.rp-summary{font-size:12.5px;color:var(--rp-quiet);font-variant-numeric:tabular-nums;white-space:nowrap}.rp-issues{display:inline-flex;align-items:center;gap:4px;margin-left:auto;border:1px solid #ffe0b2;border-radius:12px;background:var(--rp-flag-tint);padding:2px 10px;font-size:12px;font-weight:500;color:var(--rp-flag);white-space:nowrap}.rp-issues mat-icon{font-size:15px;width:15px;height:15px}.rp-rbody{display:flex;flex-direction:column;gap:16px;padding:14px 16px 16px}.rp-none{margin:0;font-size:13px;color:var(--rp-quiet)}.rp-band-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.rp-band-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.rp-band-note{font-size:11px;color:#00000073}.rp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:2px 12px;align-items:start;margin-top:8px}.rp-grid.inner{grid-template-columns:repeat(auto-fill,minmax(170px,1fr));margin-top:4px}.rp-item{min-width:0;display:flex;align-items:center;min-height:38px}.rp-item spa-select{display:block;width:100%}.rp-groups{display:flex;flex-direction:column;gap:8px}.rp-group{display:flex;flex-direction:column}.rp-ghead{display:flex;align-items:center;gap:10px;border:1px solid #e0e0e0;border-radius:6px;background:#fafafa;padding:4px 14px 4px 8px;min-height:40px;transition:background .15s,border-color .15s}.rp-ghead.on{border-color:var(--rp-on-edge);background:var(--rp-on-tint)}.rp-ghead.flag{border-color:#ffe0b2;background:var(--rp-flag-tint)}.rp-ghead.joined{border-bottom-left-radius:0;border-bottom-right-radius:0}.rp-gcheck{min-width:0}.rp-gicon{vertical-align:middle;margin-right:8px;font-size:19px;width:19px;height:19px;color:#546e7a}.rp-ghead.on .rp-gicon{color:var(--rp-on)}.rp-gname{font-size:14.5px;font-weight:600;letter-spacing:.2px;vertical-align:middle}.rp-gcount{margin-left:auto;font-size:12px;color:#00000080;font-variant-numeric:tabular-nums;white-space:nowrap}.rp-gflag{flex-shrink:0;color:var(--rp-flag);font-size:20px;width:20px;height:20px}.rp-gbody{border:1px solid #e0e0e0;border-top:0;border-radius:0 0 6px 6px;background:#fff;padding:2px 14px 10px}.rp-gbody>.rp-grid{margin-top:0}.rp-note{display:flex;align-items:flex-start;gap:8px;border:1px solid #e0e0e0;border-top:0;border-radius:0 0 6px 6px;background:#fff;padding:9px 14px 11px;font-size:12.5px;line-height:1.45;color:var(--rp-quiet)}.rp-note.flag{border-color:#ffe0b2;background:var(--rp-flag-tint);color:#000000b8}.rp-note mat-icon{flex-shrink:0;font-size:18px;width:18px;height:18px;color:#0006;margin-top:1px}.rp-note.flag mat-icon{color:var(--rp-flag)}.rp-note strong{font-weight:600;font-variant-numeric:tabular-nums}.rp-sub{grid-column:1 / -1;border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:8px 12px 10px;margin:6px 0;background:#fcfdfd}.rp-sub.flag{border-color:#ffe0b2;background:var(--rp-flag-tint)}.rp-subhead{display:flex;align-items:center;gap:8px}.rp-subhead spa-select{display:block;flex:1 1 230px;max-width:300px}.rp-actions{display:flex;gap:10px;flex-wrap:wrap;border-top:1px solid #eceff1;margin-top:2px;padding-top:14px}.rp-empty{text-align:center;padding:48px 16px;color:#00000080}.rp-empty mat-icon{font-size:40px;width:40px;height:40px}@media (max-width: 700px){.rp-page{padding:10px 0;gap:12px}.rp-head{gap:6px 10px;align-items:center}.rp-head-text{flex:1 1 auto;min-width:0}.rp-head-text h1{font-size:20px}.rp-head-text .rp-lead{display:none}.rp-head-actions{margin-left:auto;gap:2px}.rp-tools{gap:8px}.rp-search{flex:1 0 100%;max-width:none}.rp-toggle{min-height:36px;padding:4px 12px;font-size:12.5px}.rp-rhead{padding:2px 4px 2px 2px}.rp-rtoggle{gap:6px;padding:6px;flex-wrap:wrap}.rp-name{font-size:15px;flex:1 1 auto}.rp-summary{flex:1 0 100%;padding-left:30px;font-size:12px}.rp-issues{margin-left:0;font-size:11.5px;padding:1px 8px}.rp-rbody{gap:12px;padding:10px}.rp-grid,.rp-grid.inner{grid-template-columns:1fr;gap:2px}.rp-item{min-height:38px}.rp-groups{gap:8px}.rp-ghead{padding:4px 10px 4px 6px;gap:6px;min-height:42px;flex-wrap:wrap}.rp-gname{font-size:14px}.rp-gcount{order:1;flex:1 0 100%;margin-left:0;padding-left:42px;font-size:11.5px}.rp-ghead .rp-gflag{order:0;margin-left:auto}.rp-gbody{padding:2px 10px 10px}.rp-sub{padding:8px 10px 10px}.rp-subhead spa-select{max-width:none}.rp-note{padding:8px 10px 10px;font-size:12px}.rp-actions{gap:8px;padding-top:12px}.rp-actions button{flex:1 1 auto}.rp-empty{padding:32px 12px}}\n"], dependencies: [{ kind: "directive", type: i2$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$2.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"] }, { 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: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }] }); }
|
|
26201
26852
|
}
|
|
26202
26853
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: RolesComponent, decorators: [{
|
|
26203
26854
|
type: Component,
|
|
26204
|
-
args: [{ selector: "spa-roles", standalone: false, template: "<h4> Roles </h4>\n<hr />\n\n<div class=\"container-fluid mb-5\">\n\n <div class=\"d-flex justify-content-between mb-2\">\n\n <div >\n <button id=\"btnNewRole\" mat-raised-button color=\"primary\" (click)=\"addRole()\">New Role</button>\n </div>\n\n <div class=\"d-flex justify-content-end\">\n <button id=\"btnRefresh\" mat-icon-button color=\"primary\" (click)=\"refresh()\" matTooltip=\"refresh data\" matTooltipPosition=\"right\"><mat-icon >refresh</mat-icon></button>\n </div>\n\n </div>\n\n\n <div class=\"row mt-2 mb-1\" *ngFor=\"let role of roles\">\n\n <mat-card class=\"mat-elevation-z8\" style=\"width:100%\">\n\n <div class=\"d-flex justify-content-between align-items-center\">\n\n <label style=\"font-size: 16px;\">{{role.roleName}}</label>\n\n <button mat-icon-button color=\"primary\" matTooltip=\"Rename Role\" (click)=\"renameRole(role)\">\n <mat-icon>edit</mat-icon>\n </button>\n </div>\n\n <hr style=\"margin-top: 0px;\">\n\n <div class=\"tin-row\" style=\" font-size:12px;\">\n\n\n <div class=\"tin-row\" *ngFor=\"let capItem of appConfig.capItems\">\n\n <!-- Main item-->\n <mat-checkbox *ngIf=\"capItem.isBool || capItem.capSubItems\"\n color=\"primary\" style=\"min-width: 100px;\" [(ngModel)]=\"role[capItem.name]\" (ngModelChange)=\"onCapItemChange(capItem, $event, role)\">\n {{capItem.display}}\n <span *ngIf=\"!role[capItem.name] && hasSubItemsAccess(capItem, role)\" class=\"asterisk\" style=\"color: red;\">*</span>\n </mat-checkbox>\n\n <spa-select\n *ngIf=\"!capItem.isBool && !capItem.capSubItems\"\n [options]=\"roleAccessOptions\"\n optionDisplay=\"name\"\n optionValue=\"value\"\n [display]=\"capItem.display\"\n [(value)]=\"role[capItem.name]\"\n width=\"150px\" \n style=\"font-size: 12px;\">\n </spa-select>\n\n\n <ng-container *ngIf=\"capItem.capSubItems && role[capItem.name]\">\n\n <div class=\"tin-row\" *ngFor=\"let capSubItem of capItem.capSubItems\">\n\n\n <!-- Sub Item -->\n <mat-checkbox *ngIf=\"capSubItem.isBool\"\n color=\"primary\" style=\"min-width: 100px;\" [(ngModel)]=\"role[capSubItem.name]\">\n {{capSubItem.display}}\n </mat-checkbox>\n\n <spa-select\n *ngIf=\"!capSubItem.isBool\"\n [options]=\"roleAccessOptions\"\n optionDisplay=\"name\"\n optionValue=\"value\"\n [display]=\"capSubItem.display\"\n [(value)]=\"role[capSubItem.name]\"\n width=\"150px\"\n style=\"font-size: 12px;\">\n </spa-select>\n\n <ng-container *ngIf=\"capSubItem.capSubItems\">\n\n <div class=\"tin-row\" *ngFor=\"let capSubSubItem of capSubItem.capSubItems\">\n\n <!-- Sub Sub Items -->\n <mat-checkbox *ngIf=\"capSubSubItem.isBool\"\n color=\"primary\" style=\"min-width: 100px;\" [(ngModel)]=\"role[capSubSubItem.name]\">\n {{capSubSubItem.display}}\n </mat-checkbox>\n\n <spa-select\n *ngIf=\"!capSubSubItem.isBool\"\n [options]=\"roleAccessOptions\"\n optionDisplay=\"name\"\n optionValue=\"value\"\n [display]=\"capSubSubItem.display\"\n [(value)]=\"role[capSubSubItem.name]\"\n width=\"150px\" \n style=\"font-size: 12px;\">\n </spa-select>\n\n </div>\n\n </ng-container>\n\n\n\n </div>\n\n </ng-container>\n\n </div>\n\n </div>\n\n\n <mat-card-actions>\n\n <button mat-raised-button color=\"primary\" (click)=\"updateRole(role)\" style=\"margin-right:10px;\">\n <mat-icon>done_all</mat-icon>\n Update\n </button>\n\n <button mat-raised-button (click)=\"deleteRole(role)\" style=\"margin-right:10px\">\n <mat-icon>delete</mat-icon>\n Delete\n </button>\n\n </mat-card-actions>\n\n </mat-card>\n\n </div>\n\n <hr style=\"margin-top: 50px;\" />\n\n\n</div>\n\n" }]
|
|
26855
|
+
args: [{ selector: "spa-roles", standalone: false, template: "<!-- Changed: the page was one wrapping run of 100+ controls per role with no grouping, no hierarchy and no way\n to find anything. Same controls, same rules, rearranged: roles collapse to a summary line, capabilities are\n grouped under the section they belong to, and there is a search. Two rules are load-bearing and unchanged \u2014\n a section's sub-items exist only while that section is switched on, and a section that is OFF while\n something underneath it is ON is FLAGGED, never corrected. -->\n\n<div class=\"rp-page\">\n\n <div class=\"rp-head\">\n\n <div class=\"rp-head-text\">\n <h1>Roles</h1>\n <p class=\"rp-lead\">What each role can reach. Switch a section on to see and set the pages inside it.</p>\n </div>\n\n <div class=\"rp-head-actions\">\n <button id=\"btnNewRole\" mat-raised-button color=\"primary\" (click)=\"addRole()\">New Role</button>\n <button id=\"btnRefresh\" mat-icon-button color=\"primary\" (click)=\"refresh()\" matTooltip=\"Refresh roles\"><mat-icon>refresh</mat-icon></button>\n </div>\n\n </div>\n\n <!-- Added: the two ways of finding something in 200 capabilities \u2014 search it, or show only what is on. -->\n <div class=\"rp-tools\" *ngIf=\"views.length\">\n\n <div class=\"rp-search\">\n <mat-icon>search</mat-icon>\n <input type=\"text\" name=\"capFilter\" placeholder=\"Search capabilities\" autocomplete=\"off\" [(ngModel)]=\"filter\" (ngModelChange)=\"filterChanged()\" />\n <button mat-icon-button *ngIf=\"filter\" (click)=\"clearFilter()\" matTooltip=\"Clear search\"><mat-icon>close</mat-icon></button>\n </div>\n\n <button type=\"button\" class=\"rp-toggle\" [class.active]=\"enabledOnly\" (click)=\"toggleEnabledOnly()\">\n <mat-icon>{{ enabledOnly ? 'check_box' : 'check_box_outline_blank' }}</mat-icon>\n Only what is switched on\n </button>\n\n </div>\n\n <div class=\"rp-empty\" *ngIf=\"loaded && !views.length\">\n <mat-icon>work_outline</mat-icon>\n <p>No roles yet. Create one to start granting access.</p>\n </div>\n\n <mat-card class=\"rp-role\" *ngFor=\"let view of views; trackBy: trackByRole\" [class.open]=\"view.expanded\">\n\n <!-- The whole strip is the control that opens the role, so a collapsed role still says everything it has\n to say: its name, how much is on, and whether anything inside it needs looking at. -->\n <div class=\"rp-rhead\">\n\n <button type=\"button\" class=\"rp-rtoggle\" (click)=\"toggleRole(view)\" [attr.aria-expanded]=\"view.expanded\">\n <mat-icon class=\"rp-chev\">{{ view.expanded ? 'expand_more' : 'chevron_right' }}</mat-icon>\n <span class=\"rp-name\">{{ view.role.roleName }}</span>\n <span class=\"rp-summary\">{{ view.on }} of {{ view.total }} capabilities on</span>\n <span class=\"rp-issues\" *ngIf=\"view.issues\" matTooltip=\"A section is off while capabilities inside it are on\"><mat-icon>error_outline</mat-icon>{{ view.issues }} {{ view.issues === 1 ? 'inconsistency' : 'inconsistencies' }}</span>\n </button>\n\n <button mat-icon-button color=\"primary\" matTooltip=\"Rename Role\" (click)=\"renameRole(view.role)\"><mat-icon>edit</mat-icon></button>\n\n </div>\n\n <div class=\"rp-rbody\" *ngIf=\"view.expanded\">\n\n <p class=\"rp-none\" *ngIf=\"!view.anyVisible\">Nothing matches the current search or filter.</p>\n\n <!-- Top-level entries that own nothing underneath them. -->\n <div class=\"rp-band\" *ngIf=\"topPages.length && view.anyPages\">\n\n <div class=\"rp-band-head\">\n <span class=\"rp-band-name\">Pages</span>\n <span class=\"rp-band-note\">top-level menu items with no sub-items</span>\n </div>\n\n <div class=\"rp-grid\">\n <ng-container *ngFor=\"let cap of topPages; trackBy: trackByCap\">\n <div class=\"rp-item\" *ngIf=\"view.visible[cap.name]\">\n <mat-checkbox *ngIf=\"cap.isBool\" color=\"primary\" [ngModel]=\"view.role[cap.name]\" (ngModelChange)=\"setValue(view, cap, $event, true)\">{{ cap.display }}</mat-checkbox>\n <spa-select *ngIf=\"!cap.isBool\" [options]=\"roleAccessOptions\" optionDisplay=\"name\" optionValue=\"value\" [required]=\"false\" [display]=\"cap.display\" [value]=\"view.role[cap.name]\" (valueChange)=\"setValue(view, cap, $event, true)\" width=\"100%\"></spa-select>\n </div>\n </ng-container>\n </div>\n\n </div>\n\n <!-- One section per top-level capability that owns sub-items. -->\n <div class=\"rp-groups\">\n\n <ng-container *ngFor=\"let cap of topGroups; trackBy: trackByCap\">\n\n <section class=\"rp-group\" *ngIf=\"view.visible[cap.name]\">\n\n <div class=\"rp-ghead\" [class.on]=\"view.stats[cap.name].self\" [class.flag]=\"view.stats[cap.name].inconsistent\" [class.joined]=\"view.joined[cap.name]\">\n <mat-checkbox class=\"rp-gcheck\" color=\"primary\" [ngModel]=\"view.role[cap.name]\" (ngModelChange)=\"setValue(view, cap, $event, true)\"><mat-icon class=\"rp-gicon\">{{ cap.icon }}</mat-icon><span class=\"rp-gname\">{{ cap.display }}</span></mat-checkbox>\n <span class=\"rp-gcount\">{{ view.stats[cap.name].on }} of {{ view.stats[cap.name].total }} inside are on</span>\n <mat-icon class=\"rp-gflag\" *ngIf=\"view.stats[cap.name].inconsistent\">error_outline</mat-icon>\n </div>\n\n <!-- The inconsistency, said in words. The section is off, so its body is hidden and this line has\n the space; it reports and does nothing else \u2014 the values underneath are left exactly as set. -->\n <div class=\"rp-note flag\" *ngIf=\"!view.role[cap.name] && view.stats[cap.name].inconsistent\">\n <mat-icon>error_outline</mat-icon>\n <span><strong>{{ view.stats[cap.name].on }}</strong> of {{ view.stats[cap.name].total }} capabilities inside {{ cap.display }} are switched on while {{ cap.display }} itself is off, so they are probably unreachable. Nothing has been changed \u2014 switch {{ cap.display }} on to review them.</span>\n </div>\n\n <div class=\"rp-note\" *ngIf=\"!view.role[cap.name] && !view.stats[cap.name].inconsistent && filter && view.stats[cap.name].matches\">\n <mat-icon>search</mat-icon>\n <span>{{ view.stats[cap.name].matches }} capabilities inside {{ cap.display }} match this search. Switch {{ cap.display }} on to set them.</span>\n </div>\n\n <!-- PARENT-DRIVEN HIDING, unchanged: sub-items render only while the parent is on. -->\n <div class=\"rp-gbody\" *ngIf=\"cap.capSubItems && view.role[cap.name]\">\n\n <div class=\"rp-grid\">\n\n <ng-container *ngFor=\"let sub of cap.capSubItems; trackBy: trackByCap\">\n\n <ng-container *ngIf=\"view.visible[sub.name]\">\n\n <!-- A sub-item with its own sub-items gets its own block. Its children are NOT gated on its\n value \u2014 that is how the page has always behaved at this level and changing it would put\n capabilities out of reach. -->\n <div class=\"rp-sub\" *ngIf=\"sub.capSubItems && sub.capSubItems.length\" [class.flag]=\"view.stats[sub.name].inconsistent\">\n\n <div class=\"rp-subhead\">\n <mat-checkbox *ngIf=\"sub.isBool\" color=\"primary\" [ngModel]=\"view.role[sub.name]\" (ngModelChange)=\"setValue(view, sub, $event)\">{{ sub.display }}</mat-checkbox>\n <spa-select *ngIf=\"!sub.isBool\" [options]=\"roleAccessOptions\" optionDisplay=\"name\" optionValue=\"value\" [required]=\"false\" [display]=\"sub.display\" [value]=\"view.role[sub.name]\" (valueChange)=\"setValue(view, sub, $event)\" width=\"100%\"></spa-select>\n <mat-icon class=\"rp-gflag\" *ngIf=\"view.stats[sub.name].inconsistent\" matTooltip=\"{{ view.stats[sub.name].on }} capabilities below this are on while it is off\">error_outline</mat-icon>\n </div>\n\n <div class=\"rp-grid inner\">\n <ng-container *ngFor=\"let leaf of sub.capSubItems; trackBy: trackByCap\">\n <div class=\"rp-item\" *ngIf=\"view.visible[leaf.name]\">\n <mat-checkbox *ngIf=\"leaf.isBool\" color=\"primary\" [ngModel]=\"view.role[leaf.name]\" (ngModelChange)=\"setValue(view, leaf, $event)\">{{ leaf.display }}</mat-checkbox>\n <spa-select *ngIf=\"!leaf.isBool\" [options]=\"roleAccessOptions\" optionDisplay=\"name\" optionValue=\"value\" [required]=\"false\" [display]=\"leaf.display\" [value]=\"view.role[leaf.name]\" (valueChange)=\"setValue(view, leaf, $event)\" width=\"100%\"></spa-select>\n </div>\n </ng-container>\n </div>\n\n </div>\n\n <div class=\"rp-item\" *ngIf=\"!sub.capSubItems || !sub.capSubItems.length\">\n <mat-checkbox *ngIf=\"sub.isBool\" color=\"primary\" [ngModel]=\"view.role[sub.name]\" (ngModelChange)=\"setValue(view, sub, $event)\">{{ sub.display }}</mat-checkbox>\n <spa-select *ngIf=\"!sub.isBool\" [options]=\"roleAccessOptions\" optionDisplay=\"name\" optionValue=\"value\" [required]=\"false\" [display]=\"sub.display\" [value]=\"view.role[sub.name]\" (valueChange)=\"setValue(view, sub, $event)\" width=\"100%\"></spa-select>\n </div>\n\n </ng-container>\n\n </ng-container>\n\n </div>\n\n </div>\n\n </section>\n\n </ng-container>\n\n </div>\n\n <div class=\"rp-actions\">\n <button mat-raised-button color=\"primary\" (click)=\"updateRole(view.role)\"><mat-icon>done_all</mat-icon> Update</button>\n <button mat-raised-button (click)=\"deleteRole(view.role)\"><mat-icon>delete</mat-icon> Delete</button>\n </div>\n\n </div>\n\n </mat-card>\n\n</div>\n", styles: [":host{--rp-accent: #1565c0;--rp-on: #2e7d32;--rp-on-edge: #c8e6c9;--rp-on-tint: #f6fbf6;--rp-flag: #ef6c00;--rp-flag-tint: #fff8e1;--rp-quiet: rgba(0, 0, 0, .55)}.rp-page{max-width:1400px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.rp-head{display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap}.rp-head-text h1{margin:0;font-size:24px}.rp-head-text .rp-lead{margin:4px 0 0;font-size:14px;color:var(--rp-quiet);max-width:72ch}.rp-head-actions{display:flex;align-items:center;gap:8px;margin-left:auto}.rp-tools{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.rp-search{display:flex;align-items:center;gap:6px;flex:1 1 260px;max-width:380px;min-height:38px;border:1px solid rgba(0,0,0,.12);border-radius:8px;background:#fff;padding:0 4px 0 10px;transition:border-color .15s}.rp-search:focus-within{border-color:var(--rp-accent)}.rp-search>mat-icon{font-size:19px;width:19px;height:19px;color:#00000073;flex-shrink:0}.rp-search input{flex:1;min-width:0;border:0;outline:0;background:transparent;font:inherit;font-size:13.5px;padding:8px 0}.rp-search input::placeholder{color:#0006}.rp-toggle{display:inline-flex;align-items:center;gap:6px;min-height:38px;border:1px solid rgba(0,0,0,.12);border-radius:19px;background:transparent;padding:4px 14px;font:inherit;font-size:13px;color:#000000b3;cursor:pointer;transition:border-color .15s,background .15s,color .15s}.rp-toggle:hover{border-color:#90a4ae}.rp-toggle.active{border-color:var(--rp-accent);background:#e3f2fd;color:var(--rp-accent);font-weight:500}.rp-toggle mat-icon{font-size:18px;width:18px;height:18px}.rp-role{padding:0;border:1px solid #e0e0e0;border-radius:10px;box-shadow:none!important;overflow:hidden}.rp-rhead{display:flex;align-items:center;gap:4px;padding:4px 8px 4px 4px}.rp-role.open .rp-rhead{border-bottom:1px solid #eceff1}.rp-rtoggle{display:flex;align-items:center;gap:10px;flex:1;min-width:0;min-height:44px;border:0;border-radius:8px;background:transparent;padding:6px 8px;font:inherit;text-align:left;cursor:pointer;transition:background .15s}.rp-rtoggle:hover{background:#f5f7f9}.rp-rtoggle:focus-visible{outline:2px solid var(--rp-accent);outline-offset:-2px}.rp-chev{color:#00000073;flex-shrink:0}.rp-name{font-size:16px;font-weight:600;letter-spacing:.2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.rp-summary{font-size:12.5px;color:var(--rp-quiet);font-variant-numeric:tabular-nums;white-space:nowrap}.rp-issues{display:inline-flex;align-items:center;gap:4px;margin-left:auto;border:1px solid #ffe0b2;border-radius:12px;background:var(--rp-flag-tint);padding:2px 10px;font-size:12px;font-weight:500;color:var(--rp-flag);white-space:nowrap}.rp-issues mat-icon{font-size:15px;width:15px;height:15px}.rp-rbody{display:flex;flex-direction:column;gap:16px;padding:14px 16px 16px}.rp-none{margin:0;font-size:13px;color:var(--rp-quiet)}.rp-band-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.rp-band-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.rp-band-note{font-size:11px;color:#00000073}.rp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:2px 12px;align-items:start;margin-top:8px}.rp-grid.inner{grid-template-columns:repeat(auto-fill,minmax(170px,1fr));margin-top:4px}.rp-item{min-width:0;display:flex;align-items:center;min-height:38px}.rp-item spa-select{display:block;width:100%}.rp-groups{display:flex;flex-direction:column;gap:8px}.rp-group{display:flex;flex-direction:column}.rp-ghead{display:flex;align-items:center;gap:10px;border:1px solid #e0e0e0;border-radius:6px;background:#fafafa;padding:4px 14px 4px 8px;min-height:40px;transition:background .15s,border-color .15s}.rp-ghead.on{border-color:var(--rp-on-edge);background:var(--rp-on-tint)}.rp-ghead.flag{border-color:#ffe0b2;background:var(--rp-flag-tint)}.rp-ghead.joined{border-bottom-left-radius:0;border-bottom-right-radius:0}.rp-gcheck{min-width:0}.rp-gicon{vertical-align:middle;margin-right:8px;font-size:19px;width:19px;height:19px;color:#546e7a}.rp-ghead.on .rp-gicon{color:var(--rp-on)}.rp-gname{font-size:14.5px;font-weight:600;letter-spacing:.2px;vertical-align:middle}.rp-gcount{margin-left:auto;font-size:12px;color:#00000080;font-variant-numeric:tabular-nums;white-space:nowrap}.rp-gflag{flex-shrink:0;color:var(--rp-flag);font-size:20px;width:20px;height:20px}.rp-gbody{border:1px solid #e0e0e0;border-top:0;border-radius:0 0 6px 6px;background:#fff;padding:2px 14px 10px}.rp-gbody>.rp-grid{margin-top:0}.rp-note{display:flex;align-items:flex-start;gap:8px;border:1px solid #e0e0e0;border-top:0;border-radius:0 0 6px 6px;background:#fff;padding:9px 14px 11px;font-size:12.5px;line-height:1.45;color:var(--rp-quiet)}.rp-note.flag{border-color:#ffe0b2;background:var(--rp-flag-tint);color:#000000b8}.rp-note mat-icon{flex-shrink:0;font-size:18px;width:18px;height:18px;color:#0006;margin-top:1px}.rp-note.flag mat-icon{color:var(--rp-flag)}.rp-note strong{font-weight:600;font-variant-numeric:tabular-nums}.rp-sub{grid-column:1 / -1;border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:8px 12px 10px;margin:6px 0;background:#fcfdfd}.rp-sub.flag{border-color:#ffe0b2;background:var(--rp-flag-tint)}.rp-subhead{display:flex;align-items:center;gap:8px}.rp-subhead spa-select{display:block;flex:1 1 230px;max-width:300px}.rp-actions{display:flex;gap:10px;flex-wrap:wrap;border-top:1px solid #eceff1;margin-top:2px;padding-top:14px}.rp-empty{text-align:center;padding:48px 16px;color:#00000080}.rp-empty mat-icon{font-size:40px;width:40px;height:40px}@media (max-width: 700px){.rp-page{padding:10px 0;gap:12px}.rp-head{gap:6px 10px;align-items:center}.rp-head-text{flex:1 1 auto;min-width:0}.rp-head-text h1{font-size:20px}.rp-head-text .rp-lead{display:none}.rp-head-actions{margin-left:auto;gap:2px}.rp-tools{gap:8px}.rp-search{flex:1 0 100%;max-width:none}.rp-toggle{min-height:36px;padding:4px 12px;font-size:12.5px}.rp-rhead{padding:2px 4px 2px 2px}.rp-rtoggle{gap:6px;padding:6px;flex-wrap:wrap}.rp-name{font-size:15px;flex:1 1 auto}.rp-summary{flex:1 0 100%;padding-left:30px;font-size:12px}.rp-issues{margin-left:0;font-size:11.5px;padding:1px 8px}.rp-rbody{gap:12px;padding:10px}.rp-grid,.rp-grid.inner{grid-template-columns:1fr;gap:2px}.rp-item{min-height:38px}.rp-groups{gap:8px}.rp-ghead{padding:4px 10px 4px 6px;gap:6px;min-height:42px;flex-wrap:wrap}.rp-gname{font-size:14px}.rp-gcount{order:1;flex:1 0 100%;margin-left:0;padding-left:42px;font-size:11.5px}.rp-ghead .rp-gflag{order:0;margin-left:auto}.rp-gbody{padding:2px 10px 10px}.rp-sub{padding:8px 10px 10px}.rp-subhead spa-select{max-width:none}.rp-note{padding:8px 10px 10px;font-size:12px}.rp-actions{gap:8px;padding-top:12px}.rp-actions button{flex:1 1 auto}.rp-empty{padding:32px 12px}}\n"] }]
|
|
26205
26856
|
}], ctorParameters: () => [{ type: HttpService }, { type: i1$1.Router }, { type: AuthService }, { type: DataServiceLib }, { type: DialogService }, { type: i4.MatDialog }, { type: MessageService }, { type: ApiErrorService }] });
|
|
26206
26857
|
|
|
26207
26858
|
class CreateAccountComponent {
|
|
@@ -29051,15 +29702,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
29051
29702
|
}] });
|
|
29052
29703
|
|
|
29053
29704
|
// Changed: Exported routes array for use by SpaHomeRoutingModule
|
|
29705
|
+
//
|
|
29706
|
+
// Added (F3): leaf-level data.moduleKey. INERT metadata — nothing in this library reads it and no guard is
|
|
29707
|
+
// attached here; a consumer opts in from its own home route. The path says "accounting" but three separate
|
|
29708
|
+
// ModuleCatalog modules have pages under it, so the leaves that do NOT belong to Accounting say so explicitly
|
|
29709
|
+
// (deepest data wins in the resolver, so a leaf overrides the area). Keys are transcribed from the CapItem
|
|
29710
|
+
// tree in datalib.service.ts — the same source the nav menu already gates on — not invented:
|
|
29711
|
+
// invoicing -> capInvoicing.capSubItems (datalib.service.ts:372) — dashboard, quotes, invoices,
|
|
29712
|
+
// credit notes, receipts, revenue schedules, aging, statements
|
|
29713
|
+
// purchasing -> capSupplierAging (:520) is a child of capPurchasingModule (:456) despite its accounting path
|
|
29714
|
+
// Everything left bare here is a capAccounting sub-item (:267) and correctly inherits the area's "accounting".
|
|
29054
29715
|
const ACCOUNTING_ROUTES = [
|
|
29055
29716
|
{ path: "accounts", component: AccountsComponent },
|
|
29056
29717
|
{ path: "aggregates", component: AggregatesComponent },
|
|
29057
29718
|
{ path: "transaction-types", component: TransactionTypesComponent },
|
|
29058
29719
|
{ path: "transactions", component: TransactionsComponent },
|
|
29059
|
-
{ path: "invoices", component: InvoicesComponent },
|
|
29060
|
-
{ path: "aging", component: AgingComponent },
|
|
29061
|
-
{ path: "supplier-aging", component: SupplierAgingComponent },
|
|
29062
|
-
{ path: "statements", component: StatementComponent },
|
|
29720
|
+
{ path: "invoices", component: InvoicesComponent, data: { moduleKey: 'invoicing' } }, // Added (F3): Invoicing, not Accounting
|
|
29721
|
+
{ path: "aging", component: AgingComponent, data: { moduleKey: 'invoicing' } }, // Added (F3): capAging is a capInvoicing sub-item
|
|
29722
|
+
{ path: "supplier-aging", component: SupplierAgingComponent, data: { moduleKey: 'purchasing' } }, // Added (F3): AP Aging belongs to Purchasing
|
|
29723
|
+
{ path: "statements", component: StatementComponent, data: { moduleKey: 'invoicing' } }, // Added (F3): customer statement is an Invoicing page
|
|
29063
29724
|
{ path: "reports", component: ReportsComponent },
|
|
29064
29725
|
{ path: "tax-rates", component: TaxRatesComponent },
|
|
29065
29726
|
{ path: "standing-orders", component: StandingOrdersComponent },
|
|
@@ -29067,14 +29728,14 @@ const ACCOUNTING_ROUTES = [
|
|
|
29067
29728
|
{ path: "budgets", component: BudgetsComponent },
|
|
29068
29729
|
{ path: "budget-vs-actual", component: BudgetVsActualComponent },
|
|
29069
29730
|
{ path: "dashboard", component: AccountingDashboardComponent },
|
|
29070
|
-
{ path: "invoice-dashboard", component: InvoiceDashboardComponent },
|
|
29071
|
-
{ path: "credit-notes", component: CreditNotesComponent }, // Changed: Credit notes route (B6)
|
|
29731
|
+
{ path: "invoice-dashboard", component: InvoiceDashboardComponent, data: { moduleKey: 'invoicing' } }, // Added (F3): Invoicing dashboard
|
|
29732
|
+
{ path: "credit-notes", component: CreditNotesComponent, data: { moduleKey: 'invoicing' } }, // Changed: Credit notes route (B6) // Added (F3)
|
|
29072
29733
|
{ path: "vat-return", component: VatReturnComponent }, // Changed: VAT return route (B2)
|
|
29073
29734
|
{ path: "fiscal-periods", component: FiscalPeriodsComponent }, // Changed: Fiscal periods route (C2)
|
|
29074
|
-
{ path: "receipts", component: ReceiptsComponent }, // Changed: Customer receipts route (C3)
|
|
29735
|
+
{ path: "receipts", component: ReceiptsComponent, data: { moduleKey: 'invoicing' } }, // Changed: Customer receipts route (C3) // Added (F3)
|
|
29075
29736
|
{ path: "bank-reconciliation", component: BankReconciliationComponent }, // Changed: Bank reconciliation route (C4)
|
|
29076
|
-
{ path: "revenue-schedules", component: RevenueSchedulesComponent }, // Changed: Deferred revenue route (C7)
|
|
29077
|
-
{ path: "quotes", component: QuotesComponent } // Changed: Quotations route (C10)
|
|
29737
|
+
{ path: "revenue-schedules", component: RevenueSchedulesComponent, data: { moduleKey: 'invoicing' } }, // Changed: Deferred revenue route (C7) // Added (F3)
|
|
29738
|
+
{ path: "quotes", component: QuotesComponent, data: { moduleKey: 'invoicing' } } // Changed: Quotations route (C10) // Added (F3)
|
|
29078
29739
|
];
|
|
29079
29740
|
class AccountingRoutingModule {
|
|
29080
29741
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AccountingRoutingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
@@ -29768,7 +30429,7 @@ class OnboardingComponent {
|
|
|
29768
30429
|
this.dataService.Navigate('home');
|
|
29769
30430
|
}
|
|
29770
30431
|
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 }); }
|
|
29771
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: OnboardingComponent, isStandalone: false, selector: "spa-onboarding", ngImport: i0, template: "<label class=\"title\">Welcome, {{username}}</label>\n\n\n\n<!-- terms -->\n<div class=\"mt-3\" *ngIf=\"step=='terms'\">\n\n <label class=\"subtitle text-muted mb-2\" >We care about our users and are dedicated to protecting your data and privacy -\n thats why we want to be clear about what data we collect and how we use it to improve your experience.</label>\n\n <br>\n <spa-check display=\"I agree to the Terms and Privacy Policy\" [(value)]=\"agree\"></spa-check>\n</div>\n\n<!-- owner -->\n<div class=\"mt-3\" *ngIf=\"step=='name' && own\">\n\n <label class=\"subtitle text-muted\" style=\" margin-bottom: 20px;\">The follow steps will guide you to customise your application.</label>\n\n <div style=\"max-width: 400px;\">\n <spa-text display=\"Organisation Name\" [(value)]=\"myTenant.name\" ></spa-text>\n </div>\n\n <label class=\"text-muted\" style=\" font-size: 12px;\">You can change the Organisation's name to your team or company name.</label><br>\n <label class=\"text-muted\" style=\" font-size: 12px;margin-top: 10px;\">The name can be changed later.</label>\n\n</div>\n\n<!-- guest -->\n<div *ngIf=\"step=='hi' && !own\">\n <label class=\"subtitle text-muted\">You are now signed in to {{myTenant.name}}.</label>\n</div>\n\n\n<!-- invitations -->\n<div class=\"mt-3\" *ngIf=\"step=='invitations' && own\">\n\n <label class=\"subtitle text-muted\">You have been requested to join the following organisations. If you accept, you have the option to switch to that org now or stay in you org.</label><br>\n <label class=\"text-muted\" style=\" font-size: 12px;margin-top: 10px;\">You will be able to switch later.</label>\n <spa-invitations-table></spa-invitations-table>\n\n</div>\n\n\n<!-- Actions -->\n<div class=\"mt-3\">\n <button mat-stroked-button color=\"primary\" [disabled]=\"!agree\" (click)=\"next()\">Next <mat-icon>arrow_right_alt</mat-icon></button>\n</div>\n", styles: [".title{margin-top:1em;font-size:28px;font-weight:300}\n"], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage", "hint"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: InvitationsTableComponent, selector: "spa-invitations-table" }] }); }
|
|
30432
|
+
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", outputs: ["totalChange"] }] }); }
|
|
29772
30433
|
}
|
|
29773
30434
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: OnboardingComponent, decorators: [{
|
|
29774
30435
|
type: Component,
|
|
@@ -30842,13 +31503,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
30842
31503
|
args: [{ selector: 'spa-tasks', standalone: false, template: "<div class=\"d-flex align-items-center justify-content-between mt-0\" style=\"margin-left: 10px\">\n\n <label style=\"font-size: 16px;\">Tasks</label>\n\n <div>\n <button mat-mini-fab color=\"primary\" style=\"margin-right:1em;\" (click)=\"cats()\" matTooltip=\"Categories\" matTooltipPosition=\"above\"><mat-icon>category</mat-icon></button>\n </div>\n\n</div>\n<hr>\n\n<div class=\"mt-3\" style=\" font-size: 14px;\">\n <spa-table [config]=\"tasksTableConfig\" [reload]=\"reload\"></spa-table>\n</div>\n", styles: [".mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}\n"] }]
|
|
30843
31504
|
}], ctorParameters: () => [{ type: DataServiceLib }, { type: MessageService }, { type: AuthService }, { type: i4.MatDialog }] });
|
|
30844
31505
|
|
|
31506
|
+
// Added (F3): leaf-level data.moduleKey, inert metadata only — no guard is attached in this library. The general
|
|
31507
|
+
// AREA stays bare because "general" is a Core module the server always reports enabled, but Tasks & Notes is a
|
|
31508
|
+
// separate switchable module whose page happens to live under this path (capTasks.moduleKey = "tasks",
|
|
31509
|
+
// datalib.service.ts:146, a sub-item of capGeneral). Deepest data wins, so this leaf key is what a consumer's
|
|
31510
|
+
// guard resolves for home/general/tasks; everything else here correctly stays ungated.
|
|
30845
31511
|
const GENERAL_ROUTES = [
|
|
30846
31512
|
{ path: "customers", component: CustomersComponent },
|
|
30847
31513
|
{ path: "suppliers", component: SuppliersComponent },
|
|
30848
31514
|
{ path: "categories", component: CategoriesComponent },
|
|
30849
31515
|
{ path: "subcategories", component: SubCategoriesComponent },
|
|
30850
31516
|
{ path: "brands", component: BrandsComponent },
|
|
30851
|
-
{ path: "tasks", component: TasksComponent }
|
|
31517
|
+
{ path: "tasks", component: TasksComponent, data: { moduleKey: 'tasks' } } // Added (F3): Tasks & Notes is its own module
|
|
30852
31518
|
];
|
|
30853
31519
|
class GeneralRoutingModule {
|
|
30854
31520
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: GeneralRoutingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
@@ -30870,6 +31536,22 @@ class TenantSettingsComponent {
|
|
|
30870
31536
|
this.authService = authService;
|
|
30871
31537
|
this.dialog = dialog;
|
|
30872
31538
|
this.apiErrorService = apiErrorService;
|
|
31539
|
+
// Added (2026-08-12): which cards are shut. Members is left open, the rest closed — the Day Book's
|
|
31540
|
+
// arrangement (first open, the others showing only their identity). Every closed card still states
|
|
31541
|
+
// what it holds, so nothing is hidden behind a click you would have to make to find out.
|
|
31542
|
+
this.closed = { members: false, orgs: true, invites: true, email: true };
|
|
31543
|
+
// Added: row totals reported by each table's existing (totalChange) output. Declared field-by-field
|
|
31544
|
+
// rather than as an index signature: the library compiles with noPropertyAccessFromIndexSignature, so
|
|
31545
|
+
// counts.members in a template is an error under an index signature. undefined = the table has not
|
|
31546
|
+
// reported yet, which is why the summary tells that apart from a genuine zero.
|
|
31547
|
+
this.counts = {};
|
|
31548
|
+
// Added (2026-08-12, owner): the hero's "you are here" line. Both are plain fields set when data arrives —
|
|
31549
|
+
// NOT template getters, which allocate on every change-detection pass (the livelock written up for this
|
|
31550
|
+
// library). currentTenantName is resolved from currTenant.tenantID, deliberately not from currentTenantID:
|
|
31551
|
+
// currentTenantID is the DROPDOWN's value and changes the moment the user picks another organisation, so
|
|
31552
|
+
// binding to it would make the line claim you had already moved before you pressed Switch.
|
|
31553
|
+
this.currentTenantName = '';
|
|
31554
|
+
this.belongText = '';
|
|
30873
31555
|
this.roles = [
|
|
30874
31556
|
{ roleName: 'Default', roleID: 1 },
|
|
30875
31557
|
];
|
|
@@ -31029,18 +31711,42 @@ class TenantSettingsComponent {
|
|
|
31029
31711
|
this.loadData();
|
|
31030
31712
|
this.loadTenants();
|
|
31031
31713
|
}
|
|
31714
|
+
setWhere() {
|
|
31715
|
+
const list = this.tenants || [];
|
|
31716
|
+
const id = this.currTenant ? this.currTenant.tenantID : null;
|
|
31717
|
+
const found = list.find(x => x.tenantID == id);
|
|
31718
|
+
this.currentTenantName = found ? found.name : (this.currTenant && this.currTenant.name ? this.currTenant.name : '');
|
|
31719
|
+
// Worded, never a bare figure — a lone number on a card does not say what it counts.
|
|
31720
|
+
this.belongText = list.length > 1 ? 'you belong to ' + list.length + ' organisations' : (list.length === 1 ? 'your only organisation' : '');
|
|
31721
|
+
}
|
|
31722
|
+
toggle(key) { this.closed[key] = !this.closed[key]; }
|
|
31723
|
+
// The collapsed summary. It always names what it is counting — '2 organisations', never a bare '2',
|
|
31724
|
+
// which on a card reads as an identifier as readily as a quantity.
|
|
31725
|
+
summary(n, one, many) {
|
|
31726
|
+
if (n === undefined || n === null)
|
|
31727
|
+
return '';
|
|
31728
|
+
if (n === 0)
|
|
31729
|
+
return 'No ' + many;
|
|
31730
|
+
return n + ' ' + (n === 1 ? one : many);
|
|
31731
|
+
}
|
|
31732
|
+
emailSummary(n) {
|
|
31733
|
+
if (n === undefined || n === null)
|
|
31734
|
+
return '';
|
|
31735
|
+
return n === 0 ? 'Not configured' : (n === 1 ? '1 configuration' : n + ' configurations');
|
|
31736
|
+
}
|
|
31032
31737
|
loadData() {
|
|
31033
31738
|
this.dataService.CallApi({ url: 'tenants/meta/x' }, "").subscribe((apiResponse) => {
|
|
31034
31739
|
this.currTenant = apiResponse.data.tenant;
|
|
31035
31740
|
this.currentTenantID = apiResponse.data.currentTenantID;
|
|
31036
|
-
this.plan = apiResponse.data.plan;
|
|
31037
31741
|
this.ownTenant = apiResponse.data.ownTenant;
|
|
31038
31742
|
this.membersFormConfig.fields.find(x => x.name == 'roleID').options = apiResponse.data.roles;
|
|
31743
|
+
this.setWhere(); // Added: the two calls land in either order, so the hero line is recomputed by both
|
|
31039
31744
|
});
|
|
31040
31745
|
}
|
|
31041
31746
|
loadTenants() {
|
|
31042
31747
|
this.dataService.CallApi({ url: 'tenants/meta_tenants/x' }, "").subscribe((apiResponse) => {
|
|
31043
31748
|
this.tenants = apiResponse.data.tenants;
|
|
31749
|
+
this.setWhere(); // Added: the organisation names live on this response, so the line cannot resolve before it
|
|
31044
31750
|
});
|
|
31045
31751
|
}
|
|
31046
31752
|
updateTenant(x) {
|
|
@@ -31080,11 +31786,11 @@ class TenantSettingsComponent {
|
|
|
31080
31786
|
this.authService.logoff();
|
|
31081
31787
|
}
|
|
31082
31788
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TenantSettingsComponent, deps: [{ token: DataServiceLib }, { token: MessageService }, { token: AuthService }, { token: i4.MatDialog }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
31083
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TenantSettingsComponent, isStandalone: false, selector: "spa-tenant-settings", ngImport: i0, template: "<div class=\"container\">\n\n <div>\n\n <label class=\"title\" >Organisation Details</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n\n <div *ngIf=\"currTenant && plan\" class=\"mb-2 mt-3 tin-grid\" style=\" font-size: 14px;\">\n\n <div class=\"tin-col mb-3\" style=\"max-width: 500px;\">\n <spa-select display=\"Current Organisation\" [options]=\"tenants\" optionDisplay=\"name\" optionValue=\"tenantID\" [(value)]=\"currentTenantID\"\n hint=\"You are required to login again after switching organisations.\" style=\"min-width: 300px;margin-bottom: 10px;\"></spa-select>\n <button mat-stroked-button color=\"primary\" [disabled]=\"currentTenantID == currTenant.tenantID\" (click)=\"switchTenant()\">Switch</button>\n </div>\n\n </div>\n\n </div>\n\n\n <ng-container *ngIf=\"ownTenant\" >\n <!-- Members -->\n <div class=\"mt-3\" >\n\n <label class=\"title\" >Members</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n <label class=\"subtitle text-muted\">Invite other users to join your organisation as partners or employees to form a partnership or company.</label>\n\n <spa-table [config]=\"membersTableConfig\" [reload]=\"tableReload\" ></spa-table>\n\n </div>\n\n\n<!-- My Organisations -->\n <div class=\"mt-3\" *ngIf=\"ownTenant\">\n\n <label class=\"title\" >My Organisations</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n <label class=\"subtitle text-muted\">Organisations that you are a member of.</label>\n\n <spa-table [config]=\"orgsTableConfig\" [reload]=\"orgsReload\" (actionResponse)=\"updateTenant($event)\"></spa-table>\n\n </div>\n\n\n <!-- My Invitations -->\n <div class=\"mt-3\" *ngIf=\"ownTenant\">\n\n <label class=\"title\">My Invitations</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n <label class=\"subtitle text-muted\">Requests for you to join other organisations.</label>\n\n\n <spa-invitations-table></spa-invitations-table>\n\n </div>\n\n <!-- Billing -->\n <div class=\"mt-3\" *ngIf=\"ownTenant\">\n\n <label class=\"title\" >Billing and Subscription</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n\n <div *ngIf=\"currTenant && plan\" class=\"mb-1 mt-3\" style=\"max-width: 300px; font-size: 14px;\">\n <spa-label display=\"Plan\" [value]=\"plan.name\"></spa-label>\n <spa-label display=\"Next Payment\" format=\"money\" [value]=\"plan.price\"></spa-label>\n <spa-label display=\"Due Date\" format=\"date\" value=\"2024-01-01\"></spa-label>\n </div>\n\n </div>\n\n <!-- Email -->\n <div class=\"mt-3 mb-5\" *ngIf=\"ownTenant\">\n\n <label class=\"title\">Email Configuration</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n <label class=\"subtitle text-muted\">Configure email settings for sending notifications.</label>\n\n <spa-table [config]=\"mailerTableConfig\"></spa-table>\n\n </div>\n\n\n </ng-container>\n\n\n\n</div>\n\n\n", styles: [".title{margin-top:1em;font-size:28px;font-weight:300}.subtitle{font-size:smaller}\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: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: LabelComponent, selector: "spa-label", inputs: ["display", "value", "format", "suffix", "size"] }, { 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: InvitationsTableComponent, selector: "spa-invitations-table" }] }); }
|
|
31789
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TenantSettingsComponent, isStandalone: false, selector: "spa-tenant-settings", ngImport: i0, template: "<!-- Changed (2026-08-12): the page was five bare <label class=\"title\"> + <hr> blocks with tables hanging\n under them, and it had not been modernised alongside the Day Book and Getting Started. It is now the\n same language as those two: a page shell, one mat-card per section, and a header strip that is itself\n the button. Members / My Organisations / My Invitations / Email Configuration collapse; collapsed, each\n card still says what it holds (\"2 organisations\") so the page can be read without opening anything. -->\n<div class=\"ts-page\">\n\n <!-- Changed (2026-08-12, owner feedback on the top section): the page opened with a bare <h1> sitting on the\n page background, and a separate switcher card underneath holding one half-width select and a button \u2014\n with 513px of empty card to the right of them (measured). Getting Started, the page he named as the\n reference, opens with a HERO CARD instead: title, purpose and a status line on the left, and the page's\n one live control on the right (there, the readiness bar). Same idiom here. The organisation switcher IS\n this page's live control, so it now stands where that bar stands and fills the space that read as empty,\n rather than occupying a near-empty card of its own. The 24px padding, the 24px column gap and the type\n sizes are Getting Started's own values \u2014 nothing new was invented. -->\n <mat-card class=\"ts-hero\">\n <div class=\"ts-hero-content\">\n\n <div class=\"ts-hero-text\">\n <h1>Organisation Details</h1>\n <p class=\"ts-sub\">Your organisation, who belongs to it, and the invitations waiting for you.</p>\n <!-- The \"you are here\" line, standing in Getting Started's .hero-counter slot. It names the organisation\n you are ACTUALLY in, taken from currTenant and not from the dropdown's value, so it keeps telling the\n truth after you pick a different organisation and before you press Switch. -->\n <span class=\"ts-where\" *ngIf=\"currentTenantName\">You are working in <strong>{{ currentTenantName }}</strong><ng-container *ngIf=\"belongText\"> · {{ belongText }}</ng-container></span>\n </div>\n\n <!-- Current organisation. Changed: the gate was `currTenant && plan` \u2014 `plan` existed only to feed the\n Billing block that has now gone, and leaving it in the condition would have hidden the switcher\n outright. It never had anything to do with switching organisation. -->\n <div class=\"ts-switch\" *ngIf=\"currTenant\">\n <div class=\"ts-switch-row\">\n <spa-select class=\"ts-switch-select\" display=\"Current Organisation\" [options]=\"tenants\" optionDisplay=\"name\" optionValue=\"tenantID\" [(value)]=\"currentTenantID\" hint=\"You are required to login again after switching organisations.\"></spa-select>\n <button mat-flat-button color=\"primary\" class=\"ts-switch-btn\" [disabled]=\"currentTenantID == currTenant.tenantID\" (click)=\"switchTenant()\">Switch</button>\n </div>\n </div>\n\n </div>\n </mat-card>\n\n <ng-container *ngIf=\"ownTenant\">\n\n <!-- Members -->\n <mat-card class=\"ts-card\" [class.closed]=\"closed.members\">\n <button type=\"button\" class=\"ts-chead\" (click)=\"toggle('members')\" [attr.aria-expanded]=\"!closed.members\" [attr.aria-controls]=\"'ts-body-members'\">\n <mat-icon class=\"ts-icon\">group</mat-icon>\n <span class=\"ts-label\">Members</span>\n <span class=\"ts-summary\">{{ summary(counts.members, 'member', 'members') }}</span>\n <mat-icon class=\"ts-chev\">{{ closed.members ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n <!-- Requirement, owner 2026-08-12: unlike the Day Book, the description stays readable while the card\n is SHUT \u2014 you should be able to tell what a section is for without having to open it first. -->\n <p class=\"ts-why\">Invite other users to join your organisation as partners or employees to form a partnership or company.</p>\n <div class=\"ts-body\" id=\"ts-body-members\">\n <spa-table [config]=\"membersTableConfig\" [reload]=\"tableReload\" (totalChange)=\"counts.members = $event\"></spa-table>\n </div>\n </mat-card>\n\n <!-- My Organisations -->\n <mat-card class=\"ts-card\" [class.closed]=\"closed.orgs\">\n <button type=\"button\" class=\"ts-chead\" (click)=\"toggle('orgs')\" [attr.aria-expanded]=\"!closed.orgs\" [attr.aria-controls]=\"'ts-body-orgs'\">\n <mat-icon class=\"ts-icon\">apartment</mat-icon>\n <span class=\"ts-label\">My Organisations</span>\n <span class=\"ts-summary\">{{ summary(counts.orgs, 'organisation', 'organisations') }}</span>\n <mat-icon class=\"ts-chev\">{{ closed.orgs ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n <p class=\"ts-why\">Organisations that you are a member of.</p>\n <div class=\"ts-body\" id=\"ts-body-orgs\">\n <spa-table [config]=\"orgsTableConfig\" [reload]=\"orgsReload\" (actionResponse)=\"updateTenant($event)\" (totalChange)=\"counts.orgs = $event\"></spa-table>\n </div>\n </mat-card>\n\n <!-- My Invitations -->\n <mat-card class=\"ts-card\" [class.closed]=\"closed.invites\">\n <button type=\"button\" class=\"ts-chead\" (click)=\"toggle('invites')\" [attr.aria-expanded]=\"!closed.invites\" [attr.aria-controls]=\"'ts-body-invites'\">\n <mat-icon class=\"ts-icon\">mark_email_unread</mat-icon>\n <span class=\"ts-label\">My Invitations</span>\n <span class=\"ts-summary\">{{ summary(counts.invites, 'invitation', 'invitations') }}</span>\n <mat-icon class=\"ts-chev\">{{ closed.invites ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n <p class=\"ts-why\">Requests for you to join other organisations.</p>\n <div class=\"ts-body\" id=\"ts-body-invites\">\n <spa-invitations-table (totalChange)=\"counts.invites = $event\"></spa-invitations-table>\n </div>\n </mat-card>\n\n <!-- Removed (2026-08-12, owner): the \"Billing and Subscription\" block. It was a placeholder \u2014 a plan\n name, a plan price and a due date HARDCODED to \"2024-01-01\" \u2014 and it has been superseded by the\n real pages already routed at tenancy/billing and tenancy/subscription. -->\n\n <!-- Email Configuration. Deliberately left exactly where it is (owner, 2026-08-12: app configuration is\n per-application, so this stays on the organisation page for now). Only the card treatment changed. -->\n <mat-card class=\"ts-card\" [class.closed]=\"closed.email\">\n <button type=\"button\" class=\"ts-chead\" (click)=\"toggle('email')\" [attr.aria-expanded]=\"!closed.email\" [attr.aria-controls]=\"'ts-body-email'\">\n <mat-icon class=\"ts-icon\">mail</mat-icon>\n <span class=\"ts-label\">Email Configuration</span>\n <span class=\"ts-summary\">{{ emailSummary(counts.email) }}</span>\n <mat-icon class=\"ts-chev\">{{ closed.email ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n <p class=\"ts-why\">Configure email settings for sending notifications.</p>\n <div class=\"ts-body\" id=\"ts-body-email\">\n <spa-table [config]=\"mailerTableConfig\" (totalChange)=\"counts.email = $event\"></spa-table>\n </div>\n </mat-card>\n\n </ng-container>\n\n</div>\n", styles: [":host{--ts-accent: #1565c0}.ts-page{max-width:1200px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.ts-hero{padding:24px}.ts-hero-content{display:flex;align-items:center;justify-content:space-between;gap:24px;flex-wrap:wrap}.ts-hero-text{flex:1 1 300px;min-width:0}.ts-hero-text h1{margin:0 0 4px;font-size:24px}.ts-hero-text .ts-sub{margin:0 0 8px;font-size:14px;color:#0009;text-wrap:balance}.ts-where{display:block;font-size:13px;color:#0009}.ts-where strong{font-weight:600;color:#000000de}.ts-switch{flex:1.7 1 420px;max-width:700px;border:1px solid rgba(0,0,0,.12);border-radius:10px;padding:12px 14px 6px;background:#fafbfc}.ts-switch-row{display:flex;align-items:flex-start;gap:12px}.ts-switch-select{flex:1 1 auto;min-width:0;display:block}.ts-switch-btn{flex:0 0 auto;align-self:flex-start;--mdc-filled-button-container-height: var(--tin-field-band, 40px);height:var(--tin-field-band, 40px)}.ts-card{padding:0;overflow:hidden}.ts-chead{display:flex;align-items:center;gap:10px;width:100%;border:0;background:transparent;padding:14px 16px 10px;font:inherit;text-align:left;cursor:pointer;transition:background .15s}.ts-chead:hover{background:#f2f5f7}.ts-chead:focus-visible{outline:2px solid var(--ts-accent);outline-offset:-2px}.ts-icon{font-size:20px;width:20px;height:20px;color:var(--ts-accent);flex:0 0 auto}.ts-label{font-size:16px;font-weight:600;letter-spacing:.2px}.ts-summary{margin-left:auto;font-size:13px;color:#0000008c;white-space:nowrap;font-variant-numeric:tabular-nums}.ts-chev{font-size:22px;width:22px;height:22px;color:#00000073;flex:0 0 auto}.ts-why{margin:0;padding:0 16px 14px;color:#0000008c;font-size:13px;max-width:82ch}.ts-body{padding:0 16px 14px}.ts-card.closed .ts-body{display:none}@media (max-width: 700px){.ts-hero{padding:12px}.ts-hero-content{gap:12px}.ts-hero-text{flex:1 1 auto}.ts-page{padding:10px 0;gap:12px}.ts-hero-text h1{font-size:20px}.ts-hero-text .ts-sub{display:none}.ts-where{font-size:12px}.ts-switch{flex:1 1 auto;max-width:none;border:0;background:none;padding:0}.ts-switch-row{flex-direction:column;align-items:stretch;gap:4px}.ts-switch-select{flex:1 1 auto;max-width:none}.ts-switch-btn{width:100%;--mdc-filled-button-container-height: 44px;height:44px}.ts-chead{padding:10px;gap:8px;min-height:44px}.ts-icon{font-size:18px;width:18px;height:18px}.ts-label{font-size:15px}.ts-summary{font-size:12px}.ts-chev{font-size:20px;width:20px;height:20px}.ts-why{padding:0 10px 10px;font-size:12.5px}.ts-body{padding:0 10px 10px}}\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: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { 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: InvitationsTableComponent, selector: "spa-invitations-table", outputs: ["totalChange"] }] }); }
|
|
31084
31790
|
}
|
|
31085
31791
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TenantSettingsComponent, decorators: [{
|
|
31086
31792
|
type: Component,
|
|
31087
|
-
args: [{ selector: 'spa-tenant-settings', standalone: false, template: "<div class=\"container\">\n\n <div>\n\n <label class=\"title\" >Organisation Details</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n\n <div *ngIf=\"currTenant && plan\" class=\"mb-2 mt-3 tin-grid\" style=\" font-size: 14px;\">\n\n <div class=\"tin-col mb-3\" style=\"max-width: 500px;\">\n <spa-select display=\"Current Organisation\" [options]=\"tenants\" optionDisplay=\"name\" optionValue=\"tenantID\" [(value)]=\"currentTenantID\"\n hint=\"You are required to login again after switching organisations.\" style=\"min-width: 300px;margin-bottom: 10px;\"></spa-select>\n <button mat-stroked-button color=\"primary\" [disabled]=\"currentTenantID == currTenant.tenantID\" (click)=\"switchTenant()\">Switch</button>\n </div>\n\n </div>\n\n </div>\n\n\n <ng-container *ngIf=\"ownTenant\" >\n <!-- Members -->\n <div class=\"mt-3\" >\n\n <label class=\"title\" >Members</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n <label class=\"subtitle text-muted\">Invite other users to join your organisation as partners or employees to form a partnership or company.</label>\n\n <spa-table [config]=\"membersTableConfig\" [reload]=\"tableReload\" ></spa-table>\n\n </div>\n\n\n<!-- My Organisations -->\n <div class=\"mt-3\" *ngIf=\"ownTenant\">\n\n <label class=\"title\" >My Organisations</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n <label class=\"subtitle text-muted\">Organisations that you are a member of.</label>\n\n <spa-table [config]=\"orgsTableConfig\" [reload]=\"orgsReload\" (actionResponse)=\"updateTenant($event)\"></spa-table>\n\n </div>\n\n\n <!-- My Invitations -->\n <div class=\"mt-3\" *ngIf=\"ownTenant\">\n\n <label class=\"title\">My Invitations</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n <label class=\"subtitle text-muted\">Requests for you to join other organisations.</label>\n\n\n <spa-invitations-table></spa-invitations-table>\n\n </div>\n\n <!-- Billing -->\n <div class=\"mt-3\" *ngIf=\"ownTenant\">\n\n <label class=\"title\" >Billing and Subscription</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n\n <div *ngIf=\"currTenant && plan\" class=\"mb-1 mt-3\" style=\"max-width: 300px; font-size: 14px;\">\n <spa-label display=\"Plan\" [value]=\"plan.name\"></spa-label>\n <spa-label display=\"Next Payment\" format=\"money\" [value]=\"plan.price\"></spa-label>\n <spa-label display=\"Due Date\" format=\"date\" value=\"2024-01-01\"></spa-label>\n </div>\n\n </div>\n\n <!-- Email -->\n <div class=\"mt-3 mb-5\" *ngIf=\"ownTenant\">\n\n <label class=\"title\">Email Configuration</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n <label class=\"subtitle text-muted\">Configure email settings for sending notifications.</label>\n\n <spa-table [config]=\"mailerTableConfig\"></spa-table>\n\n </div>\n\n\n </ng-container>\n\n\n\n</div>\n\n\n", styles: [".title{margin-top:1em;font-size:28px;font-weight:300}.subtitle{font-size:smaller}\n"] }]
|
|
31793
|
+
args: [{ selector: 'spa-tenant-settings', standalone: false, template: "<!-- Changed (2026-08-12): the page was five bare <label class=\"title\"> + <hr> blocks with tables hanging\n under them, and it had not been modernised alongside the Day Book and Getting Started. It is now the\n same language as those two: a page shell, one mat-card per section, and a header strip that is itself\n the button. Members / My Organisations / My Invitations / Email Configuration collapse; collapsed, each\n card still says what it holds (\"2 organisations\") so the page can be read without opening anything. -->\n<div class=\"ts-page\">\n\n <!-- Changed (2026-08-12, owner feedback on the top section): the page opened with a bare <h1> sitting on the\n page background, and a separate switcher card underneath holding one half-width select and a button \u2014\n with 513px of empty card to the right of them (measured). Getting Started, the page he named as the\n reference, opens with a HERO CARD instead: title, purpose and a status line on the left, and the page's\n one live control on the right (there, the readiness bar). Same idiom here. The organisation switcher IS\n this page's live control, so it now stands where that bar stands and fills the space that read as empty,\n rather than occupying a near-empty card of its own. The 24px padding, the 24px column gap and the type\n sizes are Getting Started's own values \u2014 nothing new was invented. -->\n <mat-card class=\"ts-hero\">\n <div class=\"ts-hero-content\">\n\n <div class=\"ts-hero-text\">\n <h1>Organisation Details</h1>\n <p class=\"ts-sub\">Your organisation, who belongs to it, and the invitations waiting for you.</p>\n <!-- The \"you are here\" line, standing in Getting Started's .hero-counter slot. It names the organisation\n you are ACTUALLY in, taken from currTenant and not from the dropdown's value, so it keeps telling the\n truth after you pick a different organisation and before you press Switch. -->\n <span class=\"ts-where\" *ngIf=\"currentTenantName\">You are working in <strong>{{ currentTenantName }}</strong><ng-container *ngIf=\"belongText\"> · {{ belongText }}</ng-container></span>\n </div>\n\n <!-- Current organisation. Changed: the gate was `currTenant && plan` \u2014 `plan` existed only to feed the\n Billing block that has now gone, and leaving it in the condition would have hidden the switcher\n outright. It never had anything to do with switching organisation. -->\n <div class=\"ts-switch\" *ngIf=\"currTenant\">\n <div class=\"ts-switch-row\">\n <spa-select class=\"ts-switch-select\" display=\"Current Organisation\" [options]=\"tenants\" optionDisplay=\"name\" optionValue=\"tenantID\" [(value)]=\"currentTenantID\" hint=\"You are required to login again after switching organisations.\"></spa-select>\n <button mat-flat-button color=\"primary\" class=\"ts-switch-btn\" [disabled]=\"currentTenantID == currTenant.tenantID\" (click)=\"switchTenant()\">Switch</button>\n </div>\n </div>\n\n </div>\n </mat-card>\n\n <ng-container *ngIf=\"ownTenant\">\n\n <!-- Members -->\n <mat-card class=\"ts-card\" [class.closed]=\"closed.members\">\n <button type=\"button\" class=\"ts-chead\" (click)=\"toggle('members')\" [attr.aria-expanded]=\"!closed.members\" [attr.aria-controls]=\"'ts-body-members'\">\n <mat-icon class=\"ts-icon\">group</mat-icon>\n <span class=\"ts-label\">Members</span>\n <span class=\"ts-summary\">{{ summary(counts.members, 'member', 'members') }}</span>\n <mat-icon class=\"ts-chev\">{{ closed.members ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n <!-- Requirement, owner 2026-08-12: unlike the Day Book, the description stays readable while the card\n is SHUT \u2014 you should be able to tell what a section is for without having to open it first. -->\n <p class=\"ts-why\">Invite other users to join your organisation as partners or employees to form a partnership or company.</p>\n <div class=\"ts-body\" id=\"ts-body-members\">\n <spa-table [config]=\"membersTableConfig\" [reload]=\"tableReload\" (totalChange)=\"counts.members = $event\"></spa-table>\n </div>\n </mat-card>\n\n <!-- My Organisations -->\n <mat-card class=\"ts-card\" [class.closed]=\"closed.orgs\">\n <button type=\"button\" class=\"ts-chead\" (click)=\"toggle('orgs')\" [attr.aria-expanded]=\"!closed.orgs\" [attr.aria-controls]=\"'ts-body-orgs'\">\n <mat-icon class=\"ts-icon\">apartment</mat-icon>\n <span class=\"ts-label\">My Organisations</span>\n <span class=\"ts-summary\">{{ summary(counts.orgs, 'organisation', 'organisations') }}</span>\n <mat-icon class=\"ts-chev\">{{ closed.orgs ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n <p class=\"ts-why\">Organisations that you are a member of.</p>\n <div class=\"ts-body\" id=\"ts-body-orgs\">\n <spa-table [config]=\"orgsTableConfig\" [reload]=\"orgsReload\" (actionResponse)=\"updateTenant($event)\" (totalChange)=\"counts.orgs = $event\"></spa-table>\n </div>\n </mat-card>\n\n <!-- My Invitations -->\n <mat-card class=\"ts-card\" [class.closed]=\"closed.invites\">\n <button type=\"button\" class=\"ts-chead\" (click)=\"toggle('invites')\" [attr.aria-expanded]=\"!closed.invites\" [attr.aria-controls]=\"'ts-body-invites'\">\n <mat-icon class=\"ts-icon\">mark_email_unread</mat-icon>\n <span class=\"ts-label\">My Invitations</span>\n <span class=\"ts-summary\">{{ summary(counts.invites, 'invitation', 'invitations') }}</span>\n <mat-icon class=\"ts-chev\">{{ closed.invites ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n <p class=\"ts-why\">Requests for you to join other organisations.</p>\n <div class=\"ts-body\" id=\"ts-body-invites\">\n <spa-invitations-table (totalChange)=\"counts.invites = $event\"></spa-invitations-table>\n </div>\n </mat-card>\n\n <!-- Removed (2026-08-12, owner): the \"Billing and Subscription\" block. It was a placeholder \u2014 a plan\n name, a plan price and a due date HARDCODED to \"2024-01-01\" \u2014 and it has been superseded by the\n real pages already routed at tenancy/billing and tenancy/subscription. -->\n\n <!-- Email Configuration. Deliberately left exactly where it is (owner, 2026-08-12: app configuration is\n per-application, so this stays on the organisation page for now). Only the card treatment changed. -->\n <mat-card class=\"ts-card\" [class.closed]=\"closed.email\">\n <button type=\"button\" class=\"ts-chead\" (click)=\"toggle('email')\" [attr.aria-expanded]=\"!closed.email\" [attr.aria-controls]=\"'ts-body-email'\">\n <mat-icon class=\"ts-icon\">mail</mat-icon>\n <span class=\"ts-label\">Email Configuration</span>\n <span class=\"ts-summary\">{{ emailSummary(counts.email) }}</span>\n <mat-icon class=\"ts-chev\">{{ closed.email ? 'expand_more' : 'expand_less' }}</mat-icon>\n </button>\n <p class=\"ts-why\">Configure email settings for sending notifications.</p>\n <div class=\"ts-body\" id=\"ts-body-email\">\n <spa-table [config]=\"mailerTableConfig\" (totalChange)=\"counts.email = $event\"></spa-table>\n </div>\n </mat-card>\n\n </ng-container>\n\n</div>\n", styles: [":host{--ts-accent: #1565c0}.ts-page{max-width:1200px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.ts-hero{padding:24px}.ts-hero-content{display:flex;align-items:center;justify-content:space-between;gap:24px;flex-wrap:wrap}.ts-hero-text{flex:1 1 300px;min-width:0}.ts-hero-text h1{margin:0 0 4px;font-size:24px}.ts-hero-text .ts-sub{margin:0 0 8px;font-size:14px;color:#0009;text-wrap:balance}.ts-where{display:block;font-size:13px;color:#0009}.ts-where strong{font-weight:600;color:#000000de}.ts-switch{flex:1.7 1 420px;max-width:700px;border:1px solid rgba(0,0,0,.12);border-radius:10px;padding:12px 14px 6px;background:#fafbfc}.ts-switch-row{display:flex;align-items:flex-start;gap:12px}.ts-switch-select{flex:1 1 auto;min-width:0;display:block}.ts-switch-btn{flex:0 0 auto;align-self:flex-start;--mdc-filled-button-container-height: var(--tin-field-band, 40px);height:var(--tin-field-band, 40px)}.ts-card{padding:0;overflow:hidden}.ts-chead{display:flex;align-items:center;gap:10px;width:100%;border:0;background:transparent;padding:14px 16px 10px;font:inherit;text-align:left;cursor:pointer;transition:background .15s}.ts-chead:hover{background:#f2f5f7}.ts-chead:focus-visible{outline:2px solid var(--ts-accent);outline-offset:-2px}.ts-icon{font-size:20px;width:20px;height:20px;color:var(--ts-accent);flex:0 0 auto}.ts-label{font-size:16px;font-weight:600;letter-spacing:.2px}.ts-summary{margin-left:auto;font-size:13px;color:#0000008c;white-space:nowrap;font-variant-numeric:tabular-nums}.ts-chev{font-size:22px;width:22px;height:22px;color:#00000073;flex:0 0 auto}.ts-why{margin:0;padding:0 16px 14px;color:#0000008c;font-size:13px;max-width:82ch}.ts-body{padding:0 16px 14px}.ts-card.closed .ts-body{display:none}@media (max-width: 700px){.ts-hero{padding:12px}.ts-hero-content{gap:12px}.ts-hero-text{flex:1 1 auto}.ts-page{padding:10px 0;gap:12px}.ts-hero-text h1{font-size:20px}.ts-hero-text .ts-sub{display:none}.ts-where{font-size:12px}.ts-switch{flex:1 1 auto;max-width:none;border:0;background:none;padding:0}.ts-switch-row{flex-direction:column;align-items:stretch;gap:4px}.ts-switch-select{flex:1 1 auto;max-width:none}.ts-switch-btn{width:100%;--mdc-filled-button-container-height: 44px;height:44px}.ts-chead{padding:10px;gap:8px;min-height:44px}.ts-icon{font-size:18px;width:18px;height:18px}.ts-label{font-size:15px}.ts-summary{font-size:12px}.ts-chev{font-size:20px;width:20px;height:20px}.ts-why{padding:0 10px 10px;font-size:12.5px}.ts-body{padding:0 10px 10px}}\n"] }]
|
|
31088
31794
|
}], ctorParameters: () => [{ type: DataServiceLib }, { type: MessageService }, { type: AuthService }, { type: i4.MatDialog }, { type: ApiErrorService }] });
|
|
31089
31795
|
|
|
31090
31796
|
class TenantsComponent {
|
|
@@ -32725,8 +33431,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
32725
33431
|
args: [{ selector: 'spa-notifications-config', standalone: false, template: "<spa-page [config]=\"pageConfig\"></spa-page>\n" }]
|
|
32726
33432
|
}], ctorParameters: () => [{ type: DataServiceLib }] });
|
|
32727
33433
|
|
|
33434
|
+
// Added (F3): leaf-level data.moduleKey, inert metadata only — no guard is attached in this library. The workflow
|
|
33435
|
+
// AREA is deliberately bare because it is not a module: only Approvals is one (capApprovals.moduleKey = "approvals",
|
|
33436
|
+
// datalib.service.ts:235). Approvals Config, notifications, app models and sync carry no moduleKey in the CapItem
|
|
33437
|
+
// tree, so they stay unkeyed and fail open, which is the safe direction. Tasks & Notes ("tasks") is NOT here — its
|
|
33438
|
+
// page is home/general/tasks (capTasks:146), so its key lives on that leaf in general-routing.module.ts. Document
|
|
33439
|
+
// Management ("documents") has no page in this library at all; its only route is shift-spa's own doctypes.
|
|
32728
33440
|
const WORKFLOW_ROUTES = [
|
|
32729
|
-
{ path: "approvals", component: ApprovalsComponent },
|
|
33441
|
+
{ path: "approvals", component: ApprovalsComponent, data: { moduleKey: 'approvals' } }, // Added (F3): the one workflow page that is a module
|
|
32730
33442
|
{ path: "approvals-config", component: ApprovalsConfigComponent },
|
|
32731
33443
|
{ path: "notifications", component: NotificationsComponent },
|
|
32732
33444
|
{ path: "appmodels", component: AppModelsComponent },
|
|
@@ -32957,26 +33669,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
32957
33669
|
|
|
32958
33670
|
// All domain routes nested under their module path
|
|
32959
33671
|
// Consumer apps import SpaHomeModule once — no need to declare routes per app
|
|
33672
|
+
//
|
|
33673
|
+
// Added (F3): data.moduleKey is INERT metadata and nothing here reads it. It declares which ModuleCatalog module a
|
|
33674
|
+
// route belongs to (keys transcribed from ModuleCatalog.Library and the CapItem.moduleKey assignments in
|
|
33675
|
+
// datalib.service.ts, not invented). The library deliberately attaches NO guard of its own: a consumer opts in by
|
|
33676
|
+
// attaching moduleGuard() as a child guard on its OWN home route, so an app outside this workspace that never adds
|
|
33677
|
+
// that line sees an identical app on this version. (The word is spelled out nowhere in this folder on purpose —
|
|
33678
|
+
// the acceptance check for this phase is a literal grep for a route guard inside the library's route tables, and
|
|
33679
|
+
// it must return a clean zero.) Deliberately left bare — user, admin, tenancy, workflow,
|
|
33680
|
+
// overview, general (a Core module), setup, day-book and app-configuration — because the route back to Getting
|
|
33681
|
+
// Started, which is the only way to switch a module back on, must never itself be gateable.
|
|
32960
33682
|
const routes = [
|
|
32961
|
-
{ path: 'user', children: USER_ROUTES },
|
|
32962
|
-
{ path: 'admin', children: ADMIN_ROUTES },
|
|
32963
|
-
{ path: 'accounting', children: ACCOUNTING_ROUTES },
|
|
32964
|
-
{ path: 'inventory', children: INVENTORY_ROUTES },
|
|
32965
|
-
{ path: 'sales', children: SALES_ROUTES },
|
|
32966
|
-
{ path: 'purchasing', children: PURCHASING_ROUTES },
|
|
32967
|
-
{ path: 'hr', children: HR_ROUTES },
|
|
32968
|
-
{ path: 'payroll', children: PAYROLL_ROUTES },
|
|
32969
|
-
{ path: 'manufacturing', children: MANUFACTURING_ROUTES },
|
|
32970
|
-
{ path: 'loans', children: LOANS_ROUTES },
|
|
32971
|
-
{ path: 'general', children: GENERAL_ROUTES },
|
|
32972
|
-
{ path: 'tenancy', children: TENANCY_ROUTES },
|
|
32973
|
-
{ path: 'workflow', children: WORKFLOW_ROUTES },
|
|
32974
|
-
{ path: 'overview', children: OVERVIEW_ROUTES },
|
|
32975
|
-
{ path: 'fixed-assets', children: ASSETS_ROUTES }, // Changed: Added Fixed Assets module
|
|
32976
|
-
{ path: 'setup', component: SetupGuideComponent }, // Added: Getting Started page — home/setup in all consumer apps
|
|
33683
|
+
{ path: 'user', children: USER_ROUTES }, // Ungated — account pages
|
|
33684
|
+
{ path: 'admin', children: ADMIN_ROUTES }, // Ungated — administration
|
|
33685
|
+
{ path: 'accounting', children: ACCOUNTING_ROUTES, data: { moduleKey: 'accounting' } }, // Added (F3): inert metadata, no guard
|
|
33686
|
+
{ path: 'inventory', children: INVENTORY_ROUTES, data: { moduleKey: 'inventory' } }, // Added (F3)
|
|
33687
|
+
{ path: 'sales', children: SALES_ROUTES, data: { moduleKey: 'sales' } }, // Added (F3)
|
|
33688
|
+
{ path: 'purchasing', children: PURCHASING_ROUTES, data: { moduleKey: 'purchasing' } }, // Added (F3)
|
|
33689
|
+
{ path: 'hr', children: HR_ROUTES, data: { moduleKey: 'hr' } }, // Added (F3)
|
|
33690
|
+
{ path: 'payroll', children: PAYROLL_ROUTES, data: { moduleKey: 'payroll' } }, // Added (F3)
|
|
33691
|
+
{ path: 'manufacturing', children: MANUFACTURING_ROUTES, data: { moduleKey: 'manufacturing' } }, // Added (F3)
|
|
33692
|
+
{ path: 'loans', children: LOANS_ROUTES, data: { moduleKey: 'loans' } }, // Added (F3)
|
|
33693
|
+
{ path: 'general', children: GENERAL_ROUTES }, // Ungated — "general" is a Core module the server always reports enabled (its tasks leaf carries its own "tasks" key, F3)
|
|
33694
|
+
{ path: 'tenancy', children: TENANCY_ROUTES }, // Ungated — tenancy administration
|
|
33695
|
+
{ path: 'workflow', children: WORKFLOW_ROUTES }, // Ungated at area level — Approvals is a module in its own right and now carries a leaf key (F3)
|
|
33696
|
+
{ path: 'overview', children: OVERVIEW_ROUTES }, // Ungated — dashboards
|
|
33697
|
+
{ path: 'fixed-assets', children: ASSETS_ROUTES, data: { moduleKey: 'fixed-assets' } }, // Changed: Added Fixed Assets module // Added (F3): inert metadata
|
|
33698
|
+
{ path: 'setup', component: SetupGuideComponent }, // Added: Getting Started page — home/setup in all consumer apps // NEVER gated: the way back on
|
|
32977
33699
|
{ path: 'day-book', component: DayBookComponent }, // Added: Day Book page — home/day-book, renders empty state when unconfigured
|
|
32978
33700
|
{ path: 'app-configuration', component: AppConfigurationComponent }, // Added: App Configuration page — home/app-configuration, falls back to the library base sections when the app registers none
|
|
32979
|
-
{ path: 'agent', component: AgentPageComponent } // Added: Chat page — home/agent, every consumer app inherits it
|
|
33701
|
+
{ path: 'agent', component: AgentPageComponent, data: { moduleKey: 'agent' } } // Added: Chat page — home/agent, every consumer app inherits it // Added (F3): inert metadata
|
|
32980
33702
|
];
|
|
32981
33703
|
class SpaHomeRoutingModule {
|
|
32982
33704
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SpaHomeRoutingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
@@ -33576,5 +34298,5 @@ const ALSQUARE_SVG_WHITE = `<svg viewBox="0 0 80 80" fill="none" xmlns="http://w
|
|
|
33576
34298
|
* Generated bundle index. Do not edit.
|
|
33577
34299
|
*/
|
|
33578
34300
|
|
|
33579
|
-
export { ALSQUARE_SVG_DARK, ALSQUARE_SVG_WHITE, Account, AccountsComponent as AccountingAccountsComponent, AggregatesComponent as AccountingAggregatesComponent, AgingComponent as AccountingAgingComponent, CreditNotesComponent as AccountingCreditNotesComponent, CurrenciesComponent as AccountingCurrenciesComponent, AccountingDashboardComponent, InvoicesComponent as AccountingInvoicesComponent, AccountingModule, ReportsComponent as AccountingReportsComponent, AccountingService, StatementComponent as AccountingStatementComponent, TransactionTypesComponent as AccountingTransactionTypesComponent, TransactionsComponent as AccountingTransactionsComponent, VatReturnComponent as AccountingVatReturnComponent, Action, ActivityComponent, AdminModule, AgentComponent, AgentPageComponent, AgentService, AlertComponent, AlertConfig, AlertMessage, AnalyticsService, ApiErrorService, ApiResponse, AppConfig, AppConfigurationComponent, AppModelsComponent, AssetStatus, AssetsService, AttachComponent, AuthService, BankReconciliationComponent, BillingPageComponent, BottomTab, BrandsComponent, CacheConfig, CapItem, CapsulesComponent, CategoriesComponent, ChangePasswordComponent, ChangeUserPassword, ChartConfig, ChartsComponent, CheckComponent, ChecklistComponent, ChipsComponent, ConfigService, Constants, Core, CreateAccountComponent, CreditNoteStatus, CustomersComponent, DataServiceLib, DateComponent, DatetimeComponent, DayBookComponent, DepartmentsComponent, DetailsDialog, DetailsDialogConfig, DetailsDialogProcessor, DetailsSource, DialogService, EditorComponent, EmailComponent, EmployeesComponent, ExportService, FeatureDirective, FieldLoadIndicator, FilterComponent, FiscalPeriodStatus, FiscalPeriodsComponent, FormComponent, FormConfig, FormSkeletonComponent, GeneralModule, GeneralService, GradesComponent, GroupsComponent, HRModule, HtmlComponent, HttpService, ImportDialogComponent, IndexModule, InventoryDashboardComponent, InventoryModule, InventoryService, InvitationsTableComponent, InvoiceDashboardComponent, InvoiceItemType, InvoiceStatus, JournalEntryDialogComponent, LabelComponent, LastRouteService, ListDialogComponent, ListDialogConfig, LoaderComponent, LoaderService, LoanPaymentsComponent, LoanProductsComponent, LoansComponent, LoansModule, LoansService, LogLevel, LogService, LoginComponent, LogsComponent, MONOGRAM_TONES, ManufacturingModule, MembershipComponent, MessageService, MoneyComponent, MonogramComponent, MovementType, NavMenuComponent, NotesComponent, NotesConfig, NotificationsService, NumberComponent, OfflineIndicatorComponent, OfflineService, OnboardingComponent, OptionComponent, OverviewDashboardComponent, OverviewModule, PageComponent, PageConfig, PaginationConfig, PayrollDashboardComponent, PayrollModule, PayrollService, PerceivedProgress, PlansComponent, PositionsComponent, PrivacyDialogComponent, ProductionService, Profile, ProfileComponent, PurchaseStatus, PurchasingDashboardComponent, PurchasingModule, PurchasingService, PushNotificationService, QUIET_DELAY_MS, QUIET_MIN_MS, QuoteStatus, QuotesComponent, ReceiptDialogComponent, ReceiptStatus, ReceiptsComponent, ReconcileDialogComponent, RecoverAccountComponent, Register, RevenueScheduleDialogComponent, RevenueSchedulesComponent, Role, RoleAccess, RolesComponent, SalesDashboardComponent, SalesModule, SalesService, SearchComponent, SearchConfig, SecurityConfig, SelectBitwiseComponent, SelectComponent, SelectLiteComponent, SelectMultiComponent, SettingsComponent, SetupGuideComponent, SetupService, SignupComponent, SignupData, SpaAdminModule, SpaDayBookModule, SpaHomeModule, SpaIndexModule, SpaLandingComponent, SpaMatModule, SpaUserModule, StatementImportDialogComponent, StatusesComponent, Step, StepConfig, StepsComponent, StorageService, SubCategoriesComponent, SubscriptionPageComponent, SubscriptionService, SuppliersComponent, SyncComponent, TIN_SILENT_REQUEST, TIN_SPA_RUNTIME_CONFIG, TabService, TableComponent, TableConfig, TabsComponent, TasksComponent, TenancyModule, TenantsComponent, TermsDialogComponent, TextAreaComponent, TextComponent, TextMaskComponent, TextMultiComponent, TextSingleComponent, TileConfig, TilesComponent, TinSpaComponent, TinSpaModule, TinSpaService, TitleActionsComponent, TransactionTiming, UnitOfMeasure, UpdateService, User, UserModule, UsersComponent, ViewerComponent, WelcomeComponent, WorkflowModule, authGuard, dialogOptions, featureGuard, getTinSpaAppSettings, isSilentRequest, loadTinSpaAppSettings, loginConfig, messageDialog, monogramInitials, monogramKind, monogramPaletteIndex, provideTinSpaRuntime, resolveQuietLoading, silentContext, tinSpaLocationStrategyFactory, tinSpaMsalInstanceFactory, tinSpaRuntimeConfigFactory, viewerDialog };
|
|
34301
|
+
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, moduleGuard, monogramInitials, monogramKind, monogramPaletteIndex, provideTinSpaRuntime, resolveQuietLoading, silentContext, tinSpaLocationStrategyFactory, tinSpaMsalInstanceFactory, tinSpaRuntimeConfigFactory, viewerDialog };
|
|
33580
34302
|
//# sourceMappingURL=tin-spa.mjs.map
|