tin-spa 20.14.29 → 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 +889 -129
- package/fesm2022/tin-spa.mjs.map +1 -1
- package/index.d.ts +143 -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';
|
|
@@ -375,7 +375,22 @@ class Core {
|
|
|
375
375
|
return false;
|
|
376
376
|
}
|
|
377
377
|
static getInitialValue(field) {
|
|
378
|
-
if (field.defaultValue)
|
|
378
|
+
// Changed (E21): was `if (field.defaultValue)` — a TRUTHINESS test, so a deliberately falsy default
|
|
379
|
+
// (0, false, '') was silently ignored and the field fell through to the type default below. For most
|
|
380
|
+
// types that is harmless, because the fallthrough happens to land on the same value anyway (number -> 0,
|
|
381
|
+
// checkbox -> false, text -> ''). For a SELECT it is not: the fallthrough is `return null`, and a null
|
|
382
|
+
// posted into a non-nullable backend enum is rejected at model binding with an HTTP 400 that the user
|
|
383
|
+
// sees as the generic "Something went wrong ... a technical problem in the app" panel
|
|
384
|
+
// (api-error.service.ts:138-144). Nothing is logged server-side, because a binding failure is not an
|
|
385
|
+
// exception — which is exactly why this stayed hidden through a 124k-line API log.
|
|
386
|
+
//
|
|
387
|
+
// Measured before changing it: across ng-space and all four consumer apps exactly 11 fields declare a
|
|
388
|
+
// falsy defaultValue. Seven are number/money/checkbox and are unaffected, their fallthrough already
|
|
389
|
+
// producing the same value. The other four are selects that asked for 0 and were silently getting null:
|
|
390
|
+
// accounting.service.ts:152 itemType, :574 kind, assets.service.ts:25 defaultDepreciationMethod and
|
|
391
|
+
// :115 disposalType — three of them required, so users were being made to re-pick a value the config had
|
|
392
|
+
// already chosen for them. This is therefore behaviour-neutral everywhere except where it repairs a bug.
|
|
393
|
+
if (field.defaultValue !== undefined && field.defaultValue !== null) {
|
|
379
394
|
if ((field.type == 'date' || field.type == 'datetime') && field.defaultValue == 'now')
|
|
380
395
|
return this.nowDate(true);
|
|
381
396
|
return field.defaultValue;
|
|
@@ -417,6 +432,30 @@ class Core {
|
|
|
417
432
|
const onlyNumbers = value.replace(/[^\d.-]/g, '');
|
|
418
433
|
return onlyNumbers;
|
|
419
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
|
+
}
|
|
420
459
|
static getFirstDayOfMonth() {
|
|
421
460
|
const date = new Date();
|
|
422
461
|
return new Date(date.getFullYear(), date.getMonth(), 1);
|
|
@@ -439,20 +478,13 @@ class Core {
|
|
|
439
478
|
}
|
|
440
479
|
}
|
|
441
480
|
static isValidEmailList(list) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
else {
|
|
451
|
-
if (this.emailIsValid(list) == false) {
|
|
452
|
-
return false;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
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));
|
|
456
488
|
}
|
|
457
489
|
static { this.nullDate = "01 Jan 1969"; }
|
|
458
490
|
static isNumber(value) {
|
|
@@ -3715,6 +3747,7 @@ class DataServiceLib {
|
|
|
3715
3747
|
this.capEmployees = new CapItem;
|
|
3716
3748
|
this.capDepartments = new CapItem;
|
|
3717
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
|
|
3718
3751
|
this.capGrades = new CapItem;
|
|
3719
3752
|
this.capPayroll = new CapItem; // Changed: Added Payroll module sidebar items
|
|
3720
3753
|
this.capSalaryStructures = new CapItem;
|
|
@@ -3801,6 +3834,11 @@ class DataServiceLib {
|
|
|
3801
3834
|
includeAudit: true,
|
|
3802
3835
|
};
|
|
3803
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' } };
|
|
3804
3842
|
this.departmentDetailsConfig = {
|
|
3805
3843
|
formConfig: this.departmentFormConfig,
|
|
3806
3844
|
heroField: 'departmentID',
|
|
@@ -3820,6 +3858,8 @@ class DataServiceLib {
|
|
|
3820
3858
|
includeAudit: true,
|
|
3821
3859
|
};
|
|
3822
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' } };
|
|
3823
3863
|
this.positionDetailsConfig = {
|
|
3824
3864
|
formConfig: this.positionFormConfig,
|
|
3825
3865
|
heroField: 'positionID',
|
|
@@ -4074,6 +4114,7 @@ class DataServiceLib {
|
|
|
4074
4114
|
},
|
|
4075
4115
|
this.editPositionButton,
|
|
4076
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
|
|
4077
4118
|
],
|
|
4078
4119
|
loadAction: { url: 'positions/all/x' },
|
|
4079
4120
|
formConfig: this.positionFormConfig,
|
|
@@ -4117,6 +4158,7 @@ class DataServiceLib {
|
|
|
4117
4158
|
},
|
|
4118
4159
|
this.editDepartmentButton,
|
|
4119
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
|
|
4120
4162
|
],
|
|
4121
4163
|
loadAction: { url: 'departments/all/x' },
|
|
4122
4164
|
formConfig: this.departmentFormConfig,
|
|
@@ -4720,7 +4762,17 @@ class DataServiceLib {
|
|
|
4720
4762
|
this.capHR.moduleKey = "hr"; // Added (Setup v3)
|
|
4721
4763
|
this.capHR.link = "home/hr/employees";
|
|
4722
4764
|
this.capHR.icon = "diversity_3";
|
|
4723
|
-
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;
|
|
4724
4776
|
this.capEmployees.name = "cap18";
|
|
4725
4777
|
this.capEmployees.display = "Employees";
|
|
4726
4778
|
this.capEmployees.link = "home/hr/employees";
|
|
@@ -5070,6 +5122,13 @@ class DataServiceLib {
|
|
|
5070
5122
|
get modernDialogs() {
|
|
5071
5123
|
return this.appConfig?.dialogStyle === 'modern';
|
|
5072
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
|
+
}
|
|
5073
5132
|
// Added: panel class for the overlay pane. Dialogs apply this so surface-level styling (radius, shadow,
|
|
5074
5133
|
// header band) can live in tin-styles.css, which every consumer app already loads globally — component CSS
|
|
5075
5134
|
// cannot reach the Material surface because it is an ancestor of the component's own view.
|
|
@@ -5355,6 +5414,16 @@ class messageDialog {
|
|
|
5355
5414
|
this.data = data;
|
|
5356
5415
|
this.dataService = dataService;
|
|
5357
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;
|
|
5358
5427
|
// Added: splits the body into paragraphs on blank lines. The messages themselves already separate "what
|
|
5359
5428
|
// happened" from "what to do next" that way, but the old template rendered the whole thing as one run of
|
|
5360
5429
|
// text — which is precisely why nobody read past the first line. The first paragraph is styled as a lead,
|
|
@@ -5372,12 +5441,16 @@ class messageDialog {
|
|
|
5372
5441
|
this._confirmLabel = this.data.confirmLabel; // Added: optional caller-supplied action label for the affirmative button
|
|
5373
5442
|
this._cancelLabel = this.data.cancelLabel; // Added: optional caller-supplied label for the dismissive button
|
|
5374
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
|
|
5375
5445
|
// Added: resolve the style once, at open time. The panel class carries surface-level styling (radius,
|
|
5376
5446
|
// shadow) that component CSS cannot reach, because the Material surface is an ancestor of this view.
|
|
5377
5447
|
this.modern = !!this.dataService?.modernDialogs;
|
|
5378
5448
|
if (this.modern)
|
|
5379
5449
|
this.dialogRef.addPanelClass('tin-dialog-modern');
|
|
5380
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; }
|
|
5381
5454
|
// Added: per-type presentation. Kept as a lookup rather than scattered *ngIf branches so a new type is one
|
|
5382
5455
|
// entry here instead of an edit in three places — the old template hardcoded its heading per type, which is
|
|
5383
5456
|
// exactly how the caller-supplied subject came to be silently dropped for errors.
|
|
@@ -5389,7 +5462,6 @@ class messageDialog {
|
|
|
5389
5462
|
confirm: { icon: 'help_outline', title: 'Just confirming' },
|
|
5390
5463
|
}; }
|
|
5391
5464
|
get spec() { return messageDialog.TYPES[this.messageType] ?? messageDialog.TYPES.info; }
|
|
5392
|
-
get iconName() { return this.spec.icon; }
|
|
5393
5465
|
// Changed: the caller's subject now wins for EVERY type. Previously the heading was hardcoded per type and
|
|
5394
5466
|
// the subject was only rendered for 'info', so a friendly title like "No connection" never appeared on an
|
|
5395
5467
|
// error — the user saw the bare word "Error" instead.
|
|
@@ -5435,11 +5507,11 @@ class messageDialog {
|
|
|
5435
5507
|
this.dialogRef.close(resp);
|
|
5436
5508
|
}
|
|
5437
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 }); }
|
|
5438
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: messageDialog, isStandalone: false, selector: "lib-app-message", ngImport: i0, template: "<!-- Changed: two presentations behind one component. `modern` (appConfig.dialogStyle === 'modern') renders the\n structured layout; anything else renders the long-standing look, so an app that never opts in still gets a\n familiar dialog. Changed: classic is a SUPPORTED escape hatch, not frozen legacy \u2014 it is expected to render\n the same information as modern, and its three rendering bugs were repaired on 2026-08-06. Button ids\n (btnYes / btnNo / btnOK) are identical in BOTH branches \u2014 existing E2E specs select on them and must not\n care which style is active. -->\n\n<ng-container *ngIf=\"modern; else classic\">\n\n <!-- Rebuilt from a hardcoded per-type heading plus one undifferentiated paragraph into a structured layout \u2014\n icon chip, real title, lead paragraph, supporting detail. The type drives colour via a single class on\n the root, so adding a type is one entry in the component rather than three template branches. -->\n <div class=\"tin-msg\" [ngClass]=\"'tin-msg--' + messageType\">\n\n <div class=\"tin-msg__head\">\n <!-- The icon carries the type at a glance. It sits in a soft tinted disc rather than on a saturated\n banner: the intent is to inform, not to alarm someone who has usually done nothing wrong. -->\n <div class=\"tin-msg__icon\">\n <mat-icon>{{ iconName }}</mat-icon>\n </div>\n <h2 class=\"tin-msg__title\">{{ title }}</h2>\n </div>\n\n <mat-dialog-content class=\"tin-msg__body\">\n <!-- First paragraph is the lead (what happened); the rest is supporting detail (what to do next).\n Splitting on blank lines is what gives the eye somewhere to land \u2014 the old single run of text is\n why nobody read past the first line. -->\n <p *ngFor=\"let p of paragraphs; let first = first\" class=\"tin-msg__p\" [class.tin-msg__p--lead]=\"first\">{{ p }}</p>\n\n <!-- The backend's correlation code. Deliberately the quietest thing in the dialog \u2014 the user does not\n need to act on it, they only need to be able to read it out if they call support, at which point it\n turns a \"something broke this morning\" report into one log row. Hidden entirely when absent. -->\n <p class=\"tin-msg__ref\" *ngIf=\"reference\">If you contact your administrator, quote reference <span class=\"tin-msg__ref-code\">{{ reference }}</span>.</p>\n </mat-dialog-content>\n\n <mat-dialog-actions class=\"tin-msg__actions\">\n <!-- Changed: labels reverted to \"No\" / \"Yes\" (owner's call, 2026-08-06). The redesign had shipped them as\n \"Cancel\" / \"Yes, continue\"; these are the most-clicked buttons in the app and the original wording is\n what everybody here reads. Text only \u2014 the ids btnNo / btnYes are untouched and identical to the\n classic branch, so every E2E selector (#btnYes) and every caller is unaffected.\n Changed: the literal text is now {{ confirmLabel }} / {{ cancelLabel }} / {{ okLabel }}, which STILL\n render \"Yes\" / \"No\" / \"OK\" unless the caller passes a label. A caller that knows it is confirming a\n destructive action can name it (\"Delete\") so it no longer reads identically to a harmless one. -->\n <ng-container *ngIf=\"isConfirm; else modernAcknowledge\">\n <button id=\"btnNo\" mat-button class=\"tin-msg__btn-quiet\" (click)=\"response('no')\">{{ cancelLabel }}</button>\n <button id=\"btnYes\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('yes')\" cdkFocusInitial>{{ confirmLabel }}</button>\n </ng-container>\n <ng-template #modernAcknowledge>\n <button id=\"btnOK\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('ok')\" cdkFocusInitial>{{ okLabel }}</button>\n </ng-template>\n </mat-dialog-actions>\n\n </div>\n\n</ng-container>\n\n\n<!-- Classic: the original LOOK, kept deliberately as the escape hatch (owner's call, 2026-08-06 \u2014 retire was\n recommended and declined). It is the original markup plus FIXES, never style changes, because reverting a\n bug alongside a look would be a regression. It renders the same INFORMATION as the modern branch, in its\n own visual language:\n 1. the caller's subject is used as the heading when one was supplied. The old template hardcoded the\n heading per type and rendered the subject for 'info' only, which is why a friendly title like\n \"No connection\" showed up as the bare word \"Error\".\n 2. 'warning' and 'success' are handled. They did not exist when this markup was written, so they would\n otherwise fall through every branch and render an empty dialog.\n 3. 'error' now has an icon; the reference line is no longer trapped in the error-only branch; and the\n body renders real paragraphs instead of one collapsed run of text.\n \u26A0\uFE0F Anything added to the modern branch above has to be considered here too \u2014 that omission is exactly how\n the three bugs in 3. got here. -->\n\n<ng-template #classic>\n <div class=\"tin-msg-classic\">\n\n <h2>\n <div class=\"tin-between\">\n <mat-label>{{ classicHeading }}</mat-label>\n <!-- Added: the missing 'error' icon. Every other type had one; error \u2014 the type that most needs a cue \u2014\n rendered its heading beside empty flex space. Colour matches the modern branch's error accent\n (#c62828) so the two presentations agree on what \"error\" looks like. -->\n <mat-icon *ngIf=\"messageType=='error'\" style=\"color: #c62828;\">error</mat-icon>\n <mat-icon *ngIf=\"messageType=='confirm'\">question_mark</mat-icon>\n <mat-icon *ngIf=\"messageType=='info'\" style=\"color: steelblue;\">info</mat-icon>\n <mat-icon *ngIf=\"messageType=='warning'\" style=\"color: #ef6c00;\">warning</mat-icon>\n <mat-icon *ngIf=\"messageType=='success'\" style=\"color: #2e7d32;\">check_circle</mat-icon>\n </div>\n </h2>\n\n <mat-dialog-content>\n\n <!-- Changed: was `{{ _messageDetails }}` rendered raw, so the blank-line-separated paragraphs the messages\n are actually written in (api-error.service.ts) collapsed into one undifferentiated run of text. That\n collapse is the original complaint that started WS-7 and it was still live here. Now renders the same\n `paragraphs` split the modern branch uses \u2014 same information, classic's own plain markup. -->\n <div *ngIf=\"messageType=='error'\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <div *ngIf=\"messageType!='error'\" class=\"alert alert-secondary\" role=\"alert\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <!-- Changed: lifted OUT of the error-only branch above. The reference is a correlation code, not an\n error-specific field \u2014 a warning or info that carried one used to drop it silently. -->\n <div class=\"tin-msg-classic__ref\" *ngIf=\"reference\">Reference: {{ reference }}</div>\n\n </mat-dialog-content>\n\n <mat-dialog-actions>\n\n <!-- Changed: same caller-supplied labels as the modern branch, defaulting identically to \"Yes\" / \"No\" /\n \"OK\". Classic is expected to render the same INFORMATION as modern in its own visual language, so a\n caller that names its action must be honoured here too \u2014 otherwise the next person to pass a label\n would silently get \"Yes\" on every untracked consumer still running classic. Colours and ids untouched. -->\n <button id=\"btnYes\" mat-stroked-button style=\"color: green;\" *ngIf=\"isConfirm\" (click)=\"response('yes')\" cdkFocusInitial>{{ confirmLabel }}</button>\n\n <button id=\"btnNo\" mat-stroked-button style=\"color: red;\" *ngIf=\"isConfirm\" (click)=\"response('no')\">{{ cancelLabel }}</button>\n\n <button id=\"btnOK\" mat-stroked-button *ngIf=\"!isConfirm\" color=\"primary\" (click)=\"response('ok')\" cdkFocusInitial>{{ okLabel }}</button>\n\n </mat-dialog-actions>\n\n </div>\n</ng-template>\n", styles: [":host{display:block}.tin-msg-classic h2{margin:0!important;padding:16px 16px 0!important;font-size:20px!important;font-weight:500!important;line-height:normal!important}.tin-msg-classic .tin-between{display:flex;justify-content:space-between;align-items:center;margin:0!important;padding:0!important}.tin-msg-classic__p{margin:0 0 10px}.tin-msg-classic__p:last-child{margin-bottom:0}.tin-msg-classic__ref{margin-top:10px;font-size:12px;color:#0000008c;letter-spacing:.04em}.tin-msg{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1);--tin-title: rgba(0, 0, 0, .87);--tin-body: rgba(0, 0, 0, .6);position:relative;padding:4px 4px 0}.tin-msg:before{content:\"\";position:absolute;top:0;left:0;right:0;height:3px;background:var(--tin-accent);border-radius:3px 3px 0 0}.tin-msg--error{--tin-accent: #c62828;--tin-accent-tint: rgba(198, 40, 40, .1)}.tin-msg--warning{--tin-accent: #ef6c00;--tin-accent-tint: rgba(239, 108, 0, .12)}.tin-msg--info{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1)}.tin-msg--success{--tin-accent: #2e7d32;--tin-accent-tint: rgba(46, 125, 50, .12)}.tin-msg--confirm{--tin-accent: #5e35b1;--tin-accent-tint: rgba(94, 53, 177, .1)}.tin-msg__head{display:flex;align-items:center;gap:14px;padding:20px 20px 8px}.tin-msg__icon{flex:0 0 auto;width:40px;height:40px;border-radius:50%;background:var(--tin-accent-tint);display:inline-flex;align-items:center;justify-content:center}.tin-msg__icon .mat-icon,.tin-msg__icon mat-icon{color:var(--tin-accent);font-size:22px;width:22px;height:22px;line-height:22px}.tin-msg__title{margin:0!important;padding:0!important;font-size:18px!important;font-weight:600!important;line-height:1.3!important;letter-spacing:-.1px;color:var(--tin-title)}.tin-msg__body{padding:0 20px 4px 74px!important;max-height:60vh}.tin-msg__p{margin:0 0 12px;font-size:13.5px;line-height:1.6;color:var(--tin-body)}.tin-msg__p--lead{font-size:14.5px;font-weight:500;color:var(--tin-title)}.tin-msg__p:last-child{margin-bottom:0}.tin-msg__ref{margin:14px 0 0;padding-top:10px;border-top:1px solid rgba(0,0,0,.08);font-size:12px;line-height:1.5;color:var(--tin-muted, rgba(0, 0, 0, .55))}.tin-msg__ref-code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;letter-spacing:.06em;color:var(--tin-title, rgba(0, 0, 0, .8));white-space:nowrap;-webkit-user-select:all;user-select:all}.tin-msg__actions{display:flex;justify-content:flex-end;gap:8px;padding:12px 20px 16px!important;margin:0!important;min-height:0!important}.tin-msg__btn-primary{background:var(--tin-accent)!important;color:#fff!important;min-width:88px;font-weight:500;letter-spacing:.2px}.tin-msg__btn-quiet{color:var(--tin-body)!important;font-weight:500}.tin-msg__icon{animation:tin-msg-pop .22s cubic-bezier(.2,.7,.3,1) both}@keyframes tin-msg-pop{0%{transform:scale(.82);opacity:0}to{transform:scale(1);opacity:1}}@media (prefers-reduced-motion: reduce){.tin-msg__icon{animation:none}}@media (max-width: 480px){.tin-msg__head{padding:16px 16px 8px;gap:12px}.tin-msg__body{padding:0 16px 4px!important}.tin-msg__actions{padding:12px 16px 14px!important}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i4.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i4.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }] }); }
|
|
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" }] }); }
|
|
5439
5511
|
}
|
|
5440
5512
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: messageDialog, decorators: [{
|
|
5441
5513
|
type: Component,
|
|
5442
|
-
args: [{ selector: 'lib-app-message', standalone: false, template: "<!-- Changed: two presentations behind one component. `modern` (appConfig.dialogStyle === 'modern') renders the\n structured layout; anything else renders the long-standing look, so an app that never opts in still gets a\n familiar dialog. Changed: classic is a SUPPORTED escape hatch, not frozen legacy \u2014 it is expected to render\n the same information as modern, and its three rendering bugs were repaired on 2026-08-06. Button ids\n (btnYes / btnNo / btnOK) are identical in BOTH branches \u2014 existing E2E specs select on them and must not\n care which style is active. -->\n\n<ng-container *ngIf=\"modern; else classic\">\n\n <!-- Rebuilt from a hardcoded per-type heading plus one undifferentiated paragraph into a structured layout \u2014\n icon chip, real title, lead paragraph, supporting detail. The type drives colour via a single class on\n the root, so adding a type is one entry in the component rather than three template branches. -->\n <div class=\"tin-msg\" [ngClass]=\"'tin-msg--' + messageType\">\n\n <div class=\"tin-msg__head\">\n <!-- The icon carries the type at a glance. It sits in a soft tinted disc rather than on a saturated\n banner: the intent is to inform, not to alarm someone who has usually done nothing wrong. -->\n <div class=\"tin-msg__icon\">\n <mat-icon>{{ iconName }}</mat-icon>\n </div>\n <h2 class=\"tin-msg__title\">{{ title }}</h2>\n </div>\n\n <mat-dialog-content class=\"tin-msg__body\">\n <!-- First paragraph is the lead (what happened); the rest is supporting detail (what to do next).\n Splitting on blank lines is what gives the eye somewhere to land \u2014 the old single run of text is\n why nobody read past the first line. -->\n <p *ngFor=\"let p of paragraphs; let first = first\" class=\"tin-msg__p\" [class.tin-msg__p--lead]=\"first\">{{ p }}</p>\n\n <!-- The backend's correlation code. Deliberately the quietest thing in the dialog \u2014 the user does not\n need to act on it, they only need to be able to read it out if they call support, at which point it\n turns a \"something broke this morning\" report into one log row. Hidden entirely when absent. -->\n <p class=\"tin-msg__ref\" *ngIf=\"reference\">If you contact your administrator, quote reference <span class=\"tin-msg__ref-code\">{{ reference }}</span>.</p>\n </mat-dialog-content>\n\n <mat-dialog-actions class=\"tin-msg__actions\">\n <!-- Changed: labels reverted to \"No\" / \"Yes\" (owner's call, 2026-08-06). The redesign had shipped them as\n \"Cancel\" / \"Yes, continue\"; these are the most-clicked buttons in the app and the original wording is\n what everybody here reads. Text only \u2014 the ids btnNo / btnYes are untouched and identical to the\n classic branch, so every E2E selector (#btnYes) and every caller is unaffected.\n Changed: the literal text is now {{ confirmLabel }} / {{ cancelLabel }} / {{ okLabel }}, which STILL\n render \"Yes\" / \"No\" / \"OK\" unless the caller passes a label. A caller that knows it is confirming a\n destructive action can name it (\"Delete\") so it no longer reads identically to a harmless one. -->\n <ng-container *ngIf=\"isConfirm; else modernAcknowledge\">\n <button id=\"btnNo\" mat-button class=\"tin-msg__btn-quiet\" (click)=\"response('no')\">{{ cancelLabel }}</button>\n <button id=\"btnYes\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('yes')\" cdkFocusInitial>{{ confirmLabel }}</button>\n </ng-container>\n <ng-template #modernAcknowledge>\n <button id=\"btnOK\" mat-flat-button class=\"tin-msg__btn-primary\" (click)=\"response('ok')\" cdkFocusInitial>{{ okLabel }}</button>\n </ng-template>\n </mat-dialog-actions>\n\n </div>\n\n</ng-container>\n\n\n<!-- Classic: the original LOOK, kept deliberately as the escape hatch (owner's call, 2026-08-06 \u2014 retire was\n recommended and declined). It is the original markup plus FIXES, never style changes, because reverting a\n bug alongside a look would be a regression. It renders the same INFORMATION as the modern branch, in its\n own visual language:\n 1. the caller's subject is used as the heading when one was supplied. The old template hardcoded the\n heading per type and rendered the subject for 'info' only, which is why a friendly title like\n \"No connection\" showed up as the bare word \"Error\".\n 2. 'warning' and 'success' are handled. They did not exist when this markup was written, so they would\n otherwise fall through every branch and render an empty dialog.\n 3. 'error' now has an icon; the reference line is no longer trapped in the error-only branch; and the\n body renders real paragraphs instead of one collapsed run of text.\n \u26A0\uFE0F Anything added to the modern branch above has to be considered here too \u2014 that omission is exactly how\n the three bugs in 3. got here. -->\n\n<ng-template #classic>\n <div class=\"tin-msg-classic\">\n\n <h2>\n <div class=\"tin-between\">\n <mat-label>{{ classicHeading }}</mat-label>\n <!-- Added: the missing 'error' icon. Every other type had one; error \u2014 the type that most needs a cue \u2014\n rendered its heading beside empty flex space. Colour matches the modern branch's error accent\n (#c62828) so the two presentations agree on what \"error\" looks like. -->\n <mat-icon *ngIf=\"messageType=='error'\" style=\"color: #c62828;\">error</mat-icon>\n <mat-icon *ngIf=\"messageType=='confirm'\">question_mark</mat-icon>\n <mat-icon *ngIf=\"messageType=='info'\" style=\"color: steelblue;\">info</mat-icon>\n <mat-icon *ngIf=\"messageType=='warning'\" style=\"color: #ef6c00;\">warning</mat-icon>\n <mat-icon *ngIf=\"messageType=='success'\" style=\"color: #2e7d32;\">check_circle</mat-icon>\n </div>\n </h2>\n\n <mat-dialog-content>\n\n <!-- Changed: was `{{ _messageDetails }}` rendered raw, so the blank-line-separated paragraphs the messages\n are actually written in (api-error.service.ts) collapsed into one undifferentiated run of text. That\n collapse is the original complaint that started WS-7 and it was still live here. Now renders the same\n `paragraphs` split the modern branch uses \u2014 same information, classic's own plain markup. -->\n <div *ngIf=\"messageType=='error'\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <div *ngIf=\"messageType!='error'\" class=\"alert alert-secondary\" role=\"alert\">\n <p class=\"tin-msg-classic__p\" *ngFor=\"let p of paragraphs\">{{ p }}</p>\n </div>\n\n <!-- Changed: lifted OUT of the error-only branch above. The reference is a correlation code, not an\n error-specific field \u2014 a warning or info that carried one used to drop it silently. -->\n <div class=\"tin-msg-classic__ref\" *ngIf=\"reference\">Reference: {{ reference }}</div>\n\n </mat-dialog-content>\n\n <mat-dialog-actions>\n\n <!-- Changed: same caller-supplied labels as the modern branch, defaulting identically to \"Yes\" / \"No\" /\n \"OK\". Classic is expected to render the same INFORMATION as modern in its own visual language, so a\n caller that names its action must be honoured here too \u2014 otherwise the next person to pass a label\n would silently get \"Yes\" on every untracked consumer still running classic. Colours and ids untouched. -->\n <button id=\"btnYes\" mat-stroked-button style=\"color: green;\" *ngIf=\"isConfirm\" (click)=\"response('yes')\" cdkFocusInitial>{{ confirmLabel }}</button>\n\n <button id=\"btnNo\" mat-stroked-button style=\"color: red;\" *ngIf=\"isConfirm\" (click)=\"response('no')\">{{ cancelLabel }}</button>\n\n <button id=\"btnOK\" mat-stroked-button *ngIf=\"!isConfirm\" color=\"primary\" (click)=\"response('ok')\" cdkFocusInitial>{{ okLabel }}</button>\n\n </mat-dialog-actions>\n\n </div>\n</ng-template>\n", styles: [":host{display:block}.tin-msg-classic h2{margin:0!important;padding:16px 16px 0!important;font-size:20px!important;font-weight:500!important;line-height:normal!important}.tin-msg-classic .tin-between{display:flex;justify-content:space-between;align-items:center;margin:0!important;padding:0!important}.tin-msg-classic__p{margin:0 0 10px}.tin-msg-classic__p:last-child{margin-bottom:0}.tin-msg-classic__ref{margin-top:10px;font-size:12px;color:#0000008c;letter-spacing:.04em}.tin-msg{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1);--tin-title: rgba(0, 0, 0, .87);--tin-body: rgba(0, 0, 0, .6);position:relative;padding:4px 4px 0}.tin-msg:before{content:\"\";position:absolute;top:0;left:0;right:0;height:3px;background:var(--tin-accent);border-radius:3px 3px 0 0}.tin-msg--error{--tin-accent: #c62828;--tin-accent-tint: rgba(198, 40, 40, .1)}.tin-msg--warning{--tin-accent: #ef6c00;--tin-accent-tint: rgba(239, 108, 0, .12)}.tin-msg--info{--tin-accent: #1565c0;--tin-accent-tint: rgba(21, 101, 192, .1)}.tin-msg--success{--tin-accent: #2e7d32;--tin-accent-tint: rgba(46, 125, 50, .12)}.tin-msg--confirm{--tin-accent: #5e35b1;--tin-accent-tint: rgba(94, 53, 177, .1)}.tin-msg__head{display:flex;align-items:center;gap:14px;padding:20px 20px 8px}.tin-msg__icon{flex:0 0 auto;width:40px;height:40px;border-radius:50%;background:var(--tin-accent-tint);display:inline-flex;align-items:center;justify-content:center}.tin-msg__icon .mat-icon,.tin-msg__icon mat-icon{color:var(--tin-accent);font-size:22px;width:22px;height:22px;line-height:22px}.tin-msg__title{margin:0!important;padding:0!important;font-size:18px!important;font-weight:600!important;line-height:1.3!important;letter-spacing:-.1px;color:var(--tin-title)}.tin-msg__body{padding:0 20px 4px 74px!important;max-height:60vh}.tin-msg__p{margin:0 0 12px;font-size:13.5px;line-height:1.6;color:var(--tin-body)}.tin-msg__p--lead{font-size:14.5px;font-weight:500;color:var(--tin-title)}.tin-msg__p:last-child{margin-bottom:0}.tin-msg__ref{margin:14px 0 0;padding-top:10px;border-top:1px solid rgba(0,0,0,.08);font-size:12px;line-height:1.5;color:var(--tin-muted, rgba(0, 0, 0, .55))}.tin-msg__ref-code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;letter-spacing:.06em;color:var(--tin-title, rgba(0, 0, 0, .8));white-space:nowrap;-webkit-user-select:all;user-select:all}.tin-msg__actions{display:flex;justify-content:flex-end;gap:8px;padding:12px 20px 16px!important;margin:0!important;min-height:0!important}.tin-msg__btn-primary{background:var(--tin-accent)!important;color:#fff!important;min-width:88px;font-weight:500;letter-spacing:.2px}.tin-msg__btn-quiet{color:var(--tin-body)!important;font-weight:500}.tin-msg__icon{animation:tin-msg-pop .22s cubic-bezier(.2,.7,.3,1) both}@keyframes tin-msg-pop{0%{transform:scale(.82);opacity:0}to{transform:scale(1);opacity:1}}@media (prefers-reduced-motion: reduce){.tin-msg__icon{animation:none}}@media (max-width: 480px){.tin-msg__head{padding:16px 16px 8px;gap:12px}.tin-msg__body{padding:0 16px 4px!important}.tin-msg__actions{padding:12px 16px 14px!important}}\n"] }]
|
|
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"] }]
|
|
5443
5515
|
}], ctorParameters: () => [{ type: i4.MatDialogRef }, { type: undefined, decorators: [{
|
|
5444
5516
|
type: Inject,
|
|
5445
5517
|
args: [MAT_DIALOG_DATA]
|
|
@@ -5519,13 +5591,17 @@ class MessageService {
|
|
|
5519
5591
|
// ⚠️ Do NOT change the defaults to make every confirm read better: piglet-spa (38) and shift-spa (13) select
|
|
5520
5592
|
// these buttons by their literal text via getByRole('button', { name: 'Yes' }), and the owner reverted a
|
|
5521
5593
|
// hardcoded relabel on 2026-08-06. Opt in per call site.
|
|
5522
|
-
|
|
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) {
|
|
5523
5599
|
let type = "confirm";
|
|
5524
5600
|
let subject = "";
|
|
5525
5601
|
let details = msg;
|
|
5526
5602
|
const dialogRef = this.dialog.open(messageDialog, {
|
|
5527
5603
|
width: "400px",
|
|
5528
|
-
data: { type, subject, details, confirmLabel, cancelLabel },
|
|
5604
|
+
data: { type, subject, details, confirmLabel, cancelLabel, destructive },
|
|
5529
5605
|
});
|
|
5530
5606
|
return dialogRef.afterClosed().pipe(mergeMap((result) => {
|
|
5531
5607
|
return of(result);
|
|
@@ -6766,7 +6842,13 @@ class AccountingService {
|
|
|
6766
6842
|
{ name: 'code', type: 'text', alias: 'Account Code', section: 'classification', infoMessage: 'Chart of accounts code (1xxx assets, 2xxx liabilities, 3xxx equity, 4xxx revenue, 5xxx expenses)' }, // Changed: COA code (B1); moved into the collapsed section
|
|
6767
6843
|
{ name: 'includeInCashTotal', type: 'checkbox', alias: 'Include in Cash Total', section: 'classification', hidden: (data) => data.type !== 0 }, // Changed: Only visible for Asset accounts (type 0)
|
|
6768
6844
|
{ name: 'includeInBankTotal', type: 'checkbox', alias: 'Include in Bank Total', section: 'classification', hidden: (data) => data.type !== 0 }, // Changed: Only visible for Asset accounts (type 0)
|
|
6769
|
-
|
|
6845
|
+
// Changed (E21): defaultValue added. An untouched select initialises to null (TinCore.getInitialValue
|
|
6846
|
+
// `case 'select': return null`), and Account.CashFlow is a NON-NULLABLE CashFlowCategory, so creating an
|
|
6847
|
+
// account without opening this collapsed section posted cashFlow:null and was rejected at model binding
|
|
6848
|
+
// with a 400 — surfaced to the user as "Something went wrong ... a technical problem in the app" and
|
|
6849
|
+
// logged nowhere. 0 is Operating, which is the model's own C# default (Account.cs:69), so this makes the
|
|
6850
|
+
// form agree with the entity rather than inventing a value.
|
|
6851
|
+
{ name: 'cashFlow', type: 'select', alias: 'Cash Flow Section', section: 'classification', defaultValue: 0, infoMessage: 'IAS 7 cash flow statement classification', // Changed: C5 cash flow category
|
|
6770
6852
|
options: [
|
|
6771
6853
|
{ name: 'Operating', value: 0 },
|
|
6772
6854
|
{ name: 'Investing', value: 1 },
|
|
@@ -10722,6 +10804,47 @@ class ButtonService {
|
|
|
10722
10804
|
// Return static message
|
|
10723
10805
|
return button.confirm.message;
|
|
10724
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
|
+
}
|
|
10725
10848
|
getButtonProperties(button, row, config) {
|
|
10726
10849
|
return {
|
|
10727
10850
|
color: this.getButtonColor(button, row),
|
|
@@ -11700,6 +11823,8 @@ class SetupService {
|
|
|
11700
11823
|
// Added (v2): module catalog — empty means the app has no module concept (nothing is ever hidden)
|
|
11701
11824
|
this.modules = new BehaviorSubject([]);
|
|
11702
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
|
|
11703
11828
|
} // Changed: injected for loadStatus only
|
|
11704
11829
|
get enabled() {
|
|
11705
11830
|
return !!this.dataService.appConfig?.setupConfig?.enabled;
|
|
@@ -11738,6 +11863,7 @@ class SetupService {
|
|
|
11738
11863
|
this.status.next(apiResponse.data);
|
|
11739
11864
|
this.pendingCount.next(apiResponse.data?.pending ?? 0);
|
|
11740
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
|
|
11741
11867
|
}
|
|
11742
11868
|
},
|
|
11743
11869
|
error: () => { }
|
|
@@ -11755,10 +11881,30 @@ class SetupService {
|
|
|
11755
11881
|
// a dialog becomes a wall.
|
|
11756
11882
|
if (apiResponse.success)
|
|
11757
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"
|
|
11758
11885
|
},
|
|
11759
|
-
error: () => { } //
|
|
11886
|
+
error: () => { this.modulesLoaded = true; } // Changed (F3): a backend without module support must not leave every later ensureModules() re-requesting
|
|
11760
11887
|
});
|
|
11761
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
|
+
}
|
|
11762
11908
|
// Added (v2): nav-menu gate — unknown keys and apps without modules always pass (fail open)
|
|
11763
11909
|
isModuleEnabled(key) {
|
|
11764
11910
|
if (!key)
|
|
@@ -11804,6 +11950,26 @@ class SetupService {
|
|
|
11804
11950
|
applyApprovalPresets(items) {
|
|
11805
11951
|
return this.dataService.CallApi({ url: 'setup/approvals', method: 'post', skipCache: true }, { items });
|
|
11806
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
|
+
}
|
|
11807
11973
|
refresh() {
|
|
11808
11974
|
this.loadStatus();
|
|
11809
11975
|
}
|
|
@@ -12103,6 +12269,16 @@ class SetupGuideComponent {
|
|
|
12103
12269
|
this.roleTemplates = [];
|
|
12104
12270
|
this.selectedTemplates = {};
|
|
12105
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;
|
|
12106
12282
|
// Added (presets): picker state. `groups` is materialized (same rule as moduleGroups above); selected/role are
|
|
12107
12283
|
// plain maps keyed by preset key so a reload never clobbers a manual choice.
|
|
12108
12284
|
// Changed (presets P4): one object per domain instead of a field per domain — see PresetPicker above.
|
|
@@ -12164,6 +12340,8 @@ class SetupGuideComponent {
|
|
|
12164
12340
|
this.buildGroups();
|
|
12165
12341
|
if (this.hasRoleTemplatesStep())
|
|
12166
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
|
|
12167
12345
|
// Added (presets): only load a picker when the backend actually serves its step. The approvals step is gated
|
|
12168
12346
|
// on the approvals module, so turning that module off makes the step — and this call — disappear with it.
|
|
12169
12347
|
this.pickers.forEach(picker => { if (this.hasPresetsStep(picker))
|
|
@@ -12352,6 +12530,122 @@ class SetupGuideComponent {
|
|
|
12352
12530
|
return;
|
|
12353
12531
|
this.selectedTemplates[tpl.key] = !this.selectedTemplates[tpl.key];
|
|
12354
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
|
+
}
|
|
12355
12649
|
//---------- Preset pickers (notifications, sign-off) ----------
|
|
12356
12650
|
// ONE code path, driven by the PresetPicker objects above. Tick a row, pick who it goes to, apply. Both backends
|
|
12357
12651
|
// are create-only and skip anything already configured, so neither can overwrite a rule the user has since
|
|
@@ -12470,6 +12764,48 @@ class SetupGuideComponent {
|
|
|
12470
12764
|
removeDemoData() {
|
|
12471
12765
|
this.runDemoAction('demo-data/remove', 'This permanently deletes every example record created by demo data. Continue?', 'Demo data removed successfully');
|
|
12472
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
|
+
}
|
|
12473
12809
|
runDemoAction(url, question, success) {
|
|
12474
12810
|
if (this.demoBusy)
|
|
12475
12811
|
return;
|
|
@@ -12515,11 +12851,11 @@ class SetupGuideComponent {
|
|
|
12515
12851
|
});
|
|
12516
12852
|
}
|
|
12517
12853
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SetupGuideComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
12518
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SetupGuideComponent, isStandalone: false, selector: "spa-setup-guide", ngImport: i0, template: "<div class=\"setup-page\" *ngIf=\"status\">\n\n <!-- Hero: overall readiness + celebration state at 100% -->\n <mat-card class=\"setup-hero\" [class.celebrate]=\"status.percent === 100\">\n <div class=\"hero-content\" *ngIf=\"status.percent < 100\">\n <div class=\"hero-text\">\n <h1>{{ title }}</h1>\n <p>Complete these steps to get your system ready for day-to-day operation.</p>\n <span class=\"hero-counter\">{{ status.completed }} of {{ status.total }} steps completed</span>\n </div>\n <div class=\"hero-progress\">\n <span class=\"hero-percent\">{{ status.percent }}%</span>\n <mat-progress-bar mode=\"determinate\" [value]=\"status.percent\"></mat-progress-bar>\n </div>\n </div>\n <div class=\"hero-content celebration\" *ngIf=\"status.percent === 100\">\n <mat-icon class=\"celebrate-icon\">celebration</mat-icon>\n <div class=\"hero-text\">\n <h1>You're all set!</h1>\n <p>All setup steps are complete \u2014 your system is ready to operate.</p>\n </div>\n </div>\n </mat-card>\n\n <!-- Module picker (v2): choose what the business uses; optional modules toggle steps + menus -->\n <mat-card class=\"setup-modules\" *ngIf=\"modules.length > 0\">\n <div class=\"group-header\">\n <h2>Your modules</h2>\n <span class=\"group-counter\">Tap a module to turn it on or off \u2014 you can change this anytime.</span>\n </div>\n <!-- Changed (v3): grouped \u2014 the picker now carries the whole library catalog, so cards sit under their\n group heading (the app's own modules first). A backend without groups yields one unnamed group,\n which renders as the original flat grid. -->\n <div class=\"module-section\" *ngFor=\"let mgroup of moduleGroups\">\n <div class=\"module-section-head\">\n <span class=\"module-section-name\">{{ mgroup.name }}</span>\n <span class=\"module-section-count\">{{ mgroup.enabled }} of {{ mgroup.total }} on</span>\n </div>\n <div class=\"module-grid\">\n <div class=\"module-card\" *ngFor=\"let mod of mgroup.modules\"\n [class.enabled]=\"mod.enabled\" [class.core]=\"mod.core\" [class.busy]=\"togglingKey === mod.key\"\n (click)=\"toggleModule(mod)\">\n <div class=\"module-head\">\n <mat-icon class=\"module-icon\">{{ mod.icon || 'extension' }}</mat-icon>\n <mat-icon class=\"module-state\" [class.on]=\"mod.enabled\">{{ mod.enabled ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n </div>\n <div class=\"module-title\">{{ mod.title }}</div>\n <div class=\"module-description\">{{ mod.description }}</div>\n <span class=\"module-chip\" *ngIf=\"mod.core\">Always on</span>\n </div>\n </div>\n </div>\n <div class=\"module-actions\" *ngIf=\"!modulesConfirmed\">\n <button mat-flat-button color=\"primary\" [disabled]=\"togglingKey !== ''\" (click)=\"confirmModules()\">Confirm selection</button>\n <span class=\"module-hint\">Happy with this selection? Confirm it to complete the step below.</span>\n </div>\n </mat-card>\n\n <!-- Category groups, top-down \u2014 Changed: flat spa-checklist replaces the raised mat-accordion -->\n <mat-card class=\"setup-group\" *ngFor=\"let group of groups\">\n <div class=\"group-header\">\n <h2>{{ group.name }}</h2>\n <span class=\"group-counter\">{{ group.completed }} of {{ group.total }}</span>\n </div>\n <spa-checklist [config]=\"group.config\" [itemTemplate]=\"stepExtrasTpl\"></spa-checklist>\n </mat-card>\n\n <!-- Demo data (opt-in via setupConfig.demoData) \u2014 moved here off the per-app configuration pages -->\n <mat-card class=\"setup-group setup-demo\" *ngIf=\"showDemoData\">\n <div class=\"group-header\">\n <h2>Demo data</h2>\n <span class=\"group-counter\">Explore with example records, then clear them out</span>\n </div>\n <p class=\"demo-description\">Seeding fills the system with example records so you can try it out before capturing anything real. Removing deletes only those example records.</p>\n <div class=\"demo-actions\">\n <button mat-flat-button color=\"primary\" [disabled]=\"demoBusy\" (click)=\"seedDemoData()\"><mat-icon>add_circle</mat-icon> Seed demo data</button>\n <button mat-stroked-button color=\"warn\" [disabled]=\"demoBusy\" (click)=\"removeDemoData()\"><mat-icon>delete</mat-icon> Remove demo data</button>\n </div>\n </mat-card>\n\n</div>\n\n<!-- Projected into the expanded checklist body: predefined roles picker on the roles step (v2) -->\n<ng-template #stepExtrasTpl let-item>\n <div class=\"role-templates\" *ngIf=\"hasRoleTemplates(item.data) && roleTemplates.length > 0\">\n <div class=\"role-template\" *ngFor=\"let tpl of roleTemplates\" [class.created]=\"tpl.exists\" [class.selected]=\"!tpl.exists && selectedTemplates[tpl.key]\" (click)=\"toggleTemplate(tpl)\">\n <div class=\"role-head\">\n <mat-icon class=\"role-state\" [class.on]=\"tpl.exists || selectedTemplates[tpl.key]\">{{ (tpl.exists || selectedTemplates[tpl.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"role-name\">{{ tpl.name }}</span>\n <span class=\"role-created\" *ngIf=\"tpl.exists\"><mat-icon>check</mat-icon>Created</span>\n </div>\n <div class=\"role-description\">{{ tpl.description }}</div>\n </div>\n </div>\n\n <!-- Added (presets): the preset picker. Same row as the role picker above \u2014 same state icon, same 8px stack,\n same colours \u2014 plus ONE sentence and ONE control.\n Changed (presets P4): rendered through ONE shared template for both the notification step and the sign-off\n step, parameterised by the picker object. The two sit one above the other on the same page, so a second\n near-identical block would drift and every difference would read as a mistake. -->\n <ng-container *ngIf=\"pickerFor(item.data) as picker\">\n <ng-container *ngTemplateOutlet=\"presetPickerTpl; context: { $implicit: picker }\"></ng-container>\n </ng-container>\n</ng-template>\n\n<!-- The one picker body. `picker` carries the domain: its rows, its roles, its wording, its apply call. -->\n<ng-template #presetPickerTpl let-picker>\n <div class=\"preset-picker\" *ngIf=\"picker.loaded && picker.presets.length > 0\">\n\n <!-- No selectable role \u2192 we ask for one instead of offering ticks that would build an inert rule. The wording\n is the picker's own, because \"no roles\" and \"roles nobody is in\" are different situations. -->\n <div class=\"preset-empty\" *ngIf=\"picker.roles.length === 0\">{{ picker.emptyText }}</div>\n\n <ng-container *ngIf=\"picker.roles.length > 0\">\n <div class=\"preset-section\" *ngFor=\"let pgroup of picker.groups\">\n <div class=\"preset-section-head\">\n <span class=\"preset-section-name\">{{ pgroup.name }}</span>\n <span class=\"preset-section-count\">{{ pgroup.on }} of {{ pgroup.total }}</span>\n </div>\n <div class=\"preset-row\" *ngFor=\"let row of pgroup.rows\" role=\"checkbox\" tabindex=\"0\"\n [class.configured]=\"row.preset.exists\" [class.selected]=\"!row.preset.exists && picker.selected[row.preset.key]\"\n [attr.aria-checked]=\"row.preset.exists || !!picker.selected[row.preset.key]\" [attr.aria-disabled]=\"row.preset.exists\"\n (click)=\"togglePreset(picker, row.preset)\"\n (keydown.enter)=\"togglePreset(picker, row.preset)\"\n (keydown.space)=\"togglePreset(picker, row.preset); $event.preventDefault()\">\n <div class=\"preset-head\">\n <mat-icon class=\"preset-state\" [class.on]=\"row.preset.exists || picker.selected[row.preset.key]\">{{ (row.preset.exists || picker.selected[row.preset.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"preset-name\">{{ row.preset.name }}</span>\n <span class=\"preset-configured\" *ngIf=\"row.preset.exists\"><mat-icon>check</mat-icon>Configured</span>\n </div>\n <div class=\"preset-description\">{{ row.preset.description }}</div>\n <!-- The effect sentence comes from the server whole; `before` and `after` are its two halves either side\n of the {role} placeholder, so reading the line IS reading the configuration. The sign-off sentences\n carry their own deferred-notification caveat, which is why there is no extra note here. -->\n <div class=\"preset-effect\" *ngIf=\"!row.preset.exists\">{{ row.before }}<select class=\"preset-role\" [attr.aria-label]=\"picker.roleAria + row.preset.name\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n (change)=\"setPresetRole(picker, row.preset, $any($event.target).value)\">\n <option *ngFor=\"let role of picker.roles\" [value]=\"role.value\" [selected]=\"role.value === picker.role[row.preset.key]\">{{ role.name }}</option>\n </select>{{ row.after }}</div>\n <div class=\"preset-effect preset-done\" *ngIf=\"row.preset.exists\">{{ picker.doneText }}</div>\n </div>\n </div>\n\n <!-- One page-level channel choice, notifications only. In-app is the only channel that always works; SMS is a\n stub and is never offered; an approval request is always in-app, so the sign-off picker has no such line. -->\n <div class=\"preset-email\" *ngIf=\"picker.showEmail\" role=\"checkbox\" tabindex=\"0\" [attr.aria-checked]=\"includeEmail\"\n (click)=\"toggleIncludeEmail()\"\n (keydown.enter)=\"toggleIncludeEmail()\"\n (keydown.space)=\"toggleIncludeEmail(); $event.preventDefault()\">\n <mat-icon class=\"preset-state\" [class.on]=\"includeEmail\">{{ includeEmail ? 'check_box' : 'check_box_outline_blank' }}</mat-icon>\n <span class=\"preset-email-label\">Also send these by email</span>\n <span class=\"preset-email-hint\">In-app always. Email needs your mail settings.</span>\n </div>\n </ng-container>\n </div>\n</ng-template>\n\n<!-- Graceful empty state (feature disabled or status unavailable) -->\n<div class=\"setup-empty\" *ngIf=\"!status\">\n <mat-icon>rocket_launch</mat-icon>\n <p>Setup status is not available yet.</p>\n</div>\n", styles: [".setup-page{max-width:860px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.setup-hero{padding:24px}.hero-content{display:flex;align-items:center;justify-content:space-between;gap:24px;flex-wrap:wrap}.hero-text h1{margin:0 0 4px;font-size:24px}.hero-text p{margin:0 0 8px;color:#0009}.hero-counter{font-size:13px;color:#0009}.hero-progress{flex:1;min-width:220px;max-width:340px}.hero-percent{display:block;font-size:28px;font-weight:600;color:#2e7d32;margin-bottom:6px;text-align:right}.hero-progress mat-progress-bar{height:10px;border-radius:5px}.setup-hero.celebrate{background:linear-gradient(135deg,#e8f5e9,#f1f8e9)}.celebration{justify-content:flex-start}.celebrate-icon{font-size:48px;width:48px;height:48px;color:#2e7d32;animation:celebrate-pop .6s ease-out}@keyframes celebrate-pop{0%{transform:scale(.3);opacity:0}70%{transform:scale(1.15)}to{transform:scale(1);opacity:1}}.setup-group{padding:16px}.group-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.group-header h2{margin:0;font-size:17px}.group-counter{font-size:12px;color:#0000008c}.setup-modules{padding:16px}.module-section+.module-section{margin-top:18px}.module-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.module-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.module-section-count{font-size:11px;color:#00000073;white-space:nowrap}.module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;margin-top:8px}.module-card{border:1px solid rgba(0,0,0,.12);border-radius:10px;padding:12px;cursor:pointer;transition:border-color .15s,background .15s,opacity .15s;opacity:.72;position:relative}.module-card:hover{border-color:#90a4ae}.module-card.enabled{border-color:#4caf50;background:#f6fbf6;opacity:1}.module-card.core{cursor:default}.module-card.busy{pointer-events:none;opacity:.5}.module-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.module-icon{color:#546e7a}.module-card.enabled .module-icon{color:#2e7d32}.module-state{color:#b0bec5}.module-state.on{color:#4caf50}.module-title{font-weight:600;font-size:14px;margin-bottom:4px}.module-description{font-size:12px;color:#0009;min-height:30px}.module-chip{display:inline-block;margin-top:8px;background:#eceff1;color:#546e7a;border-radius:12px;padding:2px 10px;font-size:11px}.module-actions{display:flex;align-items:center;gap:8px 12px;margin-top:14px;flex-wrap:wrap}.module-actions button{flex-shrink:0}.module-hint{font-size:12px;color:#0000008c}.role-templates{display:flex;flex-direction:column;gap:8px;margin:0 0 12px}.role-template{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.role-template:hover{border-color:#90a4ae}.role-template.selected{border-color:#4caf50;background:#f6fbf6}.role-template.created{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.role-head{display:flex;align-items:center;gap:8px}.role-state{color:#b0bec5;flex-shrink:0}.role-state.on{color:#4caf50}.role-name{font-weight:500;font-size:13px}.role-created{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32}.role-created mat-icon{font-size:16px;width:16px;height:16px}.role-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-picker{display:flex;flex-direction:column;gap:14px;margin:0 0 12px}.preset-empty{font-size:12px;line-height:18px;color:#0009;max-width:62ch}.preset-section{display:flex;flex-direction:column;gap:8px}.preset-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.preset-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.preset-section-count{font-size:11px;color:#00000073;white-space:nowrap}.preset-row{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.preset-row:hover{border-color:#90a4ae}.preset-row:focus-visible{outline:2px solid #4caf50;outline-offset:2px}.preset-row.selected{border-color:#4caf50;background:#f6fbf6}.preset-row.configured{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.preset-head{display:flex;align-items:center;gap:8px}.preset-state{color:#b0bec5;flex-shrink:0}.preset-state.on{color:#4caf50}.preset-name{font-weight:500;font-size:13px}.preset-configured{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32;white-space:nowrap}.preset-configured mat-icon{font-size:16px;width:16px;height:16px}.preset-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-effect{font-size:12px;line-height:22px;color:#000000c7;margin:4px 0 0 32px}.preset-done{color:#2e7d32}.preset-role{font-family:inherit;font-size:12px;font-weight:500;color:#2e7d32;background:#fff;border:1px solid #c8e6c9;border-radius:6px;padding:2px 4px;margin:0 2px;max-width:100%;cursor:pointer}.preset-role:hover{border-color:#4caf50}.preset-role:focus-visible{outline:2px solid #4caf50;outline-offset:1px}.preset-email{display:flex;align-items:center;gap:8px;flex-wrap:wrap;cursor:pointer;padding:2px 0}.preset-email:focus-visible{outline:2px solid #4caf50;outline-offset:2px;border-radius:6px}.preset-email-label{font-size:13px;font-weight:500}.preset-email-hint{font-size:12px;color:#0000008c}.setup-empty{text-align:center;padding:48px 16px;color:#00000080}.setup-empty mat-icon{font-size:40px;width:40px;height:40px}.demo-description{margin:8px 0 14px;font-size:13px;color:#0009}.demo-actions{display:flex;gap:12px;flex-wrap:wrap}@media (max-width: 700px){.setup-page{padding:10px 0;gap:12px}.setup-hero{padding:12px}.setup-modules,.setup-group{padding:10px}.hero-content{gap:10px}.hero-text h1{font-size:20px}.setup-hero:not(.celebrate) .hero-text p{display:none}.hero-counter{font-size:12px}.hero-progress{min-width:0;max-width:none}.hero-percent{font-size:22px;margin-bottom:4px}.hero-progress mat-progress-bar{height:8px;border-radius:4px}.celebrate-icon{font-size:36px;width:36px;height:36px}.group-header{margin-bottom:6px;gap:2px 8px}.group-header h2{font-size:15px}.group-counter{font-size:11px}.setup-modules>.group-header,.setup-demo>.group-header{flex-wrap:wrap}.setup-modules>.group-header .group-counter,.setup-demo>.group-header .group-counter{flex:1 0 100%}.module-section+.module-section{margin-top:12px}.module-section-head{padding-bottom:4px}.module-grid{gap:8px;margin-top:6px}.module-card{padding:10px}.module-head{margin-bottom:4px}.module-title{font-size:13.5px;margin-bottom:2px}.module-description{min-height:0}.module-chip{margin-top:6px}.module-actions{margin-top:10px}.role-templates,.preset-picker{gap:6px;margin-bottom:8px}.role-template,.preset-row{padding:8px 10px}.role-description,.preset-description,.preset-effect{margin-left:0}.preset-section{gap:6px}.preset-section-head{padding-bottom:4px}.demo-description{margin:6px 0 10px;font-size:12.5px}.demo-actions{gap:8px}.setup-empty{padding:28px 12px}.module-hint{flex:0 0 100%}.preset-role{display:block;width:100%;margin:4px 0 0;padding:6px 8px}.preset-email-hint{flex:0 0 100%}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i2$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: i6$1.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: ChecklistComponent, selector: "spa-checklist", inputs: ["config", "itemTemplate"] }] }); }
|
|
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"] }] }); }
|
|
12519
12855
|
}
|
|
12520
12856
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SetupGuideComponent, decorators: [{
|
|
12521
12857
|
type: Component,
|
|
12522
|
-
args: [{ selector: 'spa-setup-guide', standalone: false, template: "<div class=\"setup-page\" *ngIf=\"status\">\n\n <!-- Hero: overall readiness + celebration state at 100% -->\n <mat-card class=\"setup-hero\" [class.celebrate]=\"status.percent === 100\">\n <div class=\"hero-content\" *ngIf=\"status.percent < 100\">\n <div class=\"hero-text\">\n <h1>{{ title }}</h1>\n <p>Complete these steps to get your system ready for day-to-day operation.</p>\n <span class=\"hero-counter\">{{ status.completed }} of {{ status.total }} steps completed</span>\n </div>\n <div class=\"hero-progress\">\n <span class=\"hero-percent\">{{ status.percent }}%</span>\n <mat-progress-bar mode=\"determinate\" [value]=\"status.percent\"></mat-progress-bar>\n </div>\n </div>\n <div class=\"hero-content celebration\" *ngIf=\"status.percent === 100\">\n <mat-icon class=\"celebrate-icon\">celebration</mat-icon>\n <div class=\"hero-text\">\n <h1>You're all set!</h1>\n <p>All setup steps are complete \u2014 your system is ready to operate.</p>\n </div>\n </div>\n </mat-card>\n\n <!-- Module picker (v2): choose what the business uses; optional modules toggle steps + menus -->\n <mat-card class=\"setup-modules\" *ngIf=\"modules.length > 0\">\n <div class=\"group-header\">\n <h2>Your modules</h2>\n <span class=\"group-counter\">Tap a module to turn it on or off \u2014 you can change this anytime.</span>\n </div>\n <!-- Changed (v3): grouped \u2014 the picker now carries the whole library catalog, so cards sit under their\n group heading (the app's own modules first). A backend without groups yields one unnamed group,\n which renders as the original flat grid. -->\n <div class=\"module-section\" *ngFor=\"let mgroup of moduleGroups\">\n <div class=\"module-section-head\">\n <span class=\"module-section-name\">{{ mgroup.name }}</span>\n <span class=\"module-section-count\">{{ mgroup.enabled }} of {{ mgroup.total }} on</span>\n </div>\n <div class=\"module-grid\">\n <div class=\"module-card\" *ngFor=\"let mod of mgroup.modules\"\n [class.enabled]=\"mod.enabled\" [class.core]=\"mod.core\" [class.busy]=\"togglingKey === mod.key\"\n (click)=\"toggleModule(mod)\">\n <div class=\"module-head\">\n <mat-icon class=\"module-icon\">{{ mod.icon || 'extension' }}</mat-icon>\n <mat-icon class=\"module-state\" [class.on]=\"mod.enabled\">{{ mod.enabled ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n </div>\n <div class=\"module-title\">{{ mod.title }}</div>\n <div class=\"module-description\">{{ mod.description }}</div>\n <span class=\"module-chip\" *ngIf=\"mod.core\">Always on</span>\n </div>\n </div>\n </div>\n <div class=\"module-actions\" *ngIf=\"!modulesConfirmed\">\n <button mat-flat-button color=\"primary\" [disabled]=\"togglingKey !== ''\" (click)=\"confirmModules()\">Confirm selection</button>\n <span class=\"module-hint\">Happy with this selection? Confirm it to complete the step below.</span>\n </div>\n </mat-card>\n\n <!-- Category groups, top-down \u2014 Changed: flat spa-checklist replaces the raised mat-accordion -->\n <mat-card class=\"setup-group\" *ngFor=\"let group of groups\">\n <div class=\"group-header\">\n <h2>{{ group.name }}</h2>\n <span class=\"group-counter\">{{ group.completed }} of {{ group.total }}</span>\n </div>\n <spa-checklist [config]=\"group.config\" [itemTemplate]=\"stepExtrasTpl\"></spa-checklist>\n </mat-card>\n\n <!-- Demo data (opt-in via setupConfig.demoData) \u2014 moved here off the per-app configuration pages -->\n <mat-card class=\"setup-group setup-demo\" *ngIf=\"showDemoData\">\n <div class=\"group-header\">\n <h2>Demo data</h2>\n <span class=\"group-counter\">Explore with example records, then clear them out</span>\n </div>\n <p class=\"demo-description\">Seeding fills the system with example records so you can try it out before capturing anything real. Removing deletes only those example records.</p>\n <div class=\"demo-actions\">\n <button mat-flat-button color=\"primary\" [disabled]=\"demoBusy\" (click)=\"seedDemoData()\"><mat-icon>add_circle</mat-icon> Seed demo data</button>\n <button mat-stroked-button color=\"warn\" [disabled]=\"demoBusy\" (click)=\"removeDemoData()\"><mat-icon>delete</mat-icon> Remove demo data</button>\n </div>\n </mat-card>\n\n</div>\n\n<!-- Projected into the expanded checklist body: predefined roles picker on the roles step (v2) -->\n<ng-template #stepExtrasTpl let-item>\n <div class=\"role-templates\" *ngIf=\"hasRoleTemplates(item.data) && roleTemplates.length > 0\">\n <div class=\"role-template\" *ngFor=\"let tpl of roleTemplates\" [class.created]=\"tpl.exists\" [class.selected]=\"!tpl.exists && selectedTemplates[tpl.key]\" (click)=\"toggleTemplate(tpl)\">\n <div class=\"role-head\">\n <mat-icon class=\"role-state\" [class.on]=\"tpl.exists || selectedTemplates[tpl.key]\">{{ (tpl.exists || selectedTemplates[tpl.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"role-name\">{{ tpl.name }}</span>\n <span class=\"role-created\" *ngIf=\"tpl.exists\"><mat-icon>check</mat-icon>Created</span>\n </div>\n <div class=\"role-description\">{{ tpl.description }}</div>\n </div>\n </div>\n\n <!-- Added (presets): the preset picker. Same row as the role picker above \u2014 same state icon, same 8px stack,\n same colours \u2014 plus ONE sentence and ONE control.\n Changed (presets P4): rendered through ONE shared template for both the notification step and the sign-off\n step, parameterised by the picker object. The two sit one above the other on the same page, so a second\n near-identical block would drift and every difference would read as a mistake. -->\n <ng-container *ngIf=\"pickerFor(item.data) as picker\">\n <ng-container *ngTemplateOutlet=\"presetPickerTpl; context: { $implicit: picker }\"></ng-container>\n </ng-container>\n</ng-template>\n\n<!-- The one picker body. `picker` carries the domain: its rows, its roles, its wording, its apply call. -->\n<ng-template #presetPickerTpl let-picker>\n <div class=\"preset-picker\" *ngIf=\"picker.loaded && picker.presets.length > 0\">\n\n <!-- No selectable role \u2192 we ask for one instead of offering ticks that would build an inert rule. The wording\n is the picker's own, because \"no roles\" and \"roles nobody is in\" are different situations. -->\n <div class=\"preset-empty\" *ngIf=\"picker.roles.length === 0\">{{ picker.emptyText }}</div>\n\n <ng-container *ngIf=\"picker.roles.length > 0\">\n <div class=\"preset-section\" *ngFor=\"let pgroup of picker.groups\">\n <div class=\"preset-section-head\">\n <span class=\"preset-section-name\">{{ pgroup.name }}</span>\n <span class=\"preset-section-count\">{{ pgroup.on }} of {{ pgroup.total }}</span>\n </div>\n <div class=\"preset-row\" *ngFor=\"let row of pgroup.rows\" role=\"checkbox\" tabindex=\"0\"\n [class.configured]=\"row.preset.exists\" [class.selected]=\"!row.preset.exists && picker.selected[row.preset.key]\"\n [attr.aria-checked]=\"row.preset.exists || !!picker.selected[row.preset.key]\" [attr.aria-disabled]=\"row.preset.exists\"\n (click)=\"togglePreset(picker, row.preset)\"\n (keydown.enter)=\"togglePreset(picker, row.preset)\"\n (keydown.space)=\"togglePreset(picker, row.preset); $event.preventDefault()\">\n <div class=\"preset-head\">\n <mat-icon class=\"preset-state\" [class.on]=\"row.preset.exists || picker.selected[row.preset.key]\">{{ (row.preset.exists || picker.selected[row.preset.key]) ? 'check_circle' : 'radio_button_unchecked' }}</mat-icon>\n <span class=\"preset-name\">{{ row.preset.name }}</span>\n <span class=\"preset-configured\" *ngIf=\"row.preset.exists\"><mat-icon>check</mat-icon>Configured</span>\n </div>\n <div class=\"preset-description\">{{ row.preset.description }}</div>\n <!-- The effect sentence comes from the server whole; `before` and `after` are its two halves either side\n of the {role} placeholder, so reading the line IS reading the configuration. The sign-off sentences\n carry their own deferred-notification caveat, which is why there is no extra note here. -->\n <div class=\"preset-effect\" *ngIf=\"!row.preset.exists\">{{ row.before }}<select class=\"preset-role\" [attr.aria-label]=\"picker.roleAria + row.preset.name\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n (change)=\"setPresetRole(picker, row.preset, $any($event.target).value)\">\n <option *ngFor=\"let role of picker.roles\" [value]=\"role.value\" [selected]=\"role.value === picker.role[row.preset.key]\">{{ role.name }}</option>\n </select>{{ row.after }}</div>\n <div class=\"preset-effect preset-done\" *ngIf=\"row.preset.exists\">{{ picker.doneText }}</div>\n </div>\n </div>\n\n <!-- One page-level channel choice, notifications only. In-app is the only channel that always works; SMS is a\n stub and is never offered; an approval request is always in-app, so the sign-off picker has no such line. -->\n <div class=\"preset-email\" *ngIf=\"picker.showEmail\" role=\"checkbox\" tabindex=\"0\" [attr.aria-checked]=\"includeEmail\"\n (click)=\"toggleIncludeEmail()\"\n (keydown.enter)=\"toggleIncludeEmail()\"\n (keydown.space)=\"toggleIncludeEmail(); $event.preventDefault()\">\n <mat-icon class=\"preset-state\" [class.on]=\"includeEmail\">{{ includeEmail ? 'check_box' : 'check_box_outline_blank' }}</mat-icon>\n <span class=\"preset-email-label\">Also send these by email</span>\n <span class=\"preset-email-hint\">In-app always. Email needs your mail settings.</span>\n </div>\n </ng-container>\n </div>\n</ng-template>\n\n<!-- Graceful empty state (feature disabled or status unavailable) -->\n<div class=\"setup-empty\" *ngIf=\"!status\">\n <mat-icon>rocket_launch</mat-icon>\n <p>Setup status is not available yet.</p>\n</div>\n", styles: [".setup-page{max-width:860px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:16px}.setup-hero{padding:24px}.hero-content{display:flex;align-items:center;justify-content:space-between;gap:24px;flex-wrap:wrap}.hero-text h1{margin:0 0 4px;font-size:24px}.hero-text p{margin:0 0 8px;color:#0009}.hero-counter{font-size:13px;color:#0009}.hero-progress{flex:1;min-width:220px;max-width:340px}.hero-percent{display:block;font-size:28px;font-weight:600;color:#2e7d32;margin-bottom:6px;text-align:right}.hero-progress mat-progress-bar{height:10px;border-radius:5px}.setup-hero.celebrate{background:linear-gradient(135deg,#e8f5e9,#f1f8e9)}.celebration{justify-content:flex-start}.celebrate-icon{font-size:48px;width:48px;height:48px;color:#2e7d32;animation:celebrate-pop .6s ease-out}@keyframes celebrate-pop{0%{transform:scale(.3);opacity:0}70%{transform:scale(1.15)}to{transform:scale(1);opacity:1}}.setup-group{padding:16px}.group-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.group-header h2{margin:0;font-size:17px}.group-counter{font-size:12px;color:#0000008c}.setup-modules{padding:16px}.module-section+.module-section{margin-top:18px}.module-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.module-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.module-section-count{font-size:11px;color:#00000073;white-space:nowrap}.module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;margin-top:8px}.module-card{border:1px solid rgba(0,0,0,.12);border-radius:10px;padding:12px;cursor:pointer;transition:border-color .15s,background .15s,opacity .15s;opacity:.72;position:relative}.module-card:hover{border-color:#90a4ae}.module-card.enabled{border-color:#4caf50;background:#f6fbf6;opacity:1}.module-card.core{cursor:default}.module-card.busy{pointer-events:none;opacity:.5}.module-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.module-icon{color:#546e7a}.module-card.enabled .module-icon{color:#2e7d32}.module-state{color:#b0bec5}.module-state.on{color:#4caf50}.module-title{font-weight:600;font-size:14px;margin-bottom:4px}.module-description{font-size:12px;color:#0009;min-height:30px}.module-chip{display:inline-block;margin-top:8px;background:#eceff1;color:#546e7a;border-radius:12px;padding:2px 10px;font-size:11px}.module-actions{display:flex;align-items:center;gap:8px 12px;margin-top:14px;flex-wrap:wrap}.module-actions button{flex-shrink:0}.module-hint{font-size:12px;color:#0000008c}.role-templates{display:flex;flex-direction:column;gap:8px;margin:0 0 12px}.role-template{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.role-template:hover{border-color:#90a4ae}.role-template.selected{border-color:#4caf50;background:#f6fbf6}.role-template.created{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.role-head{display:flex;align-items:center;gap:8px}.role-state{color:#b0bec5;flex-shrink:0}.role-state.on{color:#4caf50}.role-name{font-weight:500;font-size:13px}.role-created{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32}.role-created mat-icon{font-size:16px;width:16px;height:16px}.role-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-picker{display:flex;flex-direction:column;gap:14px;margin:0 0 12px}.preset-empty{font-size:12px;line-height:18px;color:#0009;max-width:62ch}.preset-section{display:flex;flex-direction:column;gap:8px}.preset-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,.08)}.preset-section-name{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#0009}.preset-section-count{font-size:11px;color:#00000073;white-space:nowrap}.preset-row{border:1px solid rgba(0,0,0,.08);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .15s,background .15s}.preset-row:hover{border-color:#90a4ae}.preset-row:focus-visible{outline:2px solid #4caf50;outline-offset:2px}.preset-row.selected{border-color:#4caf50;background:#f6fbf6}.preset-row.configured{background:#f6fbf6;border-color:#c8e6c9;cursor:default}.preset-head{display:flex;align-items:center;gap:8px}.preset-state{color:#b0bec5;flex-shrink:0}.preset-state.on{color:#4caf50}.preset-name{font-weight:500;font-size:13px}.preset-configured{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#2e7d32;white-space:nowrap}.preset-configured mat-icon{font-size:16px;width:16px;height:16px}.preset-description{font-size:12px;color:#0009;margin:2px 0 0 32px}.preset-effect{font-size:12px;line-height:22px;color:#000000c7;margin:4px 0 0 32px}.preset-done{color:#2e7d32}.preset-role{font-family:inherit;font-size:12px;font-weight:500;color:#2e7d32;background:#fff;border:1px solid #c8e6c9;border-radius:6px;padding:2px 4px;margin:0 2px;max-width:100%;cursor:pointer}.preset-role:hover{border-color:#4caf50}.preset-role:focus-visible{outline:2px solid #4caf50;outline-offset:1px}.preset-email{display:flex;align-items:center;gap:8px;flex-wrap:wrap;cursor:pointer;padding:2px 0}.preset-email:focus-visible{outline:2px solid #4caf50;outline-offset:2px;border-radius:6px}.preset-email-label{font-size:13px;font-weight:500}.preset-email-hint{font-size:12px;color:#0000008c}.setup-empty{text-align:center;padding:48px 16px;color:#00000080}.setup-empty mat-icon{font-size:40px;width:40px;height:40px}.demo-description{margin:8px 0 14px;font-size:13px;color:#0009}.demo-actions{display:flex;gap:12px;flex-wrap:wrap}@media (max-width: 700px){.setup-page{padding:10px 0;gap:12px}.setup-hero{padding:12px}.setup-modules,.setup-group{padding:10px}.hero-content{gap:10px}.hero-text h1{font-size:20px}.setup-hero:not(.celebrate) .hero-text p{display:none}.hero-counter{font-size:12px}.hero-progress{min-width:0;max-width:none}.hero-percent{font-size:22px;margin-bottom:4px}.hero-progress mat-progress-bar{height:8px;border-radius:4px}.celebrate-icon{font-size:36px;width:36px;height:36px}.group-header{margin-bottom:6px;gap:2px 8px}.group-header h2{font-size:15px}.group-counter{font-size:11px}.setup-modules>.group-header,.setup-demo>.group-header{flex-wrap:wrap}.setup-modules>.group-header .group-counter,.setup-demo>.group-header .group-counter{flex:1 0 100%}.module-section+.module-section{margin-top:12px}.module-section-head{padding-bottom:4px}.module-grid{gap:8px;margin-top:6px}.module-card{padding:10px}.module-head{margin-bottom:4px}.module-title{font-size:13.5px;margin-bottom:2px}.module-description{min-height:0}.module-chip{margin-top:6px}.module-actions{margin-top:10px}.role-templates,.preset-picker{gap:6px;margin-bottom:8px}.role-template,.preset-row{padding:8px 10px}.role-description,.preset-description,.preset-effect{margin-left:0}.preset-section{gap:6px}.preset-section-head{padding-bottom:4px}.demo-description{margin:6px 0 10px;font-size:12.5px}.demo-actions{gap:8px}.setup-empty{padding:28px 12px}.module-hint{flex:0 0 100%}.preset-role{display:block;width:100%;margin:4px 0 0;padding:6px 8px}.preset-email-hint{flex:0 0 100%}}\n"] }]
|
|
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"] }]
|
|
12523
12859
|
}] });
|
|
12524
12860
|
|
|
12525
12861
|
// Quiet Loading — perceived-progress engine (FSD D1).
|
|
@@ -13967,21 +14303,11 @@ class TextMultiComponent {
|
|
|
13967
14303
|
this.loadIndicator = new FieldLoadIndicator();
|
|
13968
14304
|
this.hoverChange = new EventEmitter();
|
|
13969
14305
|
} // Changed: injected ApiErrorService so a failed option load is no longer silent
|
|
13970
|
-
// Added: identical normalisation to SelectMultiComponent.toValueList, deliberately kept the same shape and
|
|
13971
|
-
// name so the two multi-value components cannot drift. `value` is documented as a ';'-delimited string, but
|
|
13972
|
-
// the truthiness guard below used to hand anything truthy straight to .split() — an array (any form whose
|
|
13973
|
-
// backend DTO takes a List<int> rewrites the bound property in place before posting) or a bare scalar both
|
|
13974
|
-
// threw "split is not a function". select-multi hit exactly that in piglet; nothing feeds text-multi an array
|
|
13975
|
-
// today, so this is hardening ahead of the first caller that does, not a repair.
|
|
13976
|
-
toValueList(raw) {
|
|
13977
|
-
if (Array.isArray(raw))
|
|
13978
|
-
return raw.filter(v => v !== null && v !== undefined && String(v).trim() !== '').map(v => String(v).trim()); // Added: the List<int> shape
|
|
13979
|
-
if (!raw)
|
|
13980
|
-
return []; // Added: preserves the previous falsy-means-nothing-entered rule for null/undefined/''
|
|
13981
|
-
return String(raw).split(';').filter(v => v.trim() !== '').map(v => v.trim()); // Changed: String() first, so a bare scalar no longer throws either
|
|
13982
|
-
}
|
|
13983
14306
|
ngOnInit() {
|
|
13984
|
-
|
|
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);
|
|
13985
14311
|
this.setupAutoComplete();
|
|
13986
14312
|
this.getData(this.loadAction);
|
|
13987
14313
|
}
|
|
@@ -14209,21 +14535,11 @@ class SelectMultiComponent {
|
|
|
14209
14535
|
}
|
|
14210
14536
|
this.initializeValues();
|
|
14211
14537
|
}
|
|
14212
|
-
// Added: `value` is documented as a ';'-delimited string, but real callers legitimately hold an array —
|
|
14213
|
-
// any form whose backend DTO takes a List<int> rewrites the bound property in place before posting
|
|
14214
|
-
// (piglet has four: wean, sell, move, treat). That array flowed straight back down [(value)] and
|
|
14215
|
-
// this.value.split(';') threw "split is not a function" on every subsequent change-detection pass.
|
|
14216
|
-
// Normalise on the way in rather than type-guarding at one call site, and keep emitting the ';' string
|
|
14217
|
-
// on the way out so no existing consumer's parsing changes.
|
|
14218
|
-
toValueList(raw) {
|
|
14219
|
-
if (Array.isArray(raw))
|
|
14220
|
-
return raw.filter(v => v !== null && v !== undefined && String(v).trim() !== '').map(v => String(v).trim()); // Added: the List<int> shape
|
|
14221
|
-
if (!raw)
|
|
14222
|
-
return []; // Added: preserves the previous falsy-means-nothing-selected rule for null/undefined/''
|
|
14223
|
-
return String(raw).split(';').filter(v => v.trim() !== '').map(v => v.trim()); // Changed: String() first, so a bare scalar (a single id) no longer throws either
|
|
14224
|
-
}
|
|
14225
14538
|
initializeValues() {
|
|
14226
|
-
|
|
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);
|
|
14227
14543
|
if (requested.length > 0) { // Changed: gate on the parsed request, not on truthiness of a raw value that may be an array
|
|
14228
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
|
|
14229
14545
|
this.selectedValues = values;
|
|
@@ -15643,6 +15959,33 @@ class TableRowComponent {
|
|
|
15643
15959
|
showBanner(message) {
|
|
15644
15960
|
this.showBannerEvent.emit(message);
|
|
15645
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
|
+
}
|
|
15646
15989
|
textDisplayed(row, column) {
|
|
15647
15990
|
let txt = row[column.name];
|
|
15648
15991
|
let max = !this.smallScreen ? column?.maxLength : column?.maxLength / 2;
|
|
@@ -15660,11 +16003,11 @@ class TableRowComponent {
|
|
|
15660
16003
|
return false;
|
|
15661
16004
|
}
|
|
15662
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 }); }
|
|
15663
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TableRowComponent, isStandalone: false, selector: "app-table-row", inputs: { column: "column", row: "row", config: "config", smallScreen: "smallScreen" }, outputs: { actionClick: "actionClick", columnClick: "columnClick", showBannerEvent: "showBannerEvent" }, usesOnChanges: true, ngImport: i0, template: "<ng-container [ngSwitch]=\"column.type\">\n <ng-container *ngSwitchCase=\"'checkbox'\">\n <spa-check [value]=\"row[column.name]\" [readonly]=\"true\"></spa-check>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'select'\">\n <spa-select-lite [options]=\"column.options\" [optionDisplay]=\"column.optionDisplay\" [optionValue]=\"column.optionValue\" [(value)]=\"row[column.name]\" width=\"90%\"></spa-select-lite>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'chip'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'background-color': vm.color || '#eceff1', 'color': 'rgba(0, 0, 0, 0.87)', 'border': 'none'}\">{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'icon'\">\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date'\">\n {{row[column.name] | date : 'dd/MM/yyyy'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date-short'\">\n {{row[column.name] | date : 'd MMM'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetime'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetimesec'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm:ss'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'money'\">\n <label [ngStyle]=\"{'color': vm.color }\">{{row[column.name] | currency:'':''}}</label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <!-- Added: monogram \u2014 the row still holds the full name, so filter/sort keep working on the underlying value -->\n <ng-container *ngSwitchCase=\"'monogram'\">\n <spa-monogram [value]=\"row[column.name]\" [key]=\"column.keyField ? row[column.keyField] : null\" (tapped)=\"showBanner($event)\"></spa-monogram>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'button'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'color': vm.color}\" >{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchDefault>\n <label [ngStyle]=\"{'color': vm.color }\">\n <ng-container *ngIf=\"column.type === 'number'\">\n {{row[column.name] | number:'1.0-2'}}\n </ng-container>\n <ng-container *ngIf=\"column.type !== 'number'\">\n {{vm.text}}\n </ng-container>\n </label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.truncated\" matTooltip='Show more' matTooltipPosition=\"above\" (click)=\"showBanner(row[column.name])\">more_horiz</mat-icon>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n</ng-container>\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px;vertical-align:middle}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#e5e5e5}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.mat-mdc-cell .mat-mdc-outlined-button,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button){height:auto!important;min-height:36px!important;white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important;line-height:1.4!important;text-align:left!important;padding:6px 16px!important}.mat-mdc-cell .mat-mdc-outlined-button .mdc-button__label,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button) .mdc-button__label{white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i1$2.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1$2.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1$2.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage", "hint"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: MonogramComponent, selector: "spa-monogram", inputs: ["value", "key"], outputs: ["tapped"] }, { kind: "component", type: SelectLiteComponent, selector: "spa-select-lite" }, { kind: "pipe", type: i1$2.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$2.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
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 }); }
|
|
15664
16007
|
}
|
|
15665
16008
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableRowComponent, decorators: [{
|
|
15666
16009
|
type: Component,
|
|
15667
|
-
args: [{ selector: 'app-table-row', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<ng-container [ngSwitch]=\"column.type\">\n <ng-container *ngSwitchCase=\"'checkbox'\">\n <spa-check [value]=\"row[column.name]\" [readonly]=\"true\"></spa-check>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'select'\">\n <spa-select-lite [options]=\"column.options\" [optionDisplay]=\"column.optionDisplay\" [optionValue]=\"column.optionValue\" [(value)]=\"row[column.name]\" width=\"90%\"></spa-select-lite>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'chip'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'background-color': vm.color || '#eceff1', 'color': 'rgba(0, 0, 0, 0.87)', 'border': 'none'}\">{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'icon'\">\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date'\">\n {{row[column.name] | date : 'dd/MM/yyyy'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'date-short'\">\n {{row[column.name] | date : 'd MMM'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetime'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'datetimesec'\">\n {{row[column.name] | date : 'dd/MM/yyyy HH:mm:ss'}}\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'money'\">\n <label [ngStyle]=\"{'color': vm.color }\">{{row[column.name] | currency:'':''}}</label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <!-- Added: monogram \u2014 the row still holds the full name, so filter/sort keep working on the underlying value -->\n <ng-container *ngSwitchCase=\"'monogram'\">\n <spa-monogram [value]=\"row[column.name]\" [key]=\"column.keyField ? row[column.keyField] : null\" (tapped)=\"showBanner($event)\"></spa-monogram>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'button'\">\n <button mat-stroked-button (click)=\"onColumnClick(column, row)\" [ngStyle]=\"{'color': vm.color}\" >{{row[column.name]}}</button>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n\n <ng-container *ngSwitchDefault>\n <label [ngStyle]=\"{'color': vm.color }\">\n <ng-container *ngIf=\"column.type === 'number'\">\n {{row[column.name] | number:'1.0-2'}}\n </ng-container>\n <ng-container *ngIf=\"column.type !== 'number'\">\n {{vm.text}}\n </ng-container>\n </label>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.truncated\" matTooltip='Show more' matTooltipPosition=\"above\" (click)=\"showBanner(row[column.name])\">more_horiz</mat-icon>\n <mat-icon class=\"col-icon\" *ngIf=\"vm.showColumnIcon\" [matTooltip]=\"row[column.icon.tipField] ?? column.icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[column.icon.tipField])\" [style.color]=\"column.icon?.color\">{{column.icon.name }}</mat-icon>\n <ng-container *ngFor=\"let icon of vm.visibleIcons\">\n <mat-icon class=\"col-icon\" [matTooltip]=\"row[icon.tipField] ?? icon?.tip\" matTooltipPosition=\"above\" (click)=\"showBanner(row[icon.tipField])\" [style.color]=\"icon.color\">{{icon.name }}</mat-icon>\n </ng-container>\n </ng-container>\n</ng-container>\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px;vertical-align:middle}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#e5e5e5}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.mat-mdc-cell .mat-mdc-outlined-button,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button){height:auto!important;min-height:36px!important;white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important;line-height:1.4!important;text-align:left!important;padding:6px 16px!important}.mat-mdc-cell .mat-mdc-outlined-button .mdc-button__label,.mat-mdc-cell .mat-mdc-button-base:not(.mat-mdc-icon-button) .mdc-button__label{white-space:normal!important;word-break:break-word!important;overflow-wrap:break-word!important}\n"] }]
|
|
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"] }]
|
|
15668
16011
|
}], ctorParameters: () => [{ type: ButtonService }], propDecorators: { column: [{
|
|
15669
16012
|
type: Input
|
|
15670
16013
|
}], row: [{
|
|
@@ -16691,7 +17034,11 @@ class EmailComponent {
|
|
|
16691
17034
|
Validators.required,
|
|
16692
17035
|
Validators.pattern('^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$')
|
|
16693
17036
|
]);
|
|
16694
|
-
|
|
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);
|
|
16695
17042
|
// Set up autocomplete filtering
|
|
16696
17043
|
this.filteredOptions = this.emailControl.valueChanges.pipe(startWith(''), map(value => this._filter(value)));
|
|
16697
17044
|
}
|
|
@@ -17362,6 +17709,7 @@ class TableComponent {
|
|
|
17362
17709
|
this.apiErrorService = apiErrorService;
|
|
17363
17710
|
this.runtimeConfig = runtimeConfig;
|
|
17364
17711
|
this.subs = []; // TS-8: breakpoint + parent-owned reload subscriptions leaked on table teardown; stacked reload subs also caused duplicate loadData fetches
|
|
17712
|
+
this.initialized = false; // Added: guards ensureInitialized — initialization must happen exactly once, whichever hook reaches it first
|
|
17365
17713
|
this.elevation = "mat-elevation-z5";
|
|
17366
17714
|
this.actionsWidth = "50px";
|
|
17367
17715
|
// Added: collapsible flat section header (sectionConfig) — Day Book sections
|
|
@@ -17465,6 +17813,17 @@ class TableComponent {
|
|
|
17465
17813
|
}));
|
|
17466
17814
|
}
|
|
17467
17815
|
ngOnInit() {
|
|
17816
|
+
this.ensureInitialized(); // Changed: the body moved to ensureInitialized so ngOnChanges can pull it forward when it has to start a load first
|
|
17817
|
+
}
|
|
17818
|
+
// Added: Angular runs ngOnChanges BEFORE ngOnInit on the first pass. A lazy tab that is already active on the
|
|
17819
|
+
// first binding (a details dialog's tableConfigs[0]) therefore started its load from ngOnChanges while
|
|
17820
|
+
// setupPagination() had not yet run, so pagedMode was still false and the rows landed on the legacy path;
|
|
17821
|
+
// ngOnInit then flipped pagedMode on and the next updateSlice() drew from the empty loadedRows window and
|
|
17822
|
+
// replaced the good rows with nothing. Initialization is now idempotent so the load site can order it first.
|
|
17823
|
+
ensureInitialized() {
|
|
17824
|
+
if (this.initialized)
|
|
17825
|
+
return;
|
|
17826
|
+
this.initialized = true;
|
|
17468
17827
|
this.sectionCollapsed = !!this.config?.sectionConfig?.collapsed; // Added: seed section collapse from config — state lives on the component, never written back to the shared config
|
|
17469
17828
|
if (this.config?.formConfig) {
|
|
17470
17829
|
this.hasFormAccess = Core.hasFormAccess(this.config.formConfig, this.authService.currentRoleSource.value);
|
|
@@ -17487,6 +17846,7 @@ class TableComponent {
|
|
|
17487
17846
|
}
|
|
17488
17847
|
if (this.inTab && changes['activeTab']) {
|
|
17489
17848
|
if (!this.hasBeenActivated && this.activeTab && this.config?.lazyLoad && this.config.loadAction) {
|
|
17849
|
+
this.ensureInitialized(); // Changed: this is the ONLY load ngOnChanges can start, and it must not run ahead of setupPagination() — see ensureInitialized
|
|
17490
17850
|
this.loadData(this.config.loadAction, "");
|
|
17491
17851
|
this.hasBeenActivated = true;
|
|
17492
17852
|
}
|
|
@@ -17645,7 +18005,12 @@ class TableComponent {
|
|
|
17645
18005
|
// A DERIVED name comes straight off a URL segment, which deriveEntityName capitalises and singularises for
|
|
17646
18006
|
// SignalR ("product-groups" -> "Product-group"). That is right for an entity key and wrong inside a
|
|
17647
18007
|
// sentence — the caption read "Loading Product-group…". De-hyphenate and lowercase it so it reads as prose.
|
|
17648
|
-
// 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.
|
|
17649
18014
|
const derived = this.deriveEntityName();
|
|
17650
18015
|
return derived ? derived.replace(/[-_]+/g, ' ').toLowerCase() : 'data';
|
|
17651
18016
|
}
|
|
@@ -17864,6 +18229,7 @@ class TableComponent {
|
|
|
17864
18229
|
this.displayedColumns = this.tableConfigService.setColumns(this.config, this.smallScreen);
|
|
17865
18230
|
this.displayedButtons = this.tableConfigService.getDisplayedButtons(this.config?.buttons, this.smallScreen, this.config);
|
|
17866
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)
|
|
17867
18233
|
// Added: the same defect through the resize door. A row opened for edit on a wide screen keeps its seeded
|
|
17868
18234
|
// editingModel when the window narrows past 600px, but its cells vanish with the columns — so the tick would
|
|
17869
18235
|
// post values the operator can no longer see. Discard the edit instead; cancel has always been non-destructive.
|
|
@@ -18360,7 +18726,14 @@ class TableComponent {
|
|
|
18360
18726
|
//Dialog events
|
|
18361
18727
|
columnClicked(column, row) {
|
|
18362
18728
|
if (column.detailsConfig) {
|
|
18363
|
-
|
|
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
|
|
18364
18737
|
this.open(button, row);
|
|
18365
18738
|
}
|
|
18366
18739
|
else {
|
|
@@ -18471,11 +18844,12 @@ class TableComponent {
|
|
|
18471
18844
|
}
|
|
18472
18845
|
return;
|
|
18473
18846
|
}
|
|
18474
|
-
|
|
18475
|
-
|
|
18476
|
-
|
|
18477
|
-
|
|
18478
|
-
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) => {
|
|
18479
18853
|
if (result == "yes") {
|
|
18480
18854
|
this.actionClickedEmit('delete', row);
|
|
18481
18855
|
this.doAction(actionButton.name, row);
|
|
@@ -18493,8 +18867,11 @@ class TableComponent {
|
|
|
18493
18867
|
row[this.config.heroField] = this.config.heroValue;
|
|
18494
18868
|
}
|
|
18495
18869
|
if (b.confirm && b.name != "delete") {
|
|
18496
|
-
|
|
18497
|
-
|
|
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) => {
|
|
18498
18875
|
if (result == "yes") {
|
|
18499
18876
|
this.execAction(b, row);
|
|
18500
18877
|
}
|
|
@@ -18548,13 +18925,17 @@ class TableComponent {
|
|
|
18548
18925
|
return "mat-elevation-z5";
|
|
18549
18926
|
}
|
|
18550
18927
|
}
|
|
18551
|
-
// Changed:
|
|
18928
|
+
// Changed: this now ALWAYS refreshes. The old body refreshed only when realTime was OFF or SignalR was DOWN,
|
|
18929
|
+
// so a healthy real-time table did nothing after a write and simply waited for a broadcast. But the SignalR
|
|
18930
|
+
// streams below are filtered on this table's own entityName, and an action that writes a DIFFERENT entity
|
|
18931
|
+
// (invoicing a rental writes an Invoice) never emits a matching one — grip's rentals grid kept rendering
|
|
18932
|
+
// "Returned" for a row already Invoiced in the database. Nothing correlates a broadcast back to the action
|
|
18933
|
+
// just performed, so "refresh unless a broadcast arrives" is not implementable without new machinery.
|
|
18934
|
+
// A same-URL re-read after a user-initiated write is idempotent; a broadcast that does arrive patches the same rows.
|
|
18552
18935
|
// In paged mode this is NOT a full reload — refreshClicked routes through loadDataPaged's same-URL branch,
|
|
18553
18936
|
// which re-fetches only the loaded window (skip=0, take=loadedCount), never the whole dataset.
|
|
18554
18937
|
realTimeRefreshOrFallback() {
|
|
18555
|
-
if (!this.effRealTime || !this.isSignalRConnected)
|
|
18556
|
-
this.refreshClicked();
|
|
18557
|
-
}
|
|
18938
|
+
this.refreshClicked(); // Changed: unconditional — was `if (!this.effRealTime || !this.isSignalRConnected)`, which made the healthy real-time case a silent no-op
|
|
18558
18939
|
}
|
|
18559
18940
|
//---------------- TinSync offline support ----------------
|
|
18560
18941
|
// Registers the table's URLs with the offline service, starts the sync engine, and wires live overlay updates
|
|
@@ -18617,6 +18998,55 @@ class TableComponent {
|
|
|
18617
18998
|
} // reuse the already-injected column instead of adding another
|
|
18618
18999
|
this.config.columns = [...this.config.columns, { name: '_offlineStatus', type: 'icon', alias: 'Sync', icons: statusIcons }]; // last data column -> renders just before Action
|
|
18619
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
|
+
}
|
|
18620
19050
|
// Captures the raw server rows and merges pending ops: creates prepend, edits patch by id, deletes hide
|
|
18621
19051
|
applyOfflineOverlay(rows) {
|
|
18622
19052
|
if (!this.offlineEntry || !Array.isArray(rows))
|
|
@@ -18776,6 +19206,25 @@ class TableComponent {
|
|
|
18776
19206
|
if (!segment)
|
|
18777
19207
|
return '';
|
|
18778
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')
|
|
18779
19228
|
return name.endsWith('s') ? name.slice(0, -1) : name; // Remove trailing 's' for singular
|
|
18780
19229
|
}
|
|
18781
19230
|
// Changed: Find the ID property key in a row object (convention: ends with 'ID')
|
|
@@ -19044,7 +19493,7 @@ class DayBookComponent {
|
|
|
19044
19493
|
const stages = (this.config?.stages || []).filter(s => Core.isItemVisible(s, this.book));
|
|
19045
19494
|
const tiles = stages.map(s => ({
|
|
19046
19495
|
name: s.countField, alias: s.label, style: 'icon', icon: s.icon || 'checklist', info: s.hint,
|
|
19047
|
-
|
|
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.
|
|
19048
19497
|
color: Core.isItemVisible({ visible: s.alert }, this.book) && s.alert !== undefined ? '#c62828' : undefined,
|
|
19049
19498
|
action: { method: 'get', url: '' }, // never fired — clickable tiles emit tileClick, which we map to a scroll
|
|
19050
19499
|
hidden: (d) => !d?.[s.countField] || !this.inLane(s.lane), // Changed: lane filter lives in the predicate so pill clicks re-filter tiles live
|
|
@@ -19054,10 +19503,9 @@ class DayBookComponent {
|
|
|
19054
19503
|
// stages — more than any row can show — so one scrolling row beats either squeezing them or stacking
|
|
19055
19504
|
// them down the page above the lists. This is a Day Book decision made once in the library rather than
|
|
19056
19505
|
// repeated in five app configs; `stageTiles` still lets an app override it, including back to 'auto'.
|
|
19057
|
-
//
|
|
19058
|
-
//
|
|
19059
|
-
|
|
19060
|
-
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
|
|
19061
19509
|
// Added: counts resolved onto a copy of the book, so a countField naming a LIST still shows a number
|
|
19062
19510
|
this.tileData = this.book ? { ...this.book, ...stages.reduce((a, s) => { a[s.countField] = this.stageCount(s); return a; }, {}) } : null;
|
|
19063
19511
|
}
|
|
@@ -20085,7 +20533,11 @@ class FormComponent {
|
|
|
20085
20533
|
return;
|
|
20086
20534
|
}
|
|
20087
20535
|
if (button.confirm) {
|
|
20088
|
-
|
|
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) => {
|
|
20089
20541
|
if (result == "yes") {
|
|
20090
20542
|
this.processCall(button);
|
|
20091
20543
|
}
|
|
@@ -21599,6 +22051,43 @@ const featureGuard = (featureKey) => {
|
|
|
21599
22051
|
};
|
|
21600
22052
|
};
|
|
21601
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
|
+
|
|
21602
22091
|
// Structural directive for plan-based feature gating in templates
|
|
21603
22092
|
// Usage: *spaFeature="'sales.quotes'" — hides element if feature is disabled
|
|
21604
22093
|
class FeatureDirective {
|
|
@@ -22105,8 +22594,18 @@ class NavMenuComponent {
|
|
|
22105
22594
|
if (!this.smallScreen) {
|
|
22106
22595
|
this.isExpanded = true;
|
|
22107
22596
|
}
|
|
22108
|
-
|
|
22109
|
-
|
|
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
|
+
});
|
|
22110
22609
|
// Added: start recording the visited page for resumeLastRoute. The nav shell only exists while signed in,
|
|
22111
22610
|
// so this one subscription covers every in-app navigation without touching individual pages.
|
|
22112
22611
|
this.lastRouteService.start();
|
|
@@ -23093,13 +23592,20 @@ class DetailsDialog {
|
|
|
23093
23592
|
}
|
|
23094
23593
|
else {
|
|
23095
23594
|
this.settleRead(quiet, () => {
|
|
23096
|
-
// Changed: TinSync — if a by-action load fails offline, fall back to the row the table already passed in so existing records stay editable
|
|
23097
|
-
|
|
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) {
|
|
23098
23602
|
this.details = this.detailsConfig.details;
|
|
23099
23603
|
this.isLoadComplete = true;
|
|
23100
23604
|
}
|
|
23101
23605
|
else {
|
|
23102
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
|
|
23103
23609
|
}
|
|
23104
23610
|
if (this.autoRefreshEnabled) {
|
|
23105
23611
|
this.autoRefreshEnabled = false;
|
|
@@ -23195,11 +23701,10 @@ class DetailsDialog {
|
|
|
23195
23701
|
const button = this.buttonService.getButton(this.buttons, 'delete');
|
|
23196
23702
|
if (!button)
|
|
23197
23703
|
return;
|
|
23198
|
-
|
|
23199
|
-
|
|
23200
|
-
|
|
23201
|
-
|
|
23202
|
-
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) => {
|
|
23203
23708
|
if (result == "yes") {
|
|
23204
23709
|
this.handleButtonAction('delete');
|
|
23205
23710
|
}
|
|
@@ -23308,11 +23813,12 @@ class DetailsDialog {
|
|
|
23308
23813
|
executeAction(button, data) {
|
|
23309
23814
|
const actionData = this.prepareActionData(button, data);
|
|
23310
23815
|
if (button.confirm) {
|
|
23311
|
-
|
|
23312
|
-
|
|
23313
|
-
|
|
23314
|
-
|
|
23315
|
-
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) => {
|
|
23316
23822
|
if (result == "yes") {
|
|
23317
23823
|
this.performApiCall(button, actionData);
|
|
23318
23824
|
}
|
|
@@ -23508,6 +24014,10 @@ class InvitationsTableComponent {
|
|
|
23508
24014
|
this.messageService = messageService;
|
|
23509
24015
|
this.authService = authService;
|
|
23510
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();
|
|
23511
24021
|
this.tableReload = new Subject();
|
|
23512
24022
|
this.invitationsTableConfig = {
|
|
23513
24023
|
greyOut: (value) => value.accepted == false,
|
|
@@ -23566,12 +24076,14 @@ class InvitationsTableComponent {
|
|
|
23566
24076
|
});
|
|
23567
24077
|
}
|
|
23568
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 }); }
|
|
23569
|
-
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"] }] }); }
|
|
23570
24080
|
}
|
|
23571
24081
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: InvitationsTableComponent, decorators: [{
|
|
23572
24082
|
type: Component,
|
|
23573
|
-
args: [{ selector: 'spa-invitations-table', standalone: false, template: "<spa-table [config]=\"invitationsTableConfig\" (actionClick)=\"invActionClicked($event)\" [reload]=\"tableReload\"></spa-table>\n" }]
|
|
23574
|
-
}], 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
|
+
}] } });
|
|
23575
24087
|
|
|
23576
24088
|
// Generic AI-assisted import wizard: Upload -> Map -> Review -> Done. Config-driven via ImportConfig.
|
|
23577
24089
|
class ImportDialogComponent {
|
|
@@ -26038,6 +26550,15 @@ class RolesComponent {
|
|
|
26038
26550
|
{ value: RoleAccess.Create, name: 'Create' },
|
|
26039
26551
|
{ value: RoleAccess.Full, name: 'Full' }
|
|
26040
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"
|
|
26041
26562
|
this.renameDialogConfig = {
|
|
26042
26563
|
formConfig: {
|
|
26043
26564
|
security: { allow: [this.dataService.capRoles] }, // Added: gate rename role form by roles cap
|
|
@@ -26073,16 +26594,184 @@ class RolesComponent {
|
|
|
26073
26594
|
ngOnInit() {
|
|
26074
26595
|
this.authService.isAuthorised(this.dataService.capRoles.name);
|
|
26075
26596
|
this.loadRoles();
|
|
26076
|
-
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
|
|
26077
26598
|
}
|
|
26078
26599
|
loadRoles() {
|
|
26079
26600
|
this.dataService.GetRole("all", "").subscribe((apiResponse) => {
|
|
26080
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
|
|
26081
26604
|
});
|
|
26082
26605
|
}
|
|
26083
26606
|
refresh() {
|
|
26084
26607
|
this.loadRoles();
|
|
26085
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; }
|
|
26086
26775
|
onCapItemChange(capItem, checked, role) {
|
|
26087
26776
|
return; //disabled to allow sub component access without granting whole menu access
|
|
26088
26777
|
if (!checked && capItem.capSubItems) {
|
|
@@ -26159,11 +26848,11 @@ class RolesComponent {
|
|
|
26159
26848
|
});
|
|
26160
26849
|
}
|
|
26161
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 }); }
|
|
26162
|
-
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"] }] }); }
|
|
26163
26852
|
}
|
|
26164
26853
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: RolesComponent, decorators: [{
|
|
26165
26854
|
type: Component,
|
|
26166
|
-
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"] }]
|
|
26167
26856
|
}], ctorParameters: () => [{ type: HttpService }, { type: i1$1.Router }, { type: AuthService }, { type: DataServiceLib }, { type: DialogService }, { type: i4.MatDialog }, { type: MessageService }, { type: ApiErrorService }] });
|
|
26168
26857
|
|
|
26169
26858
|
class CreateAccountComponent {
|
|
@@ -29013,15 +29702,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
29013
29702
|
}] });
|
|
29014
29703
|
|
|
29015
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".
|
|
29016
29715
|
const ACCOUNTING_ROUTES = [
|
|
29017
29716
|
{ path: "accounts", component: AccountsComponent },
|
|
29018
29717
|
{ path: "aggregates", component: AggregatesComponent },
|
|
29019
29718
|
{ path: "transaction-types", component: TransactionTypesComponent },
|
|
29020
29719
|
{ path: "transactions", component: TransactionsComponent },
|
|
29021
|
-
{ path: "invoices", component: InvoicesComponent },
|
|
29022
|
-
{ path: "aging", component: AgingComponent },
|
|
29023
|
-
{ path: "supplier-aging", component: SupplierAgingComponent },
|
|
29024
|
-
{ 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
|
|
29025
29724
|
{ path: "reports", component: ReportsComponent },
|
|
29026
29725
|
{ path: "tax-rates", component: TaxRatesComponent },
|
|
29027
29726
|
{ path: "standing-orders", component: StandingOrdersComponent },
|
|
@@ -29029,14 +29728,14 @@ const ACCOUNTING_ROUTES = [
|
|
|
29029
29728
|
{ path: "budgets", component: BudgetsComponent },
|
|
29030
29729
|
{ path: "budget-vs-actual", component: BudgetVsActualComponent },
|
|
29031
29730
|
{ path: "dashboard", component: AccountingDashboardComponent },
|
|
29032
|
-
{ path: "invoice-dashboard", component: InvoiceDashboardComponent },
|
|
29033
|
-
{ 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)
|
|
29034
29733
|
{ path: "vat-return", component: VatReturnComponent }, // Changed: VAT return route (B2)
|
|
29035
29734
|
{ path: "fiscal-periods", component: FiscalPeriodsComponent }, // Changed: Fiscal periods route (C2)
|
|
29036
|
-
{ path: "receipts", component: ReceiptsComponent }, // Changed: Customer receipts route (C3)
|
|
29735
|
+
{ path: "receipts", component: ReceiptsComponent, data: { moduleKey: 'invoicing' } }, // Changed: Customer receipts route (C3) // Added (F3)
|
|
29037
29736
|
{ path: "bank-reconciliation", component: BankReconciliationComponent }, // Changed: Bank reconciliation route (C4)
|
|
29038
|
-
{ path: "revenue-schedules", component: RevenueSchedulesComponent }, // Changed: Deferred revenue route (C7)
|
|
29039
|
-
{ 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)
|
|
29040
29739
|
];
|
|
29041
29740
|
class AccountingRoutingModule {
|
|
29042
29741
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AccountingRoutingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
@@ -29730,7 +30429,7 @@ class OnboardingComponent {
|
|
|
29730
30429
|
this.dataService.Navigate('home');
|
|
29731
30430
|
}
|
|
29732
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 }); }
|
|
29733
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: OnboardingComponent, isStandalone: false, selector: "spa-onboarding", ngImport: i0, template: "<label class=\"title\">Welcome, {{username}}</label>\n\n\n\n<!-- terms -->\n<div class=\"mt-3\" *ngIf=\"step=='terms'\">\n\n <label class=\"subtitle text-muted mb-2\" >We care about our users and are dedicated to protecting your data and privacy -\n thats why we want to be clear about what data we collect and how we use it to improve your experience.</label>\n\n <br>\n <spa-check display=\"I agree to the Terms and Privacy Policy\" [(value)]=\"agree\"></spa-check>\n</div>\n\n<!-- owner -->\n<div class=\"mt-3\" *ngIf=\"step=='name' && own\">\n\n <label class=\"subtitle text-muted\" style=\" margin-bottom: 20px;\">The follow steps will guide you to customise your application.</label>\n\n <div style=\"max-width: 400px;\">\n <spa-text display=\"Organisation Name\" [(value)]=\"myTenant.name\" ></spa-text>\n </div>\n\n <label class=\"text-muted\" style=\" font-size: 12px;\">You can change the Organisation's name to your team or company name.</label><br>\n <label class=\"text-muted\" style=\" font-size: 12px;margin-top: 10px;\">The name can be changed later.</label>\n\n</div>\n\n<!-- guest -->\n<div *ngIf=\"step=='hi' && !own\">\n <label class=\"subtitle text-muted\">You are now signed in to {{myTenant.name}}.</label>\n</div>\n\n\n<!-- invitations -->\n<div class=\"mt-3\" *ngIf=\"step=='invitations' && own\">\n\n <label class=\"subtitle text-muted\">You have been requested to join the following organisations. If you accept, you have the option to switch to that org now or stay in you org.</label><br>\n <label class=\"text-muted\" style=\" font-size: 12px;margin-top: 10px;\">You will be able to switch later.</label>\n <spa-invitations-table></spa-invitations-table>\n\n</div>\n\n\n<!-- Actions -->\n<div class=\"mt-3\">\n <button mat-stroked-button color=\"primary\" [disabled]=\"!agree\" (click)=\"next()\">Next <mat-icon>arrow_right_alt</mat-icon></button>\n</div>\n", styles: [".title{margin-top:1em;font-size:28px;font-weight:300}\n"], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage", "hint"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: InvitationsTableComponent, selector: "spa-invitations-table" }] }); }
|
|
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"] }] }); }
|
|
29734
30433
|
}
|
|
29735
30434
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: OnboardingComponent, decorators: [{
|
|
29736
30435
|
type: Component,
|
|
@@ -30804,13 +31503,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
30804
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"] }]
|
|
30805
31504
|
}], ctorParameters: () => [{ type: DataServiceLib }, { type: MessageService }, { type: AuthService }, { type: i4.MatDialog }] });
|
|
30806
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.
|
|
30807
31511
|
const GENERAL_ROUTES = [
|
|
30808
31512
|
{ path: "customers", component: CustomersComponent },
|
|
30809
31513
|
{ path: "suppliers", component: SuppliersComponent },
|
|
30810
31514
|
{ path: "categories", component: CategoriesComponent },
|
|
30811
31515
|
{ path: "subcategories", component: SubCategoriesComponent },
|
|
30812
31516
|
{ path: "brands", component: BrandsComponent },
|
|
30813
|
-
{ path: "tasks", component: TasksComponent }
|
|
31517
|
+
{ path: "tasks", component: TasksComponent, data: { moduleKey: 'tasks' } } // Added (F3): Tasks & Notes is its own module
|
|
30814
31518
|
];
|
|
30815
31519
|
class GeneralRoutingModule {
|
|
30816
31520
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: GeneralRoutingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
@@ -30832,6 +31536,22 @@ class TenantSettingsComponent {
|
|
|
30832
31536
|
this.authService = authService;
|
|
30833
31537
|
this.dialog = dialog;
|
|
30834
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 = '';
|
|
30835
31555
|
this.roles = [
|
|
30836
31556
|
{ roleName: 'Default', roleID: 1 },
|
|
30837
31557
|
];
|
|
@@ -30991,18 +31711,42 @@ class TenantSettingsComponent {
|
|
|
30991
31711
|
this.loadData();
|
|
30992
31712
|
this.loadTenants();
|
|
30993
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
|
+
}
|
|
30994
31737
|
loadData() {
|
|
30995
31738
|
this.dataService.CallApi({ url: 'tenants/meta/x' }, "").subscribe((apiResponse) => {
|
|
30996
31739
|
this.currTenant = apiResponse.data.tenant;
|
|
30997
31740
|
this.currentTenantID = apiResponse.data.currentTenantID;
|
|
30998
|
-
this.plan = apiResponse.data.plan;
|
|
30999
31741
|
this.ownTenant = apiResponse.data.ownTenant;
|
|
31000
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
|
|
31001
31744
|
});
|
|
31002
31745
|
}
|
|
31003
31746
|
loadTenants() {
|
|
31004
31747
|
this.dataService.CallApi({ url: 'tenants/meta_tenants/x' }, "").subscribe((apiResponse) => {
|
|
31005
31748
|
this.tenants = apiResponse.data.tenants;
|
|
31749
|
+
this.setWhere(); // Added: the organisation names live on this response, so the line cannot resolve before it
|
|
31006
31750
|
});
|
|
31007
31751
|
}
|
|
31008
31752
|
updateTenant(x) {
|
|
@@ -31042,11 +31786,11 @@ class TenantSettingsComponent {
|
|
|
31042
31786
|
this.authService.logoff();
|
|
31043
31787
|
}
|
|
31044
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 }); }
|
|
31045
|
-
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"] }] }); }
|
|
31046
31790
|
}
|
|
31047
31791
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TenantSettingsComponent, decorators: [{
|
|
31048
31792
|
type: Component,
|
|
31049
|
-
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"] }]
|
|
31050
31794
|
}], ctorParameters: () => [{ type: DataServiceLib }, { type: MessageService }, { type: AuthService }, { type: i4.MatDialog }, { type: ApiErrorService }] });
|
|
31051
31795
|
|
|
31052
31796
|
class TenantsComponent {
|
|
@@ -32687,8 +33431,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
32687
33431
|
args: [{ selector: 'spa-notifications-config', standalone: false, template: "<spa-page [config]=\"pageConfig\"></spa-page>\n" }]
|
|
32688
33432
|
}], ctorParameters: () => [{ type: DataServiceLib }] });
|
|
32689
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.
|
|
32690
33440
|
const WORKFLOW_ROUTES = [
|
|
32691
|
-
{ path: "approvals", component: ApprovalsComponent },
|
|
33441
|
+
{ path: "approvals", component: ApprovalsComponent, data: { moduleKey: 'approvals' } }, // Added (F3): the one workflow page that is a module
|
|
32692
33442
|
{ path: "approvals-config", component: ApprovalsConfigComponent },
|
|
32693
33443
|
{ path: "notifications", component: NotificationsComponent },
|
|
32694
33444
|
{ path: "appmodels", component: AppModelsComponent },
|
|
@@ -32919,26 +33669,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
32919
33669
|
|
|
32920
33670
|
// All domain routes nested under their module path
|
|
32921
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.
|
|
32922
33682
|
const routes = [
|
|
32923
|
-
{ path: 'user', children: USER_ROUTES },
|
|
32924
|
-
{ path: 'admin', children: ADMIN_ROUTES },
|
|
32925
|
-
{ path: 'accounting', children: ACCOUNTING_ROUTES },
|
|
32926
|
-
{ path: 'inventory', children: INVENTORY_ROUTES },
|
|
32927
|
-
{ path: 'sales', children: SALES_ROUTES },
|
|
32928
|
-
{ path: 'purchasing', children: PURCHASING_ROUTES },
|
|
32929
|
-
{ path: 'hr', children: HR_ROUTES },
|
|
32930
|
-
{ path: 'payroll', children: PAYROLL_ROUTES },
|
|
32931
|
-
{ path: 'manufacturing', children: MANUFACTURING_ROUTES },
|
|
32932
|
-
{ path: 'loans', children: LOANS_ROUTES },
|
|
32933
|
-
{ path: 'general', children: GENERAL_ROUTES },
|
|
32934
|
-
{ path: 'tenancy', children: TENANCY_ROUTES },
|
|
32935
|
-
{ path: 'workflow', children: WORKFLOW_ROUTES },
|
|
32936
|
-
{ path: 'overview', children: OVERVIEW_ROUTES },
|
|
32937
|
-
{ path: 'fixed-assets', children: ASSETS_ROUTES }, // Changed: Added Fixed Assets module
|
|
32938
|
-
{ 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
|
|
32939
33699
|
{ path: 'day-book', component: DayBookComponent }, // Added: Day Book page — home/day-book, renders empty state when unconfigured
|
|
32940
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
|
|
32941
|
-
{ 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
|
|
32942
33702
|
];
|
|
32943
33703
|
class SpaHomeRoutingModule {
|
|
32944
33704
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SpaHomeRoutingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
@@ -33538,5 +34298,5 @@ const ALSQUARE_SVG_WHITE = `<svg viewBox="0 0 80 80" fill="none" xmlns="http://w
|
|
|
33538
34298
|
* Generated bundle index. Do not edit.
|
|
33539
34299
|
*/
|
|
33540
34300
|
|
|
33541
|
-
export { ALSQUARE_SVG_DARK, ALSQUARE_SVG_WHITE, Account, AccountsComponent as AccountingAccountsComponent, AggregatesComponent as AccountingAggregatesComponent, AgingComponent as AccountingAgingComponent, CreditNotesComponent as AccountingCreditNotesComponent, CurrenciesComponent as AccountingCurrenciesComponent, AccountingDashboardComponent, InvoicesComponent as AccountingInvoicesComponent, AccountingModule, ReportsComponent as AccountingReportsComponent, AccountingService, StatementComponent as AccountingStatementComponent, TransactionTypesComponent as AccountingTransactionTypesComponent, TransactionsComponent as AccountingTransactionsComponent, VatReturnComponent as AccountingVatReturnComponent, Action, ActivityComponent, AdminModule, AgentComponent, AgentPageComponent, AgentService, AlertComponent, AlertConfig, AlertMessage, AnalyticsService, ApiErrorService, ApiResponse, AppConfig, AppConfigurationComponent, AppModelsComponent, AssetStatus, AssetsService, AttachComponent, AuthService, BankReconciliationComponent, BillingPageComponent, BottomTab, BrandsComponent, CacheConfig, CapItem, CapsulesComponent, CategoriesComponent, ChangePasswordComponent, ChangeUserPassword, ChartConfig, ChartsComponent, CheckComponent, ChecklistComponent, ChipsComponent, ConfigService, Constants, Core, CreateAccountComponent, CreditNoteStatus, CustomersComponent, DataServiceLib, DateComponent, DatetimeComponent, DayBookComponent, DepartmentsComponent, DetailsDialog, DetailsDialogConfig, DetailsDialogProcessor, DetailsSource, DialogService, EditorComponent, EmailComponent, EmployeesComponent, ExportService, FeatureDirective, FieldLoadIndicator, FilterComponent, FiscalPeriodStatus, FiscalPeriodsComponent, FormComponent, FormConfig, FormSkeletonComponent, GeneralModule, GeneralService, GradesComponent, GroupsComponent, HRModule, HtmlComponent, HttpService, ImportDialogComponent, IndexModule, InventoryDashboardComponent, InventoryModule, InventoryService, InvitationsTableComponent, InvoiceDashboardComponent, InvoiceItemType, InvoiceStatus, JournalEntryDialogComponent, LabelComponent, LastRouteService, ListDialogComponent, ListDialogConfig, LoaderComponent, LoaderService, LoanPaymentsComponent, LoanProductsComponent, LoansComponent, LoansModule, LoansService, LogLevel, LogService, LoginComponent, LogsComponent, MONOGRAM_TONES, ManufacturingModule, MembershipComponent, MessageService, MoneyComponent, MonogramComponent, MovementType, NavMenuComponent, NotesComponent, NotesConfig, NotificationsService, NumberComponent, OfflineIndicatorComponent, OfflineService, OnboardingComponent, OptionComponent, OverviewDashboardComponent, OverviewModule, PageComponent, PageConfig, PaginationConfig, PayrollDashboardComponent, PayrollModule, PayrollService, PerceivedProgress, PlansComponent, PositionsComponent, PrivacyDialogComponent, ProductionService, Profile, ProfileComponent, PurchaseStatus, PurchasingDashboardComponent, PurchasingModule, PurchasingService, PushNotificationService, QUIET_DELAY_MS, QUIET_MIN_MS, QuoteStatus, QuotesComponent, ReceiptDialogComponent, ReceiptStatus, ReceiptsComponent, ReconcileDialogComponent, RecoverAccountComponent, Register, RevenueScheduleDialogComponent, RevenueSchedulesComponent, Role, RoleAccess, RolesComponent, SalesDashboardComponent, SalesModule, SalesService, SearchComponent, SearchConfig, SecurityConfig, SelectBitwiseComponent, SelectComponent, SelectLiteComponent, SelectMultiComponent, SettingsComponent, SetupGuideComponent, SetupService, SignupComponent, SignupData, SpaAdminModule, SpaDayBookModule, SpaHomeModule, SpaIndexModule, SpaLandingComponent, SpaMatModule, SpaUserModule, StatementImportDialogComponent, StatusesComponent, Step, StepConfig, StepsComponent, StorageService, SubCategoriesComponent, SubscriptionPageComponent, SubscriptionService, SuppliersComponent, SyncComponent, TIN_SILENT_REQUEST, TIN_SPA_RUNTIME_CONFIG, TabService, TableComponent, TableConfig, TabsComponent, TasksComponent, TenancyModule, TenantsComponent, TermsDialogComponent, TextAreaComponent, TextComponent, TextMaskComponent, TextMultiComponent, TextSingleComponent, TileConfig, TilesComponent, TinSpaComponent, TinSpaModule, TinSpaService, TitleActionsComponent, TransactionTiming, UnitOfMeasure, UpdateService, User, UserModule, UsersComponent, ViewerComponent, WelcomeComponent, WorkflowModule, authGuard, dialogOptions, featureGuard, getTinSpaAppSettings, isSilentRequest, loadTinSpaAppSettings, loginConfig, messageDialog, monogramInitials, monogramKind, monogramPaletteIndex, provideTinSpaRuntime, resolveQuietLoading, silentContext, tinSpaLocationStrategyFactory, tinSpaMsalInstanceFactory, tinSpaRuntimeConfigFactory, viewerDialog };
|
|
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 };
|
|
33542
34302
|
//# sourceMappingURL=tin-spa.mjs.map
|