tin-spa 20.14.82 → 20.14.85
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 +334 -92
- package/fesm2022/tin-spa.mjs.map +1 -1
- package/index.d.ts +23 -3
- package/package.json +1 -1
package/fesm2022/tin-spa.mjs
CHANGED
|
@@ -40,9 +40,9 @@ import * as i5 from '@angular/material/progress-bar';
|
|
|
40
40
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
|
41
41
|
import * as i7$1 from '@angular/material/tooltip';
|
|
42
42
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
43
|
-
import * as i15 from '@angular/material/table';
|
|
43
|
+
import * as i15$1 from '@angular/material/table';
|
|
44
44
|
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
|
45
|
-
import * as
|
|
45
|
+
import * as i15 from '@angular/cdk/drag-drop';
|
|
46
46
|
import { moveItemInArray, transferArrayItem, DragDropModule } from '@angular/cdk/drag-drop';
|
|
47
47
|
import * as i16 from '@angular/material/paginator';
|
|
48
48
|
import { MatPaginatorModule } from '@angular/material/paginator';
|
|
@@ -67,7 +67,7 @@ import { trigger, transition, style, animate } from '@angular/animations';
|
|
|
67
67
|
import * as i4$3 from '@angular/material/badge';
|
|
68
68
|
import { MatBadgeModule } from '@angular/material/badge';
|
|
69
69
|
import { MatNativeDateModule, MAT_DATE_LOCALE } from '@angular/material/core';
|
|
70
|
-
import * as i20$
|
|
70
|
+
import * as i20$1 from '@angular/material/progress-spinner';
|
|
71
71
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
72
72
|
import { MatRadioModule } from '@angular/material/radio';
|
|
73
73
|
import { MatSliderModule } from '@angular/material/slider';
|
|
@@ -78,7 +78,7 @@ import * as i3$2 from '@angular/material/tabs';
|
|
|
78
78
|
import { MatTabsModule } from '@angular/material/tabs';
|
|
79
79
|
import * as i19$1 from '@angular/material/sidenav';
|
|
80
80
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
|
81
|
-
import * as i20
|
|
81
|
+
import * as i20 from '@angular/material/toolbar';
|
|
82
82
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
|
83
83
|
import * as i21 from '@angular/material/expansion';
|
|
84
84
|
import { MatExpansionModule } from '@angular/material/expansion';
|
|
@@ -494,6 +494,16 @@ class Core {
|
|
|
494
494
|
if (field.serverOwned) {
|
|
495
495
|
return true;
|
|
496
496
|
}
|
|
497
|
+
// Added (readonly-always-1, ARBITER-RULED 2026-08-22 option A): the SECOND instance of the serverOwned
|
|
498
|
+
// override pattern above, and it sits here for the same structural reason — ahead of the create branch,
|
|
499
|
+
// which returns false for every field and is what made `readonly` unusable as a display lock on a create
|
|
500
|
+
// form. Nine sites across shift and piglet had to stay `type: 'label'` because of that, each carrying a
|
|
501
|
+
// note saying so. This is a DISPLAY lock only: unlike serverOwned it does NOT touch buildSubmitPayload,
|
|
502
|
+
// so the value still posts — which is what a valueCalculation-fed reference or a picker-computed readout
|
|
503
|
+
// needs. Required-validation is untouched either way.
|
|
504
|
+
if (field.readonlyAlways) {
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
497
507
|
// Default readonly conditions
|
|
498
508
|
if (config.mode == 'create') {
|
|
499
509
|
return false;
|
|
@@ -2902,7 +2912,7 @@ class AccountingService {
|
|
|
2902
2912
|
{ name: 'name', type: 'text', required: true },
|
|
2903
2913
|
{ name: 'type', type: 'select', required: true, loadAction: { url: 'accounts/list/accountTypes' } },
|
|
2904
2914
|
{ name: 'currencyID', type: 'select', alias: 'Currency', loadAction: { url: 'currencies/list/x' } }, // Changed: Added currency select for multi-currency support
|
|
2905
|
-
{ name: 'balanceDisplay', alias: 'Balance', type: '
|
|
2915
|
+
{ name: 'balanceDisplay', alias: 'Balance', type: 'text', readonly: true, hideOnCreate: true }, // Changed: promoted — this is the reason the dialog gets opened // Changed (label-ngspace-1, owner 2026-08-22): type label -> read-only text. spa-label rendered this through its DEFAULT branch (no format/suffix/size declared), which is {{value}} plain — identical to what spa-text shows — so the on-screen value is unchanged and only the field style differs.
|
|
2906
2916
|
{ name: 'classification', type: 'section', alias: 'Classification & Reporting', collapseOnView: true }, // Added: collapsed by default; these are set-once settings
|
|
2907
2917
|
{ 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
|
|
2908
2918
|
{ name: 'includeInCashTotal', type: 'checkbox', alias: 'Include in Cash Total', section: 'classification', hidden: (data) => data.type !== 0 }, // Changed: Only visible for Asset accounts (type 0)
|
|
@@ -2944,7 +2954,7 @@ class AccountingService {
|
|
|
2944
2954
|
{ name: 'paymentDate', type: 'date', alias: 'Payment Date', required: true }, // Changed (NFD-2, Arbiter 2026-08-19): max: 'today' REMOVED — deliberately uncapped. A payment may legitimately carry a future date (post-dated cheque), so the future IS a valid answer here. Not method-conditioned: uncapped for every payment method.
|
|
2945
2955
|
{ name: 'method', type: 'select', alias: 'Payment Method', required: true, defaultValue: 1, loadAction: { url: 'invoicepayments/list/methods' } },
|
|
2946
2956
|
{ name: 'currencyID', type: 'select', alias: 'Currency', loadAction: { url: 'currencies/list/x' }, defaultFirstValue: true, infoMessage: 'Currency for this payment (defaults to base currency)' }, // Changed: Added currency select for multi-currency payments
|
|
2947
|
-
{ name: 'outstandingAmount', type: '
|
|
2957
|
+
{ name: 'outstandingAmount', type: 'text', alias: 'Outstanding', readonly: true }, // Changed (label-ngspace-1, owner 2026-08-22): type label -> read-only text. spa-label rendered this through its DEFAULT branch (no format/suffix/size declared), which is {{value}} plain — identical to what spa-text shows — so the on-screen value is unchanged and only the field style differs.
|
|
2948
2958
|
{ name: 'amount', type: 'money', alias: 'Amount', required: true, span: true },
|
|
2949
2959
|
{ name: 'reference', type: 'text', alias: 'Reference', span: true }
|
|
2950
2960
|
],
|
|
@@ -2997,8 +3007,8 @@ class AccountingService {
|
|
|
2997
3007
|
fields: [
|
|
2998
3008
|
{ name: 'customerID', alias: 'Customer', type: 'select', required: true, loadAction: { url: 'customers/list/x' }, detailsConfig: this.dataService.customerDetailsConfig },
|
|
2999
3009
|
{ name: 'status', type: 'select', hideOnCreate: true, readonly: true, serverOwned: true, loadAction: { url: 'invoices/list/statuses' } }, // Changed (CFW-1): Invoice.Status carries [ServerOwned] — a create body carrying status:2 stored an invoice born PAID with no items and no payments
|
|
3000
|
-
{ name: 'totalDisplay', type: '
|
|
3001
|
-
{ name: 'outstandingAmount', type: '
|
|
3010
|
+
{ name: 'totalDisplay', type: 'text', alias: 'Total', format: 'text', readonly: true, hideOnCreate: true }, // Changed (label-ngspace-1, owner 2026-08-22): type label -> read-only text. spa-label rendered this through its DEFAULT branch (no format/suffix/size declared), which is {{value}} plain — identical to what spa-text shows — so the on-screen value is unchanged and only the field style differs.
|
|
3011
|
+
{ name: 'outstandingAmount', type: 'text', alias: 'Outstanding', readonly: true, hideOnCreate: true }, // Changed (label-ngspace-1, owner 2026-08-22): type label -> read-only text. spa-label rendered this through its DEFAULT branch (no format/suffix/size declared), which is {{value}} plain — identical to what spa-text shows — so the on-screen value is unchanged and only the field style differs.
|
|
3002
3012
|
{ name: 'invoiceInfo', type: 'section', alias: 'Invoice Details', collapseOnView: true },
|
|
3003
3013
|
{ name: 'invoiceDate', type: 'date', max: 'today', required: true, section: 'invoiceInfo' }, // Added (NFD-1): max: 'today' — this date records something that has ALREADY happened, so the future is not a valid answer; DateBounds resolves the token at render time, never at build time. Back-dating stays allowed.
|
|
3004
3014
|
{ name: 'invoiceNumber', type: 'text', alias: 'Invoice #', section: 'invoiceInfo' }, // Changed: Made editable on create for optional manual numbering
|
|
@@ -3007,7 +3017,7 @@ class AccountingService {
|
|
|
3007
3017
|
{ name: 'amounts', type: 'section', alias: 'Amount Breakdown', hideOnCreate: true, collapseOnView: true },
|
|
3008
3018
|
{ name: 'subTotal', type: 'money', alias: 'Sub Total', readonly: true, hideOnCreate: true, section: 'amounts' }, // Changed: Show SubTotal (before tax)
|
|
3009
3019
|
{ name: 'taxTotal', type: 'money', alias: 'VAT', readonly: true, hideOnCreate: true, section: 'amounts' }, // Changed: Show TaxTotal (VAT amount)
|
|
3010
|
-
{ name: 'paidAmount', type: '
|
|
3020
|
+
{ name: 'paidAmount', type: 'text', alias: 'Paid', readonly: true, hideOnCreate: true, section: 'amounts' } // Changed (label-ngspace-1, owner 2026-08-22): type label -> read-only text. spa-label rendered this through its DEFAULT branch (no format/suffix/size declared), which is {{value}} plain — identical to what spa-text shows — so the on-screen value is unchanged and only the field style differs.
|
|
3011
3021
|
],
|
|
3012
3022
|
loadAction: { url: 'invoices/id' },
|
|
3013
3023
|
heroField: 'invoiceID',
|
|
@@ -3065,7 +3075,7 @@ class AccountingService {
|
|
|
3065
3075
|
fixedTitle: true,
|
|
3066
3076
|
fields: [
|
|
3067
3077
|
{ name: 'invoiceItemID', type: 'number', hidden: true },
|
|
3068
|
-
{ name: 'description', type: '
|
|
3078
|
+
{ name: 'description', type: 'text', alias: 'Item', readonly: true, span: true }, // Changed (label-ngspace-1, owner 2026-08-22): type label -> read-only text. spa-label rendered this through its DEFAULT branch (no format/suffix/size declared), which is {{value}} plain — identical to what spa-text shows — so the on-screen value is unchanged and only the field style differs.
|
|
3069
3079
|
{ name: 'invoiceID', type: 'select', alias: 'Move to Invoice', required: true, span: true, loadAction: { url: 'invoiceitems/list/draft-invoices' } }
|
|
3070
3080
|
],
|
|
3071
3081
|
loadAction: { url: 'invoiceitems/id' }, // Fixed: removed placeholder - TinCore appends ID automatically
|
|
@@ -3741,7 +3751,7 @@ class AccountingService {
|
|
|
3741
3751
|
fixedTitle: true,
|
|
3742
3752
|
fields: [
|
|
3743
3753
|
{ name: 'transactionTemplateID', type: 'number', hidden: true },
|
|
3744
|
-
{ name: 'name', type: '
|
|
3754
|
+
{ name: 'name', type: 'text', alias: 'Template', readonly: true, span: true }, // Changed (label-ngspace-1, owner 2026-08-22): type label -> read-only text. spa-label rendered this through its DEFAULT branch (no format/suffix/size declared), which is {{value}} plain — identical to what spa-text shows — so the on-screen value is unchanged and only the field style differs.
|
|
3745
3755
|
{ name: 'transactionTypeID', type: 'select', alias: 'Transaction Type', required: true, detailsConfig: this.transactionTypeDetailsConfig, loadAction: { url: 'transactionTypes/list/x' } },
|
|
3746
3756
|
{ name: 'date', type: 'date', max: 'today', required: true, defaultValue: 'now' }, // Added (NFD-1): max: 'today' — this date records something that has ALREADY happened, so the future is not a valid answer; DateBounds resolves the token at render time, never at build time. Back-dating stays allowed.
|
|
3747
3757
|
{ name: 'debitAccountID', type: 'select', alias: 'Debit Account', required: true, masterField: 'transactionTypeID', masterValueField: 'debitAccountType', masterOptionValue: 'type', loadAction: { url: 'accounts/list/x' }, detailsConfig: this.accountBaseDetailsConfig },
|
|
@@ -4074,9 +4084,9 @@ class AccountingService {
|
|
|
4074
4084
|
{ name: 'name', type: 'text', required: true, alias: 'Budget Name' },
|
|
4075
4085
|
{ name: 'fiscalYear', type: 'number', required: true, alias: 'Fiscal Year', defaultValue: new Date().getFullYear() },
|
|
4076
4086
|
{ name: 'description', type: 'text', span: true },
|
|
4077
|
-
{ name: 'statusName', type: '
|
|
4087
|
+
{ name: 'statusName', type: 'text', alias: 'Status', readonly: true, hideOnCreate: true }, // Changed (label-ngspace-1, owner 2026-08-22): type label -> read-only text. spa-label rendered this through its DEFAULT branch (no format/suffix/size declared), which is {{value}} plain — identical to what spa-text shows — so the on-screen value is unchanged and only the field style differs.
|
|
4078
4088
|
{ name: 'isActive', type: 'checkbox', alias: 'Active', readonly: true, hideOnCreate: true },
|
|
4079
|
-
{ name: 'totalBudgetedDisplay', type: '
|
|
4089
|
+
{ name: 'totalBudgetedDisplay', type: 'text', alias: 'Total Budgeted', readonly: true, hideOnCreate: true } // Changed (label-ngspace-1, owner 2026-08-22): type label -> read-only text. spa-label rendered this through its DEFAULT branch (no format/suffix/size declared), which is {{value}} plain — identical to what spa-text shows — so the on-screen value is unchanged and only the field style differs.
|
|
4080
4090
|
],
|
|
4081
4091
|
loadAction: { url: 'budgets/id' },
|
|
4082
4092
|
heroField: 'budgetID'
|
|
@@ -4282,13 +4292,13 @@ class AccountingService {
|
|
|
4282
4292
|
fields: [
|
|
4283
4293
|
{ name: 'customerID', alias: 'Customer', type: 'select', required: true, loadAction: { url: 'customers/list/x' }, detailsConfig: this.dataService.customerDetailsConfig }, // Changed: dropdown quick-add
|
|
4284
4294
|
{ name: 'status', type: 'select', hideOnCreate: true, readonly: true, serverOwned: true, loadAction: { url: 'creditnotes/list/statuses' } }, // Changed (CFW-1): CreditNote.Status carries [ServerOwned]
|
|
4285
|
-
{ name: 'totalDisplay', type: '
|
|
4295
|
+
{ name: 'totalDisplay', type: 'text', alias: 'Total', readonly: true, hideOnCreate: true }, // Changed (label-ngspace-1, owner 2026-08-22): type label -> read-only text. spa-label rendered this through its DEFAULT branch (no format/suffix/size declared), which is {{value}} plain — identical to what spa-text shows — so the on-screen value is unchanged and only the field style differs.
|
|
4286
4296
|
{ name: 'reason', type: 'text', alias: 'Reason', span: true },
|
|
4287
4297
|
{ name: 'invoiceID', type: 'select', alias: 'Invoice', nullable: true, loadAction: { url: 'invoiceitems/list/draft-invoices' }, hidden: true }, // Hidden generic select — invoice linkage set via create-from-invoice
|
|
4288
4298
|
{ name: 'creditNoteInfo', type: 'section', alias: 'Credit Note Details', collapseOnView: true },
|
|
4289
4299
|
{ name: 'creditNoteDate', type: 'date', max: 'today', required: true, alias: 'Date', section: 'creditNoteInfo' }, // Added (NFD-1): max: 'today' — this date records something that has ALREADY happened, so the future is not a valid answer; DateBounds resolves the token at render time, never at build time. Back-dating stays allowed.
|
|
4290
4300
|
{ name: 'creditNoteNumber', type: 'text', alias: 'Credit Note #', readonly: true, hideOnCreate: true, section: 'creditNoteInfo' },
|
|
4291
|
-
{ name: 'invoiceNumber', type: '
|
|
4301
|
+
{ name: 'invoiceNumber', type: 'text', alias: 'Invoice #', readonly: true, hideOnCreate: true, section: 'creditNoteInfo' }, // Changed (label-ngspace-1, owner 2026-08-22): type label -> read-only text. spa-label rendered this through its DEFAULT branch (no format/suffix/size declared), which is {{value}} plain — identical to what spa-text shows — so the on-screen value is unchanged and only the field style differs.
|
|
4292
4302
|
{ name: 'amounts', type: 'section', alias: 'Amount Breakdown', hideOnCreate: true, collapseOnView: true },
|
|
4293
4303
|
{ name: 'subTotal', type: 'money', alias: 'Sub Total', readonly: true, hideOnCreate: true, section: 'amounts' },
|
|
4294
4304
|
{ name: 'taxTotal', type: 'money', alias: 'VAT', readonly: true, hideOnCreate: true, section: 'amounts' }
|
|
@@ -4473,12 +4483,12 @@ class AccountingService {
|
|
|
4473
4483
|
fields: [
|
|
4474
4484
|
{ name: 'customerID', alias: 'Customer', type: 'select', required: true, loadAction: { url: 'customers/list/x' }, detailsConfig: this.dataService.customerDetailsConfig }, // Changed: dropdown quick-add
|
|
4475
4485
|
{ name: 'status', type: 'select', hideOnCreate: true, readonly: true, serverOwned: true, loadAction: { url: 'quotes/list/statuses' } }, // Changed (CFW-1): Quote.Status carries [ServerOwned] — the flagship, MEASURED moving 0 -> 2 from an edit body in Phase 1
|
|
4476
|
-
{ name: 'totalDisplay', type: '
|
|
4486
|
+
{ name: 'totalDisplay', type: 'text', alias: 'Total', readonly: true, hideOnCreate: true }, // Changed (label-ngspace-1, owner 2026-08-22): type label -> read-only text. spa-label rendered this through its DEFAULT branch (no format/suffix/size declared), which is {{value}} plain — identical to what spa-text shows — so the on-screen value is unchanged and only the field style differs.
|
|
4477
4487
|
{ name: 'expiryDate', type: 'date', alias: 'Valid Until', nullable: true, defaultValue: '' }, // Changed: defaultValue '' stops TinCore pre-filling TODAY — a quote whose validity expires the day it is raised is valid for zero days, and nullable:true already says the intended default is EMPTY
|
|
4478
4488
|
{ name: 'quoteInfo', type: 'section', alias: 'Quote Details', collapseOnView: true },
|
|
4479
4489
|
{ name: 'quoteDate', type: 'date', max: 'today', required: true, alias: 'Date', section: 'quoteInfo' }, // Added (NFD-1): max: 'today' — this date records something that has ALREADY happened, so the future is not a valid answer; DateBounds resolves the token at render time, never at build time. Back-dating stays allowed.
|
|
4480
4490
|
{ name: 'quoteNumber', type: 'text', alias: 'Quote #', readonly: true, hideOnCreate: true, section: 'quoteInfo' },
|
|
4481
|
-
{ name: 'convertedInvoiceNumber', type: '
|
|
4491
|
+
{ name: 'convertedInvoiceNumber', type: 'text', alias: 'Invoice #', readonly: true, hideOnCreate: true, section: 'quoteInfo' }, // Changed (label-ngspace-1, owner 2026-08-22): type label -> read-only text. spa-label rendered this through its DEFAULT branch (no format/suffix/size declared), which is {{value}} plain — identical to what spa-text shows — so the on-screen value is unchanged and only the field style differs.
|
|
4482
4492
|
{ name: 'notes', type: 'text', alias: 'Notes', span: true, section: 'quoteInfo' },
|
|
4483
4493
|
{ name: 'amounts', type: 'section', alias: 'Amount Breakdown', hideOnCreate: true, collapseOnView: true },
|
|
4484
4494
|
{ name: 'subTotal', type: 'money', alias: 'Sub Total', readonly: true, hideOnCreate: true, section: 'amounts' },
|
|
@@ -4654,7 +4664,7 @@ class AccountingService {
|
|
|
4654
4664
|
{ name: 'startDate', type: 'date', required: true, alias: 'Start Date' },
|
|
4655
4665
|
{ name: 'endDate', type: 'date', required: true, alias: 'End Date' },
|
|
4656
4666
|
{ name: 'isYearEnd', type: 'checkbox', alias: 'Year-End Period' },
|
|
4657
|
-
{ name: 'statusName', type: '
|
|
4667
|
+
{ name: 'statusName', type: 'text', alias: 'Status', readonly: true, hideOnCreate: true } // Changed (label-ngspace-1, owner 2026-08-22): type label -> read-only text. spa-label rendered this through its DEFAULT branch (no format/suffix/size declared), which is {{value}} plain — identical to what spa-text shows — so the on-screen value is unchanged and only the field style differs.
|
|
4658
4668
|
],
|
|
4659
4669
|
loadAction: { url: 'fiscalperiods/id' },
|
|
4660
4670
|
heroField: 'fiscalPeriodID'
|
|
@@ -4949,7 +4959,7 @@ class SalesService {
|
|
|
4949
4959
|
{ name: 'paymentDate', type: 'date', alias: 'Payment Date', required: true }, // Changed (NFD-3): max: 'today' REMOVED — a post-dated payment instrument is ordinary business, so a future payment date is a valid answer; verified the server (InvoiceService.RecordPayment) does not reject one either. Pinned by payment-date-uncapped-guard.spec.ts.
|
|
4950
4960
|
{ name: 'method', type: 'select', alias: 'Payment Method', required: true, defaultValue: 1, loadAction: { url: 'invoicepayments/list/methods' } },
|
|
4951
4961
|
{ name: 'currencyID', type: 'select', alias: 'Currency', loadAction: { url: 'currencies/list/x' }, defaultFirstValue: true, detailsConfig: this.accountingService.currencyDetailsConfig, infoMessage: 'Currency for this payment (defaults to base currency)' }, // Changed: dropdown quick-add
|
|
4952
|
-
{ name: 'totalAmount', type: '
|
|
4962
|
+
{ name: 'totalAmount', type: 'text', alias: 'Total Amount', readonly: true }, // Changed (label-ngspace-1, owner 2026-08-22): type label -> read-only text. spa-label rendered this through its DEFAULT branch (no format/suffix/size declared), which is {{value}} plain — identical to what spa-text shows — so the on-screen value is unchanged and only the field style differs.
|
|
4953
4963
|
{ name: 'amount', type: 'money', alias: 'Amount', required: true, span: true },
|
|
4954
4964
|
{ name: 'paymentReferenceTemp', type: 'text', alias: 'Reference', span: true }
|
|
4955
4965
|
],
|
|
@@ -5439,7 +5449,7 @@ class PurchasingService {
|
|
|
5439
5449
|
{ name: 'paymentDate', type: 'date', alias: 'Payment Date', required: true }, // Changed (NFD-2, Arbiter 2026-08-19): max: 'today' REMOVED — deliberately uncapped. A payment may legitimately carry a future date (post-dated cheque), so the future IS a valid answer here. Not method-conditioned: uncapped for every payment method.
|
|
5440
5450
|
{ name: 'method', type: 'select', alias: 'Payment Method', required: true, defaultValue: 2, loadAction: { url: 'supplierpayments/list/methods' } },
|
|
5441
5451
|
{ name: 'currencyID', type: 'select', alias: 'Currency', loadAction: { url: 'currencies/list/x' }, defaultFirstValue: true, detailsConfig: this.accountingService.currencyDetailsConfig, infoMessage: 'Currency for this payment (defaults to base currency)' }, // Changed: dropdown quick-add
|
|
5442
|
-
{ name: 'outstandingAmount', type: '
|
|
5452
|
+
{ name: 'outstandingAmount', type: 'text', alias: 'Outstanding', readonly: true }, // Changed (label-ngspace-1, owner 2026-08-22): type label -> read-only text. spa-label rendered this through its DEFAULT branch (no format/suffix/size declared), which is {{value}} plain — identical to what spa-text shows — so the on-screen value is unchanged and only the field style differs.
|
|
5443
5453
|
{ name: 'amount', type: 'money', alias: 'Amount', required: true, span: true },
|
|
5444
5454
|
{ name: 'reference', type: 'text', alias: 'Reference', span: true }
|
|
5445
5455
|
],
|
|
@@ -9988,7 +9998,7 @@ class DataServiceLib {
|
|
|
9988
9998
|
title: 'Approval',
|
|
9989
9999
|
fields: [
|
|
9990
10000
|
{ name: 'blank', type: 'blank', span: true },
|
|
9991
|
-
{ name: 'statusName', type: '
|
|
10001
|
+
{ name: 'statusName', type: 'text', alias: 'Status', span: true, readonly: true }, // Changed (label-ngspace-1, owner 2026-08-22): type label -> read-only text; readonly:true ADDED — spa-label was never editable, and this form is not a create form, so testReadOnly returns true here and the field stays locked.
|
|
9992
10002
|
{ name: 'message', type: 'text-area', required: true, span: true, rows: 3 },
|
|
9993
10003
|
],
|
|
9994
10004
|
loadAction: { url: 'approvals/id' },
|
|
@@ -10809,13 +10819,13 @@ class DataServiceLib {
|
|
|
10809
10819
|
{ key: 'outOfStock', lane: 'stores', listField: 'outOfStock', tableConfig: inventory.productsTableConfig,
|
|
10810
10820
|
columns: ['name', { column: 'sku', alias: 'SKU' }, 'categoryName'],
|
|
10811
10821
|
extraColumns: [{ name: 'minimumLevel', alias: 'Minimum' }], // book-only — the page's own column is minimumInventoryLevel and would read blank here
|
|
10812
|
-
buttons: [{ button: 'view', display: 'Open the product', icon: { name: '
|
|
10822
|
+
buttons: [{ button: 'view', display: 'Open the product', icon: { name: 'launch' }, tip: 'Opens this product without leaving the Day Book' }], // Changed (sweep-1, owner directive 2026-08-20): was inventory_2 + blue. A row VIEW is `launch` estate-wide (shift-spa, grip-spa, piglet's dayBookViewPig) and inventory_2 is ALSO this lane's own section glyph, so the action and the card it sat on wore the same icon for two different meanings. The blue is dropped with it — an action icon is not recoloured away from its siblings. test-spa was swept on 08-20; this is the library's own copy of the same demo config, missed then
|
|
10813
10823
|
overrides: { ...this.libraryDayBookChrome, minColumns: ['name', 'categoryName', 'minimumLevel'], sectionConfig: { title: 'Out of Stock', icon: 'remove_shopping_cart', chips: [{ text: 'nothing left to sell', color: '#c62828' }] } }
|
|
10814
10824
|
},
|
|
10815
10825
|
{ key: 'lowStock', lane: 'stores', listField: 'lowStock', tableConfig: inventory.productsTableConfig,
|
|
10816
10826
|
columns: ['name', { column: 'sku', alias: 'SKU' }, 'categoryName'],
|
|
10817
10827
|
extraColumns: [{ name: 'available', alias: 'In Stock' }, { name: 'minimumLevel', alias: 'Minimum' }, { name: 'shortfall', alias: 'Short By' }],
|
|
10818
|
-
buttons: [{ button: 'view', display: 'Open the product', icon: { name: '
|
|
10828
|
+
buttons: [{ button: 'view', display: 'Open the product', icon: { name: 'launch' }, tip: 'Opens this product without leaving the Day Book' }], // Changed (sweep-1, owner directive 2026-08-20): was inventory_2 + blue. A row VIEW is `launch` estate-wide (shift-spa, grip-spa, piglet's dayBookViewPig) and inventory_2 is ALSO this lane's own section glyph, so the action and the card it sat on wore the same icon for two different meanings. The blue is dropped with it — an action icon is not recoloured away from its siblings. test-spa was swept on 08-20; this is the library's own copy of the same demo config, missed then
|
|
10819
10829
|
overrides: { ...this.libraryDayBookChrome, minColumns: ['name', 'available', 'shortfall'], sectionConfig: { title: 'Low Stock', icon: 'inventory_2', chips: [{ text: 'below the minimum you chose to hold', color: '#ef6c00' }] } }
|
|
10820
10830
|
}
|
|
10821
10831
|
];
|
|
@@ -14529,6 +14539,11 @@ class SuffixComponent {
|
|
|
14529
14539
|
this.copyContent = false;
|
|
14530
14540
|
this.isHovered = false;
|
|
14531
14541
|
this.clearContent = false;
|
|
14542
|
+
// Added: this component had NO notion of readonly at all, so the clear X rendered and mutated on a locked
|
|
14543
|
+
// field in every one of the 10 field types that render it. Gated in the template AND here: the template
|
|
14544
|
+
// stops the click, this stops any other caller, and a write affordance that is wrong to offer is also
|
|
14545
|
+
// wrong to honour. Copy is untouched — it reads.
|
|
14546
|
+
this.readonly = false;
|
|
14532
14547
|
this.infoClick = new EventEmitter();
|
|
14533
14548
|
this.copyClick = new EventEmitter();
|
|
14534
14549
|
this.clearClick = new EventEmitter();
|
|
@@ -14554,6 +14569,9 @@ class SuffixComponent {
|
|
|
14554
14569
|
}
|
|
14555
14570
|
onClearClick(event) {
|
|
14556
14571
|
event.stopPropagation();
|
|
14572
|
+
if (this.readonly) {
|
|
14573
|
+
return;
|
|
14574
|
+
} // Added: never write through a locked field, whatever route reached here
|
|
14557
14575
|
// Changed: was `this.value = ''`. Every parent binds [(value)]="value", so this writes straight into the
|
|
14558
14576
|
// parent's own value BEFORE clearClick runs. For date/datetime — which had no (clearClick) at all until
|
|
14559
14577
|
// this change — that write was the ONLY effect, and '' is not what an emptied field means: an int property
|
|
@@ -14564,11 +14582,11 @@ class SuffixComponent {
|
|
|
14564
14582
|
this.clearClick.emit();
|
|
14565
14583
|
}
|
|
14566
14584
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SuffixComponent, deps: [{ token: MessageService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
14567
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SuffixComponent, isStandalone: false, selector: "spa-suffix", inputs: { label: "label", infoMessage: "infoMessage", copyContent: "copyContent", isHovered: "isHovered", clearContent: "clearContent", value: "value" }, outputs: { infoClick: "infoClick", copyClick: "copyClick", clearClick: "clearClick", valueChange: "valueChange" }, ngImport: i0, template: "<label *ngIf=\"label\"> {{label}}</label>\n<button mat-icon-button *ngIf=\"copyContent && isHovered\" (click)=\"onCopyClick($event)\" matTooltip=\"Copy Content\" matTooltipPosition=\"above\" style=\"opacity: 1;\">\n <mat-icon class=\"tinyIcon\">content_copy</mat-icon>\n</button>\n<button mat-icon-button *ngIf=\"clearContent && isHovered\" (click)=\"onClearClick($event)\" matTooltip=\"Clear Content\" matTooltipPosition=\"above\" style=\"opacity: 1;\"
|
|
14585
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SuffixComponent, isStandalone: false, selector: "spa-suffix", inputs: { label: "label", infoMessage: "infoMessage", copyContent: "copyContent", isHovered: "isHovered", clearContent: "clearContent", readonly: "readonly", value: "value" }, outputs: { infoClick: "infoClick", copyClick: "copyClick", clearClick: "clearClick", valueChange: "valueChange" }, ngImport: i0, template: "<label *ngIf=\"label\"> {{label}}</label>\n<button mat-icon-button *ngIf=\"copyContent && isHovered\" (click)=\"onCopyClick($event)\" matTooltip=\"Copy Content\" matTooltipPosition=\"above\" style=\"opacity: 1;\">\n <mat-icon class=\"tinyIcon\">content_copy</mat-icon>\n</button>\n<button mat-icon-button *ngIf=\"clearContent && isHovered && !readonly\" (click)=\"onClearClick($event)\" matTooltip=\"Clear Content\" matTooltipPosition=\"above\" style=\"opacity: 1;\"><!-- Changed: `&& !readonly` \u2014 the X is the one affordance here that WRITES, and it rendered and fired on a locked field, so a view-only dialog could still be emptied. Copy above is deliberately NOT gated: reading a locked value is exactly what a view dialog is for. -->\n <mat-icon class=\"tinyIcon\">close</mat-icon>\n</button>\n<button mat-icon-button *ngIf=\"infoMessage\" (click)=\"onInfoClick($event)\" [matTooltip]=\"infoMessage\" matTooltipPosition=\"above\" style=\"opacity: 1;margin-right: 2px;\">\n <mat-icon class=\"tinyIcon\" style=\"color: steelblue;\">info</mat-icon>\n</button>\n", styles: [".smallIcon{font-size:14px}\n"], dependencies: [{ kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] }); }
|
|
14568
14586
|
}
|
|
14569
14587
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SuffixComponent, decorators: [{
|
|
14570
14588
|
type: Component,
|
|
14571
|
-
args: [{ selector: 'spa-suffix', standalone: false, template: "<label *ngIf=\"label\"> {{label}}</label>\n<button mat-icon-button *ngIf=\"copyContent && isHovered\" (click)=\"onCopyClick($event)\" matTooltip=\"Copy Content\" matTooltipPosition=\"above\" style=\"opacity: 1;\">\n <mat-icon class=\"tinyIcon\">content_copy</mat-icon>\n</button>\n<button mat-icon-button *ngIf=\"clearContent && isHovered\" (click)=\"onClearClick($event)\" matTooltip=\"Clear Content\" matTooltipPosition=\"above\" style=\"opacity: 1;\"
|
|
14589
|
+
args: [{ selector: 'spa-suffix', standalone: false, template: "<label *ngIf=\"label\"> {{label}}</label>\n<button mat-icon-button *ngIf=\"copyContent && isHovered\" (click)=\"onCopyClick($event)\" matTooltip=\"Copy Content\" matTooltipPosition=\"above\" style=\"opacity: 1;\">\n <mat-icon class=\"tinyIcon\">content_copy</mat-icon>\n</button>\n<button mat-icon-button *ngIf=\"clearContent && isHovered && !readonly\" (click)=\"onClearClick($event)\" matTooltip=\"Clear Content\" matTooltipPosition=\"above\" style=\"opacity: 1;\"><!-- Changed: `&& !readonly` \u2014 the X is the one affordance here that WRITES, and it rendered and fired on a locked field, so a view-only dialog could still be emptied. Copy above is deliberately NOT gated: reading a locked value is exactly what a view dialog is for. -->\n <mat-icon class=\"tinyIcon\">close</mat-icon>\n</button>\n<button mat-icon-button *ngIf=\"infoMessage\" (click)=\"onInfoClick($event)\" [matTooltip]=\"infoMessage\" matTooltipPosition=\"above\" style=\"opacity: 1;margin-right: 2px;\">\n <mat-icon class=\"tinyIcon\" style=\"color: steelblue;\">info</mat-icon>\n</button>\n", styles: [".smallIcon{font-size:14px}\n"] }]
|
|
14572
14590
|
}], ctorParameters: () => [{ type: MessageService }], propDecorators: { label: [{
|
|
14573
14591
|
type: Input
|
|
14574
14592
|
}], infoMessage: [{
|
|
@@ -14579,6 +14597,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
14579
14597
|
type: Input
|
|
14580
14598
|
}], clearContent: [{
|
|
14581
14599
|
type: Input
|
|
14600
|
+
}], readonly: [{
|
|
14601
|
+
type: Input
|
|
14582
14602
|
}], value: [{
|
|
14583
14603
|
type: Input
|
|
14584
14604
|
}], infoClick: [{
|
|
@@ -14853,11 +14873,11 @@ class SelectMultiComponent {
|
|
|
14853
14873
|
this.loadIndicator.destroy(); // Added: clear any pending delay/hold timer on a field torn down mid-read
|
|
14854
14874
|
}
|
|
14855
14875
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SelectMultiComponent, deps: [{ token: MessageService }, { token: DataServiceLib }, { token: ApiErrorService }, { token: OfflineService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
14856
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SelectMultiComponent, isStandalone: false, selector: "spa-select-multi", inputs: { display: "display", value: "value", readonly: "readonly", required: "required", hint: "hint", options: "options", optionDisplay: "optionDisplay", optionValue: "optionValue", infoMessage: "infoMessage", copyContent: "copyContent", clearContent: "clearContent", nullable: "nullable", placeholder: "placeholder", width: "width", suffix: "suffix", loadAction: "loadAction", selectAll: "selectAll", refetchOnChange: "refetchOnChange" }, outputs: { valueChange: "valueChange", hoverChange: "hoverChange" }, usesOnChanges: true, ngImport: i0, template: "<!-- Changed (WS-1 Phase 3): one class added \u2014 no structural change. It tints the hint line the field already renders. Same treatment as spa-select-common, spa-text-single and spa-text-multi; see the .css for why the state is spoken on the field rather than in a toast. -->\n<!-- Changed (placeholder conventions): floatLabel=\"always\" \u2014 with the default 'auto' Material hides the placeholder behind the resting label, so \"Select X\" could never be seen in the empty state the convention is FOR. spa-select-common has always floated for this reason; this brings the multi to parity. -->\n<mat-form-field floatLabel=\"always\" [class.spa-options-unavailable]=\"optionsUnavailableOffline\" [ngStyle]=\"{'width':width ?? '100%'}\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <mat-select [formControl]=\"control\" [required]=\"required\" multiple [placeholder]=\"computedPlaceholder\" [compareWith]=\"compareWith\" (selectionChange)=\"selectionChange($event)\">\n <mat-option *ngIf=\"nullable\" [value]=\"null\">None</mat-option>\n <mat-option *ngFor=\"let option of options\" [value]=\"option[optionValue]\">\n {{option[optionDisplay]}}\n </mat-option>\n </mat-select>\n <!-- Changed (WS-1 Phase 3): the hint now reads computedHint, and \u2014 the part that matters \u2014 it is GATED on computedHint too, not on hint.\n This differs from spa-select-common and spa-text-single only because the surface differs: those bind [hintLabel], which renders whenever\n it is non-empty, whereas this template renders an explicit <mat-hint> behind an *ngIf. Left gated on `hint` the offline notice would have\n been invisible at the ONLY live target in the estate: notifications.component.ts's roleIDs field declares no `hint` at all, so\n form.component.html's [hint]=\"field.hint\" binds undefined and the *ngIf never opens. It would likewise never appear in the filter/option\n context, where option.component.html binds [loadAction] but never binds [hint] \u2014 the exact context where a lookup can fail with nothing\n else on screen to explain it. Both surfaces render the same .mat-mdc-form-field-hint element, so the copied css rule is unchanged. -->\n <mat-hint *ngIf=\"computedHint\">{{computedHint}}</mat-hint>\n <div matSuffix class=\"suffix-icons\">\n <!-- Added (WS-1 Phase 3): the house's own offline mark, the same cloud_off spa-offline-indicator, spa-select-common, spa-text-single and spa-text-multi already use, so the state is recognisable without reading \u2014 which is what carries it at phone width, where the hint line has least room. The refresh button below is left exactly as it was and stays reachable on purpose: it is the one action that fixes this once the connection is back, and getData() clears the flag on the first successful load. -->\n <mat-icon *ngIf=\"optionsUnavailableOffline\" class=\"tinyIcon spa-options-unavailable-icon\" [matTooltip]=\"computedHint\" matTooltipPosition=\"above\">cloud_off</mat-icon>\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"true\" [(value)]=\"value\">\n </spa-suffix>\n </div>\n</mat-form-field>\n", styles: [".spa-options-unavailable ::ng-deep .mat-mdc-form-field-hint{color:#ef6c00}.spa-options-unavailable-icon{color:#ef6c00;vertical-align:middle}\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: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i7.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: i7$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }] }); }
|
|
14876
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SelectMultiComponent, isStandalone: false, selector: "spa-select-multi", inputs: { display: "display", value: "value", readonly: "readonly", required: "required", hint: "hint", options: "options", optionDisplay: "optionDisplay", optionValue: "optionValue", infoMessage: "infoMessage", copyContent: "copyContent", clearContent: "clearContent", nullable: "nullable", placeholder: "placeholder", width: "width", suffix: "suffix", loadAction: "loadAction", selectAll: "selectAll", refetchOnChange: "refetchOnChange" }, outputs: { valueChange: "valueChange", hoverChange: "hoverChange" }, usesOnChanges: true, ngImport: i0, template: "<!-- Changed (WS-1 Phase 3): one class added \u2014 no structural change. It tints the hint line the field already renders. Same treatment as spa-select-common, spa-text-single and spa-text-multi; see the .css for why the state is spoken on the field rather than in a toast. -->\n<!-- Changed (placeholder conventions): floatLabel=\"always\" \u2014 with the default 'auto' Material hides the placeholder behind the resting label, so \"Select X\" could never be seen in the empty state the convention is FOR. spa-select-common has always floated for this reason; this brings the multi to parity. -->\n<mat-form-field floatLabel=\"always\" [class.spa-options-unavailable]=\"optionsUnavailableOffline\" [ngStyle]=\"{'width':width ?? '100%'}\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <mat-select [formControl]=\"control\" [required]=\"required\" multiple [placeholder]=\"computedPlaceholder\" [compareWith]=\"compareWith\" (selectionChange)=\"selectionChange($event)\">\n <mat-option *ngIf=\"nullable\" [value]=\"null\">None</mat-option>\n <mat-option *ngFor=\"let option of options\" [value]=\"option[optionValue]\">\n {{option[optionDisplay]}}\n </mat-option>\n </mat-select>\n <!-- Changed (WS-1 Phase 3): the hint now reads computedHint, and \u2014 the part that matters \u2014 it is GATED on computedHint too, not on hint.\n This differs from spa-select-common and spa-text-single only because the surface differs: those bind [hintLabel], which renders whenever\n it is non-empty, whereas this template renders an explicit <mat-hint> behind an *ngIf. Left gated on `hint` the offline notice would have\n been invisible at the ONLY live target in the estate: notifications.component.ts's roleIDs field declares no `hint` at all, so\n form.component.html's [hint]=\"field.hint\" binds undefined and the *ngIf never opens. It would likewise never appear in the filter/option\n context, where option.component.html binds [loadAction] but never binds [hint] \u2014 the exact context where a lookup can fail with nothing\n else on screen to explain it. Both surfaces render the same .mat-mdc-form-field-hint element, so the copied css rule is unchanged. -->\n <mat-hint *ngIf=\"computedHint\">{{computedHint}}</mat-hint>\n <div matSuffix class=\"suffix-icons\">\n <!-- Added (WS-1 Phase 3): the house's own offline mark, the same cloud_off spa-offline-indicator, spa-select-common, spa-text-single and spa-text-multi already use, so the state is recognisable without reading \u2014 which is what carries it at phone width, where the hint line has least room. The refresh button below is left exactly as it was and stays reachable on purpose: it is the one action that fixes this once the connection is back, and getData() clears the flag on the first successful load. -->\n <mat-icon *ngIf=\"optionsUnavailableOffline\" class=\"tinyIcon spa-options-unavailable-icon\" [matTooltip]=\"computedHint\" matTooltipPosition=\"above\">cloud_off</mat-icon>\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clear()\" [isHovered]=\"true\" [(value)]=\"value\">\n </spa-suffix>\n </div>\n</mat-form-field>\n", styles: [".spa-options-unavailable ::ng-deep .mat-mdc-form-field-hint{color:#ef6c00}.spa-options-unavailable-icon{color:#ef6c00;vertical-align:middle}\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: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i7.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: i7$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "readonly", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }] }); }
|
|
14857
14877
|
}
|
|
14858
14878
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SelectMultiComponent, decorators: [{
|
|
14859
14879
|
type: Component,
|
|
14860
|
-
args: [{ selector: 'spa-select-multi', standalone: false, template: "<!-- Changed (WS-1 Phase 3): one class added \u2014 no structural change. It tints the hint line the field already renders. Same treatment as spa-select-common, spa-text-single and spa-text-multi; see the .css for why the state is spoken on the field rather than in a toast. -->\n<!-- Changed (placeholder conventions): floatLabel=\"always\" \u2014 with the default 'auto' Material hides the placeholder behind the resting label, so \"Select X\" could never be seen in the empty state the convention is FOR. spa-select-common has always floated for this reason; this brings the multi to parity. -->\n<mat-form-field floatLabel=\"always\" [class.spa-options-unavailable]=\"optionsUnavailableOffline\" [ngStyle]=\"{'width':width ?? '100%'}\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <mat-select [formControl]=\"control\" [required]=\"required\" multiple [placeholder]=\"computedPlaceholder\" [compareWith]=\"compareWith\" (selectionChange)=\"selectionChange($event)\">\n <mat-option *ngIf=\"nullable\" [value]=\"null\">None</mat-option>\n <mat-option *ngFor=\"let option of options\" [value]=\"option[optionValue]\">\n {{option[optionDisplay]}}\n </mat-option>\n </mat-select>\n <!-- Changed (WS-1 Phase 3): the hint now reads computedHint, and \u2014 the part that matters \u2014 it is GATED on computedHint too, not on hint.\n This differs from spa-select-common and spa-text-single only because the surface differs: those bind [hintLabel], which renders whenever\n it is non-empty, whereas this template renders an explicit <mat-hint> behind an *ngIf. Left gated on `hint` the offline notice would have\n been invisible at the ONLY live target in the estate: notifications.component.ts's roleIDs field declares no `hint` at all, so\n form.component.html's [hint]=\"field.hint\" binds undefined and the *ngIf never opens. It would likewise never appear in the filter/option\n context, where option.component.html binds [loadAction] but never binds [hint] \u2014 the exact context where a lookup can fail with nothing\n else on screen to explain it. Both surfaces render the same .mat-mdc-form-field-hint element, so the copied css rule is unchanged. -->\n <mat-hint *ngIf=\"computedHint\">{{computedHint}}</mat-hint>\n <div matSuffix class=\"suffix-icons\">\n <!-- Added (WS-1 Phase 3): the house's own offline mark, the same cloud_off spa-offline-indicator, spa-select-common, spa-text-single and spa-text-multi already use, so the state is recognisable without reading \u2014 which is what carries it at phone width, where the hint line has least room. The refresh button below is left exactly as it was and stays reachable on purpose: it is the one action that fixes this once the connection is back, and getData() clears the flag on the first successful load. -->\n <mat-icon *ngIf=\"optionsUnavailableOffline\" class=\"tinyIcon spa-options-unavailable-icon\" [matTooltip]=\"computedHint\" matTooltipPosition=\"above\">cloud_off</mat-icon>\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"true\" [(value)]=\"value\">\n </spa-suffix>\n </div>\n</mat-form-field>\n", styles: [".spa-options-unavailable ::ng-deep .mat-mdc-form-field-hint{color:#ef6c00}.spa-options-unavailable-icon{color:#ef6c00;vertical-align:middle}\n"] }]
|
|
14880
|
+
args: [{ selector: 'spa-select-multi', standalone: false, template: "<!-- Changed (WS-1 Phase 3): one class added \u2014 no structural change. It tints the hint line the field already renders. Same treatment as spa-select-common, spa-text-single and spa-text-multi; see the .css for why the state is spoken on the field rather than in a toast. -->\n<!-- Changed (placeholder conventions): floatLabel=\"always\" \u2014 with the default 'auto' Material hides the placeholder behind the resting label, so \"Select X\" could never be seen in the empty state the convention is FOR. spa-select-common has always floated for this reason; this brings the multi to parity. -->\n<mat-form-field floatLabel=\"always\" [class.spa-options-unavailable]=\"optionsUnavailableOffline\" [ngStyle]=\"{'width':width ?? '100%'}\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <mat-select [formControl]=\"control\" [required]=\"required\" multiple [placeholder]=\"computedPlaceholder\" [compareWith]=\"compareWith\" (selectionChange)=\"selectionChange($event)\">\n <mat-option *ngIf=\"nullable\" [value]=\"null\">None</mat-option>\n <mat-option *ngFor=\"let option of options\" [value]=\"option[optionValue]\">\n {{option[optionDisplay]}}\n </mat-option>\n </mat-select>\n <!-- Changed (WS-1 Phase 3): the hint now reads computedHint, and \u2014 the part that matters \u2014 it is GATED on computedHint too, not on hint.\n This differs from spa-select-common and spa-text-single only because the surface differs: those bind [hintLabel], which renders whenever\n it is non-empty, whereas this template renders an explicit <mat-hint> behind an *ngIf. Left gated on `hint` the offline notice would have\n been invisible at the ONLY live target in the estate: notifications.component.ts's roleIDs field declares no `hint` at all, so\n form.component.html's [hint]=\"field.hint\" binds undefined and the *ngIf never opens. It would likewise never appear in the filter/option\n context, where option.component.html binds [loadAction] but never binds [hint] \u2014 the exact context where a lookup can fail with nothing\n else on screen to explain it. Both surfaces render the same .mat-mdc-form-field-hint element, so the copied css rule is unchanged. -->\n <mat-hint *ngIf=\"computedHint\">{{computedHint}}</mat-hint>\n <div matSuffix class=\"suffix-icons\">\n <!-- Added (WS-1 Phase 3): the house's own offline mark, the same cloud_off spa-offline-indicator, spa-select-common, spa-text-single and spa-text-multi already use, so the state is recognisable without reading \u2014 which is what carries it at phone width, where the hint line has least room. The refresh button below is left exactly as it was and stays reachable on purpose: it is the one action that fixes this once the connection is back, and getData() clears the flag on the first successful load. -->\n <mat-icon *ngIf=\"optionsUnavailableOffline\" class=\"tinyIcon spa-options-unavailable-icon\" [matTooltip]=\"computedHint\" matTooltipPosition=\"above\">cloud_off</mat-icon>\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clear()\" [isHovered]=\"true\" [(value)]=\"value\">\n </spa-suffix>\n </div>\n</mat-form-field>\n", styles: [".spa-options-unavailable ::ng-deep .mat-mdc-form-field-hint{color:#ef6c00}.spa-options-unavailable-icon{color:#ef6c00;vertical-align:middle}\n"] }]
|
|
14861
14881
|
}], ctorParameters: () => [{ type: MessageService }, { type: DataServiceLib }, { type: ApiErrorService }, { type: OfflineService }], propDecorators: { display: [{
|
|
14862
14882
|
type: Input
|
|
14863
14883
|
}], value: [{
|
|
@@ -16104,6 +16124,93 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
16104
16124
|
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 <!-- Changed (gs-percent-required-only): the gauge now measures REQUIRED steps only, so 100% no longer means\n \"nothing left on the page\" \u2014 optional steps can still be outstanding underneath. Saying \"All setup steps\n are complete\" in that state would be plainly untrue against the list the user is looking at, so the\n celebration splits in two. Same card, same icon, same tone: only the sentence changes, and the second\n variant points DOWN the page instead of closing the subject off. -->\n <div class=\"hero-content celebration\" *ngIf=\"status.percent === 100\">\n <mat-icon class=\"celebrate-icon\">celebration</mat-icon>\n <div class=\"hero-text\" *ngIf=\"status.completed >= status.total\">\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 class=\"hero-text\" *ngIf=\"status.completed < status.total\">\n <h1>You're set up</h1>\n <p>Everything required is done \u2014 your system is ready to operate. The optional extras below are there whenever you want them.</p>\n <span class=\"hero-counter\">{{ status.completed }} of {{ status.total }} steps completed</span>\n </div>\n </div>\n </mat-card>\n\n <!-- Module picker (v2): choose what the business uses; optional modules toggle steps + menus -->\n <!-- Changed (fusion, 2026-08-18): gains a stable id so the \"Choose your modules\" step further down the page can\n scroll here and put focus on the picker. The step is the RECEIPT of this decision; this card is where the\n decision is made, and until now nothing on screen said so. -->\n <!-- Changed (v2-1): the picker is a FOLDING card with the same Day Book subcard head as every other section\n on the page, and it is the gate \u2014 see `gateOpen` in the component. Removed with it: the \"Not confirmed\n yet\" pill the owner asked to drop (\"he does not want it\"). It is not replaced by anything, and it does\n not need to be: before confirmation this card is the ONLY thing on the page, which states the same fact\n far more strongly than a pill ever did. -->\n <mat-card class=\"setup-modules setup-group\" id=\"setup-modules-card\" *ngIf=\"modules.length > 0\" [class.closed]=\"modulesCollapsed\">\n <h2 class=\"group-heading\">\n <button type=\"button\" class=\"group-header tin-disc-row\" (click)=\"toggleModulesCard()\"\n [attr.aria-expanded]=\"!modulesCollapsed\" aria-controls=\"setup-modules-body\"\n [class.group-header-static]=\"!gateOpen\">\n <!-- While the gate is shut the card cannot fold \u2014 a shut picker on a gated page is a blank page \u2014 so it\n shows no disclosure marker, exactly as a non-collapsible spa-table section shows none\n (table.component.html:11). An affordance that does nothing is worse than no affordance. -->\n <span class=\"tin-disc\" *ngIf=\"gateOpen\" [class.open]=\"!modulesCollapsed\"><mat-icon>chevron_right</mat-icon></span>\n <mat-icon class=\"group-icon\">tune</mat-icon>\n <span class=\"group-text\">\n <span class=\"group-name\">Your modules</span>\n <!-- KEPT verbatim, on his instruction. It is the one sentence that says the cards are tappable. -->\n <span class=\"group-caption\">Tap a module to turn it on or off \u2014 you can change this anytime.</span>\n </span>\n <span class=\"group-badges\">\n <span class=\"gb gb-done\">{{ enabledModuleCount }} on</span>\n </span>\n </button>\n </h2>\n <div class=\"group-chips modules-chips\" *ngIf=\"modulesCollapsed\"><span class=\"group-chip-summary\">{{ modulesSummary }}</span></div>\n <div class=\"group-body\" id=\"setup-modules-body\">\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 <!-- Changed (P3): the section head is a real <button>, so the whole band is the target and Enter/Space work\n without a keydown handler. `aria-expanded` and `aria-controls` are on the control, which is where a screen\n reader looks for them. The grid is hidden with display:none, NOT *ngIf \u2014 the ruling of 2026-08-12, and the\n same choice tenant-settings.component.css :69 makes: the cards stay mounted, so the count and the summary\n a shut section shows are LIVE, never a blank that fills in once you open it. It also means a section you\n shut keeps whatever state is inside it. -->\n <div class=\"module-section\" *ngFor=\"let mgroup of moduleGroups; let mi = index\" [class.closed]=\"mgroup.collapsed\">\n <!-- Changed (2026-08-17, owner): a shut band \"looks flat\u2026 like plain text going downwards\u2026 nothing\n attracts someone to see that there are more details when they expand\". The disclosure marker moves to\n the LEFT of the name \u2014 where reading starts \u2014 and the trailing chevron goes, so there is one\n affordance rather than one nobody saw. -->\n <button type=\"button\" class=\"module-section-head tin-disc-row\" (click)=\"toggleModuleSection(mgroup)\"\n [attr.aria-expanded]=\"!mgroup.collapsed\" [attr.aria-controls]=\"'module-grid-' + mi\">\n <span class=\"tin-disc\" [class.open]=\"!mgroup.collapsed\"><mat-icon>chevron_right</mat-icon></span>\n <span class=\"module-section-name\">{{ mgroup.name }}</span>\n <span class=\"module-section-count\">{{ mgroup.enabled }} of {{ mgroup.total }} on</span>\n </button>\n <!-- What a shut section still says about itself: which modules are on, or that none are. Shown only while\n shut \u2014 open, the cards themselves say it, and repeating them there would be noise. -->\n <div class=\"module-section-summary\" *ngIf=\"mgroup.collapsed\">{{ mgroup.summary }}</div>\n <div class=\"module-grid\" [id]=\"'module-grid-' + mi\">\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 <!-- Changed (v3, owner): the label carries FORWARD MOTION, and the trailing arrow is the visual half of\n it. The old \"Confirm selection\" described the click and stopped there; on a page where this button is\n the ONLY thing on screen, nothing said that pressing it reveals the rest. \"Continue\" plus an arrow\n pointing out of the card is the smallest thing that says \"this is step one of more\". -->\n <button mat-flat-button color=\"primary\" class=\"module-confirm\" [disabled]=\"togglingKey !== ''\" (click)=\"confirmModules()\">\n <!-- \u26A0\uFE0F `iconPositionEnd` is REQUIRED and is not cosmetic. MatButton's template carries TWO\n <ng-content select=\"mat-icon\"> slots and content projection fills the FIRST match regardless of\n source order \u2014 so an icon written after the label still renders BEFORE it. Measured here: the\n arrow came out on the left of \"Confirm & continue\" until this attribute was added. -->\n <span>Confirm & continue</span><mat-icon iconPositionEnd class=\"module-confirm-arrow\">arrow_forward</mat-icon>\n </button>\n <!-- Changed (v2-1, owner): the hint no longer names \"Choose your modules\" and no longer mentions Company\n Setup at ALL. Both were removed on instruction, and the reason is the gate: at the moment this\n sentence is read, Company Setup does not exist on the page yet, so pointing at it named something\n the reader cannot see. The sentence now describes only this card and what pressing the button does. -->\n <!-- Changed (v3, owner): \"\u2026the modules you want to proceed\", and then the INTENT \u2014 \"we need you to do\n this part first before the next item\". The second clause is the whole point of the edit: it is the\n only sentence that tells a first-time user this gate is a step and not the entire page. What made\n room for it was dropping \"you can change this at any time\" \u2014 not a loss, because the card's own\n caption six lines up already says \"you can change this anytime\", and saying it twice in one card\n was spending the reader's attention on reassurance instead of direction. -->\n <span class=\"module-hint\">Confirm the modules you want to proceed \u2014 <strong>this comes first</strong>, then your setup steps appear below.</span>\n </div>\n </div>\n </mat-card>\n\n <!-- Category groups, top-down \u2014 Changed: flat spa-checklist replaces the raised mat-accordion -->\n <!-- Changed (GS P4): trackBy is load-bearing, not a micro-optimisation. buildGroups() rebuilds these group\n objects on EVERY refresh(), so without an identity Angular destroyed and recreated every spa-checklist\n underneath \u2014 and a recreated component starts with a blank seededKey, re-adopts config.expandedKey and\n throws away the step the user had opened. Tracking by the category name keeps ONE component instance per\n group alive across a refresh, which is what makes the seededKey guard in checklist.component.ts work. -->\n <!-- Changed (fusion, 2026-08-18): a category group FOLDS. It was the only tier on this page that could not \u2014\n an <h2>, a counter and a permanently open checklist \u2014 while the module bands and the preset bands inside it\n have folded since P3/P4. The Day Book's lane group is the reference: SHUT, the head and its chip strip form\n ONE card carrying a ring, coloured badges and a summary; OPEN, the card dissolves and the checklist rows\n become the content layer. The checkboxes and the crossing-off inside spa-checklist are untouched \u2014 they are\n the thing this page is FOR, and the ring is a summary of them, never a replacement.\n The heading WRAPS the control (the W3C accordion pattern): the whole band stays one target and Enter/Space\n work without a keydown handler, while the category keeps its <h2> for anyone navigating by heading. -->\n <!-- Changed (v2-1, owner): the head is now the DAY BOOK SUBCARD \u2014 a bordered, shaded strip carrying the\n disclosure marker, a section icon, the title and its description, with the status badges pinned to the\n FAR RIGHT. Every measurement is copied from the Day Book's own section header (spa-table's\n .tbl-section-header, table.component.css:205-212) rather than approximated; see the CSS.\n THREE THINGS WERE REMOVED FROM THIS ROW, all on instruction:\n \u2022 the progress ring \u2014 \"it's not coming out exactly the way I want it\". spa-progress-ring itself is\n KEPT and still exported: it is public API and deleting it would break any consumer that uses it.\n This page simply stops calling it. It has no other caller in the library.\n \u2022 the info icon, whose tooltip text is now shown permanently as the caption below the title \u2014 so the\n sentence was not lost, it was promoted. It also still rides on the aria-label, as it did before.\n \u2022 the \"4 of 8\" numeric, which the badges beside it already say in words, and said twice. -->\n <!-- \uD83D\uDD34 THE GATE (v2-1, owner). Everything from here down does not render at all until the modules are\n confirmed. *ngIf and NOT display:none \u2014 deliberately the opposite of the ruling that governs the\n collapses on this page, and for a reason that does not apply to them: a collapsed section is\n information the user may want back in one click, so it stays mounted and live. A gated page is work\n the user must not start yet, and the picker above is about to REBUILD it \u2014 mounting a checklist that\n is going to be replaced wholesale would fire every step's action wiring and every preset load for\n modules that may be about to be switched off. `gateOpen` is true for any tenant who has ever touched\n the picker, and for any app with no module catalog at all, so this is a first-run gate only. -->\n <ng-container *ngIf=\"gateOpen\">\n <mat-card class=\"setup-group\" *ngFor=\"let group of groups; let gi = index; trackBy: trackByGroup\" [class.closed]=\"group.collapsed\" [class.all-done]=\"group.completed >= group.total\">\n <h2 class=\"group-heading\">\n <button type=\"button\" class=\"group-header tin-disc-row\" (click)=\"toggleCategory(group)\"\n [attr.aria-expanded]=\"!group.collapsed\" [attr.aria-controls]=\"'group-body-' + gi\"\n [attr.aria-label]=\"group.name + ' \u2014 ' + group.completed + ' of ' + group.total + ' complete. ' + group.about\">\n <span class=\"tin-disc\" [class.open]=\"!group.collapsed\"><mat-icon>chevron_right</mat-icon></span>\n <mat-icon class=\"group-icon\">{{ group.icon }}</mat-icon>\n <span class=\"group-text\">\n <span class=\"group-name\">{{ group.name }}</span>\n <span class=\"group-caption\">{{ group.about }}</span>\n </span>\n <!-- The coloured badges, now pinned to the far right \u2014 the Day Book's \"free to dispatch\" position.\n Shown in BOTH states, not only shut: they are the section's status line, and a section you have\n opened has not stopped having one. Same green and amber as the ticks they count. -->\n <span class=\"group-badges\">\n <span class=\"gb gb-done\" *ngIf=\"group.done > 0\">{{ group.done }} done</span>\n <span class=\"gb gb-todo\" *ngIf=\"group.todo > 0\">{{ group.todo }} to do</span>\n <span class=\"gb gb-optional\" *ngIf=\"group.optional > 0\">{{ group.optional }} optional</span>\n </span>\n </button>\n </h2>\n <!-- What a shut group still says about itself: what is LEFT, not what is done \u2014 as CHIPS, and nothing else.\n A separate summary line was written here first and removed after seeing it rendered: it named the same\n outstanding steps the chips below already name, so every shut group said \"Add a supplier\" twice, once as\n dead text and once as a control. That is the same defect the module sections closed when they rejected a\n section per module (\"a '1 of 1' counter and a summary repeating the header directly above it\"). The chips\n ARE the summary here, and unlike a sentence they are a way in. -->\n <div class=\"group-chips\" *ngIf=\"group.collapsed\">\n <button type=\"button\" class=\"group-chip\" *ngFor=\"let c of group.chips\" [class.optional]=\"c.optional\" (click)=\"goToStep(group, c.key)\">{{ c.label }}</button>\n <span class=\"group-chip-none\" *ngIf=\"!group.chips.length\"><mat-icon>task_alt</mat-icon>Nothing left here</span>\n </div>\n <!-- \uD83D\uDD34 Hidden with display:none via .closed, NOT *ngIf \u2014 the ruling of 2026-08-12, the same one the module and\n preset sections follow. The steps stay MOUNTED, so the counter, the badges and the summary on a shut group\n are LIVE rather than a blank that fills in once you open it, and a group you shut keeps the step the user\n had expanded and every tick inside it. -->\n <div class=\"group-body\" [id]=\"'group-body-' + gi\">\n <spa-checklist [config]=\"group.config\" [itemTemplate]=\"stepExtrasTpl\"></spa-checklist>\n </div>\n </mat-card>\n\n <!-- Demo data (opt-in via setupConfig.demoData) \u2014 moved here off the per-app configuration pages -->\n <!-- Inside the gate: seeding example records is the last thing a first-run user should be offered, and the\n offers themselves are per-MODULE, so half of them may be about to disappear. -->\n <mat-card class=\"setup-group setup-demo\" *ngIf=\"showDemoData\">\n <!-- Changed (v2-1): the last header on the page that was still the OLD shape \u2014 an <h2> on the left and a\n loose caption on the right. It is now the same subcard as every other section, so the page has ONE\n header language rather than five-and-a-half. It is deliberately NOT a button: this card does not fold,\n it has no steps to count, and a marker that opens nothing is worse than no marker (the same reasoning\n .tbl-section-static carries in the Day Book's own table). -->\n <div class=\"group-header group-header-static\">\n <mat-icon class=\"group-icon\">science</mat-icon>\n <span class=\"group-text\">\n <h2 class=\"group-name\">Demo data</h2>\n <span class=\"group-caption\">Explore with example records, then clear them out</span>\n </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 </ng-container>\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 <!-- Changed (P6): the block gains a stable id so the row's \"Example data\" action can scroll to it and put focus\n on its button. Keyed by step so the ids stay unique across the 19 steps on this page. -->\n <div class=\"seed-offers\" [id]=\"'seed-offers-' + item.data.key\" *ngIf=\"item.data && seedRowsByStep[item.data.key] as seedRows\">\n <!-- \uD83D\uDD34 Changed (2026-08-19, owner): a seed offer is COLLAPSED BY DEFAULT, estate-wide. His reason, and it is\n the whole design: seeding examples is OPTIONAL, and the step card around it only asks Create / View /\n Import \u2014 so making everyone read two explanation lines, a prerequisites line and a notice band before\n they can reach that question was the offer shouting over the step. Shut, this says its name and offers\n to say more. Nothing about the OPEN state changed: the description, effect, \"Also adds\", the notice\n band, the \"already in this list\" status and the Add button are byte-identical to the approved card.\n The fold is the page's own: a real <button> head carrying .tin-disc / .tin-disc-row, exactly as\n .group-header, .module-section-head and .cl-sub-head do \u2014 not a fourth disclosure idiom.\n NOTHING IS PERSISTED \u2014 no expandedKey, no storage, no seeding from `applied`. A fresh load starts every\n offer shut, which is what \"collapsed by default\" has to mean if it is to mean anything.\n *ngIf and NOT display:none, deliberately parting from the 2026-08-12 ruling: that ruling protects folds\n whose SHUT state shows live derived numbers (a \"3 of 5\" count, a summary). This head shows the offer's\n own name and its Added chip, both of which live on the row above the fold, so nothing inside the body\n is read while it is shut and keeping it mounted would buy nothing. -->\n <div class=\"seed-offer\" *ngFor=\"let row of seedRows\" [class.added]=\"row.offer.applied\" [class.open]=\"isSeedOpen(row.offer)\">\n <button type=\"button\" class=\"seed-head tin-disc-row\" (click)=\"toggleSeed(row.offer)\"\n [attr.aria-expanded]=\"isSeedOpen(row.offer)\" [attr.aria-controls]=\"'seed-body-' + row.offer.key\">\n <span class=\"tin-disc\" [class.open]=\"isSeedOpen(row.offer)\"><mat-icon>chevron_right</mat-icon></span>\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 </button>\n <div class=\"seed-body\" [id]=\"'seed-body-' + row.offer.key\" *ngIf=\"isSeedOpen(row.offer)\">\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 </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 <!-- Changed (GS P4): the section band is now a real <button>, exactly as the module sections became in P3 \u2014\n whole band is the target, Enter/Space work without a keydown handler, and aria-expanded/aria-controls sit\n on the control where a screen reader looks for them. The section NAMES now come from the module the\n preset belongs to, so these read Foundation / Finance / Supply Chain \u2014 the same words, in the same\n order, as the Your modules card above. The rows are hidden with display:none, NOT *ngIf (ruling of\n 2026-08-12): the rows stay mounted, so the head's \"n of m\" and the summary line are LIVE in the shut\n state, and a section you shut keeps every tick and every role choice inside it. -->\n <div class=\"preset-section\" *ngFor=\"let pgroup of picker.groups; let pi = index\" [class.closed]=\"pgroup.collapsed\">\n <!-- Changed (2026-08-17): same leading disclosure marker as the module bands above \u2014 the owner's rule is\n that it applies to every section that folds this way, not only the one in his screenshot. -->\n <button type=\"button\" class=\"preset-section-head tin-disc-row\" (click)=\"togglePresetSection(picker, pgroup)\"\n [attr.aria-expanded]=\"!pgroup.collapsed\" [attr.aria-controls]=\"'preset-rows-' + picker.stepKey + '-' + pi\">\n <span class=\"tin-disc\" [class.open]=\"!pgroup.collapsed\"><mat-icon>chevron_right</mat-icon></span>\n <span class=\"preset-section-name\">{{ pgroup.name }}</span>\n <span class=\"preset-section-count\">{{ pgroup.on }} of {{ pgroup.total }}</span>\n </button>\n <!-- What a shut section still says: which of its rules are on, or that none are. Shown only while shut \u2014\n open, the rows themselves say it. -->\n <div class=\"preset-section-summary\" *ngIf=\"pgroup.collapsed\">{{ pgroup.summary }}</div>\n <div class=\"preset-rows\" [id]=\"'preset-rows-' + picker.stepKey + '-' + pi\">\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 <!-- Changed (GS Phase 5): the hand-rolled <select> that sat INSIDE this sentence is gone, replaced by the\n house control. The sentence keeps its job \u2014 it still reads as one sentence, and the roles are still\n written into it where {role} was, so reading the line is still reading the configuration \u2014 but it now\n STATES the choice instead of hosting the editor, and the editor is the library field below it. A\n mat-form-field cannot live mid-sentence at 12px: it carries a floating label and its own hint line,\n and a multi-select's chips need the full width to be readable at 393px.\n With nothing chosen the sentence says so (\"nobody yet\") rather than leaving a hole, because that is\n precisely the state that keeps the row out of the button's count. -->\n <div class=\"preset-effect\" *ngIf=\"!row.preset.exists\">{{ row.before }}<span class=\"preset-role-names\" [class.none]=\"!picker.roleValue[row.preset.key]\">{{ picker.roleNames[row.preset.key] }}</span>{{ row.after }}</div>\n <!-- role=\"group\" + aria-label because spa-select-multi's accessible name is its mat-label, which is the\n same four words on every row; the group names WHICH rule this field belongs to, as the old\n <select>'s aria-label did. click/keydown are stopped exactly as they were on the native select \u2014 the\n field sits inside a row whose whole body toggles the tick.\n The required flag is bound to the tick, not hard-coded: a row nobody has ticked genuinely does not need a\n role, while a TICKED row with no roles is silently dropped by apply \u2014 so this is the one state where\n the field should mark itself, and Material already knows how to. -->\n <div class=\"preset-role-field\" *ngIf=\"!row.preset.exists\" role=\"group\" [attr.aria-label]=\"row.roleAria\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\">\n <spa-select-multi [display]=\"picker.roleLabel\" placeholder=\"Choose roles\" optionDisplay=\"name\" optionValue=\"value\" [options]=\"picker.roles\" [required]=\"!!picker.selected[row.preset.key]\" [value]=\"picker.roleValue[row.preset.key]\" (valueChange)=\"setPresetRoles(picker, row.preset, $event)\"></spa-select-multi>\n </div>\n <div class=\"preset-effect preset-done\" *ngIf=\"row.preset.exists\">{{ picker.doneText }}</div>\n </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:1200px;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-counter{font-size:12px;color:#0000008c}.group-heading{margin:0;font-size:inherit;font-weight:inherit}.group-header{display:flex;align-items:center;width:100%;gap:10px;justify-content:flex-start;border:1px solid rgba(0,0,0,.08);background:#fafafa;font:inherit;color:inherit;text-align:left;padding:10px 12px;border-radius:10px;margin-bottom:8px;transition:border-color .15s ease,background-color .15s ease}.group-header.tin-disc-row{cursor:pointer}.group-header.tin-disc-row:hover{border-color:#90a4ae;background:#f2f5f7}.group-header h2{margin:0}.group-header.tin-disc-row:focus-visible{outline:2px solid #4caf50;outline-offset:2px}.group-header.group-header-static{cursor:default}.group-header.group-header-static:hover{border-color:#00000014;background:#fafafa}.group-text{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1 1 auto}.group-name{font-size:14px;font-weight:600;color:#000000d1}.group-caption{font-size:12.5px;line-height:1.45;color:#0000008c;max-width:82ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.group-icon{flex:0 0 auto;color:#546e7a;font-size:20px!important;width:24px!important;height:24px!important;line-height:24px!important;overflow:visible;display:inline-flex;align-items:center;justify-content:center}.group-badges{display:inline-flex;gap:6px;flex-wrap:wrap;margin-left:auto;flex:0 0 auto}.gb{font-size:11px;font-weight:600;letter-spacing:.02em;border-radius:10px;padding:1px 8px;white-space:nowrap}.gb-done{background:#e8f5e9;color:#2e7d32}.gb-todo{background:#fff3e0;color:#e65100}.gb-optional{background:#0000000f;color:#0000008c}.group-chips{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0 0 34px}.group-chip{display:inline-flex;align-items:center;border:1px solid rgba(0,0,0,.12);border-radius:14px;background:transparent;padding:3px 11px;font:inherit;font-size:12.5px;color:#000000b3;cursor:pointer;transition:border-color .15s,color .15s}.group-chip:hover{border-color:#4caf50;color:#2e7d32}.group-chip:focus-visible{outline:2px solid #4caf50;outline-offset:2px}.group-chip.optional{border-style:dashed;color:#00000080}.group-chip-none{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;color:#2e7d32}.group-chip-none mat-icon{font-size:16px;width:16px;height:16px}.group-chip-summary{font-size:12.5px;color:#0000008c}.modules-chips{margin-left:34px}.setup-group.closed .group-body{display:none}.group-body{margin-top:8px}.setup-group.closed.all-done{border-left:3px solid #4caf50}.setup-modules{padding:16px}.module-section+.module-section{margin-top:18px}.module-section-head{display:flex;align-items:center;gap:12px;width:100%;padding:2px 0 6px;border:0;border-bottom:1px solid rgba(0,0,0,.08);background:none;font:inherit;color:inherit;text-align:left;cursor:pointer}.module-section-head:hover{border-bottom-color:#0000003d}.module-section-head:hover .module-section-name{color:#000000d9}.module-section-head:focus-visible{outline:2px solid #4caf50;outline-offset:2px}.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;margin-left:auto;font-variant-numeric:tabular-nums}.module-section-head{border-radius:6px 6px 0 0;padding-left:6px;padding-right:6px;min-height:36px;transition:background-color .15s ease,border-bottom-color .15s ease}.module-section-head:hover{background:#00000008}.module-section-summary{margin-top:6px;font-size:12.5px;color:#0000008c}.module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;margin-top:8px}.module-section.closed .module-grid{display:none}.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}.module-confirm{display:inline-flex;align-items:center}.module-confirm-arrow{font-size:18px;width:18px;height:18px;transition:transform .15s ease}.module-confirm:hover:not([disabled]) .module-confirm-arrow{transform:translate(2px)}.module-hint strong{font-weight:600;color:#000000bf}.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;font-size:22px;width:24px;height:24px;line-height:24px;overflow:visible;display:inline-flex;align-items:center;justify-content:center}.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;width:100%;padding:0;border:0;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer}.seed-head:focus-visible{outline:2px solid #4caf50;outline-offset:2px;border-radius:6px}.seed-body{padding-left:30px}.seed-state{color:#b0bec5;flex-shrink:0;font-size:22px!important;width:24px!important;height:24px!important;line-height:24px!important;overflow:visible;display:inline-flex;align-items:center;justify-content:center}.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!important;width:16px!important;height:16px!important;line-height:16px!important}.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:center;gap:12px;width:100%;padding:2px 0 6px;border:0;border-bottom:1px solid rgba(0,0,0,.08);background:none;font:inherit;color:inherit;text-align:left;cursor:pointer}.preset-section-head:hover{border-bottom-color:#0000003d}.preset-section-head:hover .preset-section-name{color:#000000d9}.preset-section-head:focus-visible{outline:2px solid #4caf50;outline-offset:2px}.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;margin-left:auto;font-variant-numeric:tabular-nums}.preset-section-head{border-radius:6px 6px 0 0;padding-left:6px;padding-right:6px;min-height:36px;transition:background-color .15s ease,border-bottom-color .15s ease}.preset-section-head:hover{background:#00000008}.preset-section-summary{font-size:12.5px;color:#0000008c}.preset-rows{display:flex;flex-direction:column;gap:8px}.preset-section.closed .preset-rows{display:none}.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;font-size:22px;width:24px;height:24px;line-height:24px;overflow:visible;display:inline-flex;align-items:center;justify-content:center}.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-names{font-weight:500;color:#2e7d32}.preset-role-names.none{color:#ef6c00}.preset-role-field{margin:6px 0 0 32px;max-width:360px}.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;font-size:22px;width:24px;height:24px;line-height:24px;overflow:visible;display:inline-flex;align-items:center;justify-content:center}.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-summary{margin-top:5px;font-size:12px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.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-body{padding-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,.module-section-head{padding:8px 6px 6px;min-height:40px}.tin-disc{width:20px;height:20px}.preset-section-summary{font-size:12px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.preset-rows{gap:6px}.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%;box-sizing:border-box;margin-left:0;padding-left:32px;white-space:normal}.demo-actions button{width:100%}.module-hint{flex:0 0 100%}.preset-role-field{margin:6px 0 0;max-width:none}.preset-email-hint{flex:0 0 100%}}@media (max-width: 700px){.group-header.tin-disc-row{gap:8px;padding:2px 4px;min-height:40px;flex-wrap:wrap}.group-name{font-size:15px;min-width:0;overflow-wrap:anywhere}.group-header .group-counter{font-size:11px}.group-about{font-size:15px;width:15px;height:15px}.group-badges{flex:0 0 calc(100% - 26px);order:3;margin-left:26px}.gb{font-size:10.5px;padding:1px 7px}.group-chips{margin-left:26px;gap:6px}.group-chip{min-height:32px;max-width:100%;text-align:left}.setup-group.closed:hover{transform:none;box-shadow:none}.modules-unconfirmed{flex:0 0 100%;order:3;margin-top:4px}}\n"] }]
|
|
16105
16125
|
}] });
|
|
16106
16126
|
|
|
16127
|
+
// Added (grouped-orderable): the reorder contract, in ONE place, shared by the flat table and the grouped view.
|
|
16128
|
+
// Everything here was lifted verbatim out of table.component's onRowDrop path rather than reimplemented, because
|
|
16129
|
+
// the two views must post the SAME request — the complete ordered id list for one scope, no order numbers,
|
|
16130
|
+
// position IS the order (TinWeb Contracts\ReorderRequest.cs). Two copies of that rule would drift.
|
|
16131
|
+
// Deliberately NOT exported from public-api: this is library-internal plumbing, not new consumer surface.
|
|
16132
|
+
class RowOrderService {
|
|
16133
|
+
constructor(dataService, apiErrorService) {
|
|
16134
|
+
this.dataService = dataService;
|
|
16135
|
+
this.apiErrorService = apiErrorService;
|
|
16136
|
+
}
|
|
16137
|
+
// `orderable: true` is shorthand for {}; false/absent -> null, which every caller reads as "zero affordances".
|
|
16138
|
+
normalize(raw) {
|
|
16139
|
+
return raw ? (raw === true ? {} : raw) : null;
|
|
16140
|
+
}
|
|
16141
|
+
// The row's identity for the posted id list. Callers pass their own fallback keys in priority order, because
|
|
16142
|
+
// the flat table has heroField/rowIdKey/findIdKey behind it and the grouped view does not — but idField must
|
|
16143
|
+
// win everywhere, which is the sharp edge that cost reorder-demo-1 a whole debugging session: a config
|
|
16144
|
+
// carrying loadIDField beats the ...ID convention and every row then reports the PARENT's id.
|
|
16145
|
+
resolveId(row, cfg, fallbackKeys) {
|
|
16146
|
+
const key = cfg?.idField || fallbackKeys.find(k => !!k);
|
|
16147
|
+
return key && row ? row[key] : null;
|
|
16148
|
+
}
|
|
16149
|
+
// Defaults to the FIRST path segment of the load URL + '/reorder', the route the base [HttpPost("reorder")]
|
|
16150
|
+
// action hangs off ('product-groups?action=list' -> 'product-groups/reorder').
|
|
16151
|
+
action(cfg, loadAction) {
|
|
16152
|
+
if (cfg?.action)
|
|
16153
|
+
return cfg.action;
|
|
16154
|
+
const entity = (loadAction?.url || '').split('?')[0].split('/')[0];
|
|
16155
|
+
return { url: `${entity}/reorder`, method: 'post' };
|
|
16156
|
+
}
|
|
16157
|
+
// An explicit scopeValue wins (the scope is often a chip the user picked and is not on the rows at all), then
|
|
16158
|
+
// scopeField read off a row. `fallbackScope` is what the GROUPED view supplies — the group's own key, which is
|
|
16159
|
+
// the scope by construction there. Flat callers pass nothing and keep the previous null-means-tenant-wide result.
|
|
16160
|
+
scope(cfg, rows, fallbackScope) {
|
|
16161
|
+
if (!cfg)
|
|
16162
|
+
return null;
|
|
16163
|
+
if (cfg.scopeValue !== undefined)
|
|
16164
|
+
return cfg.scopeValue;
|
|
16165
|
+
if (cfg.scopeField)
|
|
16166
|
+
return rows?.[0]?.[cfg.scopeField] ?? null;
|
|
16167
|
+
return fallbackScope ?? null;
|
|
16168
|
+
}
|
|
16169
|
+
// Added: the ids are only postable if every row resolved one AND no two rows resolved the SAME one. The short
|
|
16170
|
+
// list was already refused locally; the DUPLICATE list was not, and it is the more confusing of the two — the
|
|
16171
|
+
// server answers HTTP 200 with a body refusal from its set-comparison guard, so the only symptom on screen was
|
|
16172
|
+
// a row springing back, indistinguishable from a genuine stale-page conflict. It is a misconfigured idField
|
|
16173
|
+
// every time, so it is named as one here instead of being posted.
|
|
16174
|
+
validateIds(ids, rowCount) {
|
|
16175
|
+
if (ids.length !== rowCount)
|
|
16176
|
+
return 'These rows cannot be reordered: one or more has no id to save against.';
|
|
16177
|
+
if (new Set(ids).size !== ids.length)
|
|
16178
|
+
return 'These rows cannot be reordered: two or more share the same id. Set orderable.idField to the row\'s own id.';
|
|
16179
|
+
return null;
|
|
16180
|
+
}
|
|
16181
|
+
// Optimistic move already happened in the caller; this posts it and puts it back if the server says no.
|
|
16182
|
+
persist(cfg, loadAction, ids, scope, handlers) {
|
|
16183
|
+
const action = this.action(cfg, loadAction);
|
|
16184
|
+
this.dataService.CallApi(action, { scope, ids }).subscribe({
|
|
16185
|
+
next: (response) => {
|
|
16186
|
+
if (response?.success)
|
|
16187
|
+
return; // quiet settle — a successful drag is its own feedback, no snackbar
|
|
16188
|
+
handlers.revert();
|
|
16189
|
+
// A conflict means somebody else changed this scope, so the rows on screen are stale and reverting alone
|
|
16190
|
+
// would leave the user looking at a list the server disagrees with. Refuse AND refresh.
|
|
16191
|
+
if (response?.reason === 'conflict' && handlers.refresh)
|
|
16192
|
+
handlers.refresh();
|
|
16193
|
+
this.apiErrorService.presentAppFailure(response, 'action', action.url); // never raw server text on a user surface
|
|
16194
|
+
},
|
|
16195
|
+
error: (err) => {
|
|
16196
|
+
handlers.revert();
|
|
16197
|
+
this.apiErrorService.present(err); // transport failure — classified and deduped, never a raw error string
|
|
16198
|
+
}
|
|
16199
|
+
});
|
|
16200
|
+
}
|
|
16201
|
+
// The local refusals above still have to SAY something, and both views route them through the same door the
|
|
16202
|
+
// server's refusals use, so a user sees one consistent failure surface.
|
|
16203
|
+
presentLocalRefusal(message, url) {
|
|
16204
|
+
this.apiErrorService.presentAppFailure({ success: false, message }, 'action', url);
|
|
16205
|
+
}
|
|
16206
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: RowOrderService, deps: [{ token: DataServiceLib }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
16207
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: RowOrderService, providedIn: 'root' }); }
|
|
16208
|
+
}
|
|
16209
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: RowOrderService, decorators: [{
|
|
16210
|
+
type: Injectable,
|
|
16211
|
+
args: [{ providedIn: 'root' }]
|
|
16212
|
+
}], ctorParameters: () => [{ type: DataServiceLib }, { type: ApiErrorService }] });
|
|
16213
|
+
|
|
16107
16214
|
// Quiet Loading — perceived-progress engine (FSD D1).
|
|
16108
16215
|
//
|
|
16109
16216
|
// Maps elapsed time to a percentage on a deterministic, asymptotic curve: a steady climb from zero that
|
|
@@ -16498,11 +16605,11 @@ class TextComponent {
|
|
|
16498
16605
|
return "";
|
|
16499
16606
|
}
|
|
16500
16607
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TextComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
16501
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TextComponent, isStandalone: false, selector: "spa-text", inputs: { appearance: "appearance", readonly: "readonly", hint: "hint", display: "display", placeholder: "placeholder", value: "value", format: "format", type: "type", width: "width", copyContent: "copyContent", clearContent: "clearContent", required: "required", min: "min", max: "max", regex: "regex", suffix: "suffix", infoMessage: "infoMessage" }, outputs: { valueChange: "valueChange", leave: "leave", enterPress: "enterPress" }, usesOnChanges: true, ngImport: i0, template: "<!-- Simple text input - Password, textarea, and autocomplete moved to separate components -->\n<mat-form-field [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [hintLabel]=\"hint\" [appearance]=\"appearance ?? 'fill'\" [ngStyle]=\"{'width':width ?? '100%'}\" style=\"margin-right: 5px;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label *ngIf=\"format=='text'\">{{display}}</mat-label>\n <mat-label *ngIf=\"format=='date'\">{{display | date:'dd/MM/yyyy'}}</mat-label>\n <input matInput autocomplete=\"off\" (blur)=\"leaved()\" (keyup.enter)=\"enterPressed()\" [type]=\"type\" [placeholder]=\"computedPlaceholder\" [formControl]=\"control\" [required]=\"required\" [readonly]=\"readonly\"/>\n <mat-error *ngIf=\"control.invalid && (control.dirty || control.touched)\">{{validate(control)}}</mat-error>\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [""], dependencies: [{ 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: i2.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }] }); }
|
|
16608
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TextComponent, isStandalone: false, selector: "spa-text", inputs: { appearance: "appearance", readonly: "readonly", hint: "hint", display: "display", placeholder: "placeholder", value: "value", format: "format", type: "type", width: "width", copyContent: "copyContent", clearContent: "clearContent", required: "required", min: "min", max: "max", regex: "regex", suffix: "suffix", infoMessage: "infoMessage" }, outputs: { valueChange: "valueChange", leave: "leave", enterPress: "enterPress" }, usesOnChanges: true, ngImport: i0, template: "<!-- Simple text input - Password, textarea, and autocomplete moved to separate components -->\n<mat-form-field [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [hintLabel]=\"hint\" [appearance]=\"appearance ?? 'fill'\" [ngStyle]=\"{'width':width ?? '100%'}\" style=\"margin-right: 5px;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label *ngIf=\"format=='text'\">{{display}}</mat-label>\n <mat-label *ngIf=\"format=='date'\">{{display | date:'dd/MM/yyyy'}}</mat-label>\n <input matInput autocomplete=\"off\" (blur)=\"leaved()\" (keyup.enter)=\"enterPressed()\" [type]=\"type\" [placeholder]=\"computedPlaceholder\" [formControl]=\"control\" [required]=\"required\" [readonly]=\"readonly\"/>\n <mat-error *ngIf=\"control.invalid && (control.dirty || control.touched)\">{{validate(control)}}</mat-error>\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [""], dependencies: [{ 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: i2.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "readonly", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }] }); }
|
|
16502
16609
|
}
|
|
16503
16610
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TextComponent, decorators: [{
|
|
16504
16611
|
type: Component,
|
|
16505
|
-
args: [{ selector: 'spa-text', standalone: false, template: "<!-- Simple text input - Password, textarea, and autocomplete moved to separate components -->\n<mat-form-field [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [hintLabel]=\"hint\" [appearance]=\"appearance ?? 'fill'\" [ngStyle]=\"{'width':width ?? '100%'}\" style=\"margin-right: 5px;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label *ngIf=\"format=='text'\">{{display}}</mat-label>\n <mat-label *ngIf=\"format=='date'\">{{display | date:'dd/MM/yyyy'}}</mat-label>\n <input matInput autocomplete=\"off\" (blur)=\"leaved()\" (keyup.enter)=\"enterPressed()\" [type]=\"type\" [placeholder]=\"computedPlaceholder\" [formControl]=\"control\" [required]=\"required\" [readonly]=\"readonly\"/>\n <mat-error *ngIf=\"control.invalid && (control.dirty || control.touched)\">{{validate(control)}}</mat-error>\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n" }]
|
|
16612
|
+
args: [{ selector: 'spa-text', standalone: false, template: "<!-- Simple text input - Password, textarea, and autocomplete moved to separate components -->\n<mat-form-field [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [hintLabel]=\"hint\" [appearance]=\"appearance ?? 'fill'\" [ngStyle]=\"{'width':width ?? '100%'}\" style=\"margin-right: 5px;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label *ngIf=\"format=='text'\">{{display}}</mat-label>\n <mat-label *ngIf=\"format=='date'\">{{display | date:'dd/MM/yyyy'}}</mat-label>\n <input matInput autocomplete=\"off\" (blur)=\"leaved()\" (keyup.enter)=\"enterPressed()\" [type]=\"type\" [placeholder]=\"computedPlaceholder\" [formControl]=\"control\" [required]=\"required\" [readonly]=\"readonly\"/>\n <mat-error *ngIf=\"control.invalid && (control.dirty || control.touched)\">{{validate(control)}}</mat-error>\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n" }]
|
|
16506
16613
|
}], ctorParameters: () => [], propDecorators: { appearance: [{
|
|
16507
16614
|
type: Input
|
|
16508
16615
|
}], readonly: [{
|
|
@@ -16764,7 +16871,7 @@ class TextSingleComponent {
|
|
|
16764
16871
|
}
|
|
16765
16872
|
// Changed: Call onSelectChange callback if provided via field (only fires on explicit option selection)
|
|
16766
16873
|
if (this.field?.onSelectChange && this.data) {
|
|
16767
|
-
this.field.onSelectChange(valueToEmit, this.data, selectedOption);
|
|
16874
|
+
this.field.onSelectChange(valueToEmit, this.data, selectedOption, this.field.formConfig); // Changed (addpig-label-leak): forward the LIVE FormConfig stamped on the field by spa-form — same reason as spa-select-common, this is the third place the hook is invoked from
|
|
16768
16875
|
}
|
|
16769
16876
|
}
|
|
16770
16877
|
// Added: THE KEYBOARD PATH. The <input> carried no (input), no (change) and no (blur), and leaved() below was
|
|
@@ -17038,11 +17145,11 @@ class TextSingleComponent {
|
|
|
17038
17145
|
this.valueChange.emit(this.value);
|
|
17039
17146
|
}
|
|
17040
17147
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TextSingleComponent, deps: [{ token: DataServiceLib }, { token: DialogService }, { token: ButtonService }, { token: MessageService }, { token: ApiErrorService }, { token: OfflineService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
17041
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TextSingleComponent, isStandalone: false, selector: "spa-text-single", inputs: { appearance: "appearance", readonly: "readonly", hint: "hint", display: "display", placeholder: "placeholder", value: "value", width: "width", copyContent: "copyContent", clearContent: "clearContent", options: "options", optionDisplay: "optionDisplay", optionValue: "optionValue", loadAction: "loadAction", required: "required", min: "min", max: "max", regex: "regex", suffix: "suffix", infoMessage: "infoMessage", field: "field", data: "data", detailsConfig: "detailsConfig", masterField: "masterField" }, outputs: { valueChange: "valueChange", leave: "leave", enterPress: "enterPress" }, usesOnChanges: true, ngImport: i0, template: "<!-- Autocomplete text input -->\n<!-- Changed (WS-1 Phase 3): hintLabel now reads computedHint, plus one class \u2014 no structural change. It tints the hint line the field already renders. Same treatment as spa-select-common; see the .css for why the state is spoken on the field rather than in a toast. -->\n<!-- Changed (placeholder conventions): floatLabel=\"always\" \u2014 with 'auto' the resting label covers the placeholder, so \"Enter or select Breed\" was never visible in the empty state it exists to explain. The floated label stays the CLEAN label ({{display}}) \u2014 the verb lives only in the placeholder. -->\n<mat-form-field floatLabel=\"always\" [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [hintLabel]=\"computedHint\" [class.spa-options-unavailable]=\"optionsUnavailableOffline\" [appearance]=\"appearance ?? 'fill'\" [ngStyle]=\"{'width':width ?? '100%'}\" style=\"margin-right: 5px;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <!-- Changed: (input) and (blur) added. This input had NO event binding of any kind, and leaved() in the .ts was bound to nothing, so (optionSelected) below was the only route to the model \u2014 every keystroke, including emptying the box, was discarded while the screen showed otherwise. (input) is a DOM event, so it fires on real typing only and never on a programmatic setValue. -->\n <input [placeholder]=\"computedPlaceholder\" [formControl]=\"myControl\" matInput [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\" [required]=\"required\" [readonly]=\"readonly\" (input)=\"typed()\" (blur)=\"leaved()\">\n <mat-error *ngIf=\"myControl.invalid && (myControl.dirty || myControl.touched)\">{{validate(myControl)}}</mat-error>\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"onOptionSelected($event)\"> <!-- Changed: route through onOptionSelected so the \"add new\" option opens the create dialog -->\n <!-- Added (dropdown quick-add): always-first create option \u2014 outside the filter, so it shows even when nothing matches -->\n <mat-option *ngIf=\"addOptionLabel\" [value]=\"ADD_NEW_OPTION\" class=\"spa-add-new-option\">\n <mat-icon class=\"tinyIcon spa-add-new-icon\">add</mat-icon>{{addOptionLabel}}\n </mat-option>\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"multiDimension ? option[optionDisplay] : option\">\n {{multiDimension ? option[optionDisplay] : option}}\n </mat-option>\n </mat-autocomplete>\n <div matSuffix class=\"suffix-icons\">\n <!-- Changed: Added Add button for creating new items via detailsConfig -->\n <button mat-icon-button *ngIf=\"detailsConfig && canCreate() && isHovered\" (click)=\"onPeekClick($event, 'create')\" matTooltip=\"Add\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" style=\"color: green;\">add</mat-icon>\n </button>\n <!-- Changed: Added View button for viewing selected item via detailsConfig -->\n <button mat-icon-button *ngIf=\"detailsConfig && canView() && isHovered && value\" (click)=\"onPeekClick($event, 'view')\" matTooltip=\"View\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" color=\"primary\">launch</mat-icon>\n </button>\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <!-- Added (WS-1 Phase 3): the house's own offline mark, the same cloud_off spa-offline-indicator and spa-select-common already use, so the state is recognisable without reading \u2014 which is what carries it at phone width, where the hint line has least room. The refresh button below is left exactly as it was and stays reachable on purpose: it is the one action that fixes this once the connection is back, and getData() clears the flag on the first successful load. -->\n <mat-icon *ngIf=\"optionsUnavailableOffline\" class=\"tinyIcon spa-options-unavailable-icon\" [matTooltip]=\"computedHint\" matTooltipPosition=\"above\">cloud_off</mat-icon>\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [".spa-add-new-option{border-bottom:1px solid rgba(0,0,0,.12);font-weight:500;color:green}.spa-add-new-icon{color:green;vertical-align:middle;margin-right:4px}.spa-options-unavailable ::ng-deep .mat-mdc-form-field-hint{color:#ef6c00}.spa-options-unavailable-icon{color:#ef6c00;vertical-align:middle}\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: i2.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i14.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "directive", type: i14.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "directive", type: i7$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }] }); }
|
|
17148
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TextSingleComponent, isStandalone: false, selector: "spa-text-single", inputs: { appearance: "appearance", readonly: "readonly", hint: "hint", display: "display", placeholder: "placeholder", value: "value", width: "width", copyContent: "copyContent", clearContent: "clearContent", options: "options", optionDisplay: "optionDisplay", optionValue: "optionValue", loadAction: "loadAction", required: "required", min: "min", max: "max", regex: "regex", suffix: "suffix", infoMessage: "infoMessage", field: "field", data: "data", detailsConfig: "detailsConfig", masterField: "masterField" }, outputs: { valueChange: "valueChange", leave: "leave", enterPress: "enterPress" }, usesOnChanges: true, ngImport: i0, template: "<!-- Autocomplete text input -->\n<!-- Changed (WS-1 Phase 3): hintLabel now reads computedHint, plus one class \u2014 no structural change. It tints the hint line the field already renders. Same treatment as spa-select-common; see the .css for why the state is spoken on the field rather than in a toast. -->\n<!-- Changed (placeholder conventions): floatLabel=\"always\" \u2014 with 'auto' the resting label covers the placeholder, so \"Enter or select Breed\" was never visible in the empty state it exists to explain. The floated label stays the CLEAN label ({{display}}) \u2014 the verb lives only in the placeholder. -->\n<mat-form-field floatLabel=\"always\" [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [hintLabel]=\"computedHint\" [class.spa-options-unavailable]=\"optionsUnavailableOffline\" [appearance]=\"appearance ?? 'fill'\" [ngStyle]=\"{'width':width ?? '100%'}\" style=\"margin-right: 5px;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <!-- Changed: (input) and (blur) added. This input had NO event binding of any kind, and leaved() in the .ts was bound to nothing, so (optionSelected) below was the only route to the model \u2014 every keystroke, including emptying the box, was discarded while the screen showed otherwise. (input) is a DOM event, so it fires on real typing only and never on a programmatic setValue. -->\n <input [placeholder]=\"computedPlaceholder\" [formControl]=\"myControl\" matInput [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\" [required]=\"required\" [readonly]=\"readonly\" (input)=\"typed()\" (blur)=\"leaved()\">\n <mat-error *ngIf=\"myControl.invalid && (myControl.dirty || myControl.touched)\">{{validate(myControl)}}</mat-error>\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"onOptionSelected($event)\"> <!-- Changed: route through onOptionSelected so the \"add new\" option opens the create dialog -->\n <!-- Added (dropdown quick-add): always-first create option \u2014 outside the filter, so it shows even when nothing matches -->\n <mat-option *ngIf=\"addOptionLabel\" [value]=\"ADD_NEW_OPTION\" class=\"spa-add-new-option\">\n <mat-icon class=\"tinyIcon spa-add-new-icon\">add</mat-icon>{{addOptionLabel}}\n </mat-option>\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"multiDimension ? option[optionDisplay] : option\">\n {{multiDimension ? option[optionDisplay] : option}}\n </mat-option>\n </mat-autocomplete>\n <div matSuffix class=\"suffix-icons\">\n <!-- Changed: Added Add button for creating new items via detailsConfig -->\n <button mat-icon-button *ngIf=\"detailsConfig && canCreate() && isHovered\" (click)=\"onPeekClick($event, 'create')\" matTooltip=\"Add\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" style=\"color: green;\">add</mat-icon>\n </button>\n <!-- Changed: Added View button for viewing selected item via detailsConfig -->\n <button mat-icon-button *ngIf=\"detailsConfig && canView() && isHovered && value\" (click)=\"onPeekClick($event, 'view')\" matTooltip=\"View\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" color=\"primary\">launch</mat-icon>\n </button>\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <!-- Added (WS-1 Phase 3): the house's own offline mark, the same cloud_off spa-offline-indicator and spa-select-common already use, so the state is recognisable without reading \u2014 which is what carries it at phone width, where the hint line has least room. The refresh button below is left exactly as it was and stays reachable on purpose: it is the one action that fixes this once the connection is back, and getData() clears the flag on the first successful load. -->\n <mat-icon *ngIf=\"optionsUnavailableOffline\" class=\"tinyIcon spa-options-unavailable-icon\" [matTooltip]=\"computedHint\" matTooltipPosition=\"above\">cloud_off</mat-icon>\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [".spa-add-new-option{border-bottom:1px solid rgba(0,0,0,.12);font-weight:500;color:green}.spa-add-new-icon{color:green;vertical-align:middle;margin-right:4px}.spa-options-unavailable ::ng-deep .mat-mdc-form-field-hint{color:#ef6c00}.spa-options-unavailable-icon{color:#ef6c00;vertical-align:middle}\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: i2.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i14.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "directive", type: i14.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "directive", type: i7$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "readonly", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }] }); }
|
|
17042
17149
|
}
|
|
17043
17150
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TextSingleComponent, decorators: [{
|
|
17044
17151
|
type: Component,
|
|
17045
|
-
args: [{ selector: 'spa-text-single', standalone: false, template: "<!-- Autocomplete text input -->\n<!-- Changed (WS-1 Phase 3): hintLabel now reads computedHint, plus one class \u2014 no structural change. It tints the hint line the field already renders. Same treatment as spa-select-common; see the .css for why the state is spoken on the field rather than in a toast. -->\n<!-- Changed (placeholder conventions): floatLabel=\"always\" \u2014 with 'auto' the resting label covers the placeholder, so \"Enter or select Breed\" was never visible in the empty state it exists to explain. The floated label stays the CLEAN label ({{display}}) \u2014 the verb lives only in the placeholder. -->\n<mat-form-field floatLabel=\"always\" [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [hintLabel]=\"computedHint\" [class.spa-options-unavailable]=\"optionsUnavailableOffline\" [appearance]=\"appearance ?? 'fill'\" [ngStyle]=\"{'width':width ?? '100%'}\" style=\"margin-right: 5px;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <!-- Changed: (input) and (blur) added. This input had NO event binding of any kind, and leaved() in the .ts was bound to nothing, so (optionSelected) below was the only route to the model \u2014 every keystroke, including emptying the box, was discarded while the screen showed otherwise. (input) is a DOM event, so it fires on real typing only and never on a programmatic setValue. -->\n <input [placeholder]=\"computedPlaceholder\" [formControl]=\"myControl\" matInput [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\" [required]=\"required\" [readonly]=\"readonly\" (input)=\"typed()\" (blur)=\"leaved()\">\n <mat-error *ngIf=\"myControl.invalid && (myControl.dirty || myControl.touched)\">{{validate(myControl)}}</mat-error>\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"onOptionSelected($event)\"> <!-- Changed: route through onOptionSelected so the \"add new\" option opens the create dialog -->\n <!-- Added (dropdown quick-add): always-first create option \u2014 outside the filter, so it shows even when nothing matches -->\n <mat-option *ngIf=\"addOptionLabel\" [value]=\"ADD_NEW_OPTION\" class=\"spa-add-new-option\">\n <mat-icon class=\"tinyIcon spa-add-new-icon\">add</mat-icon>{{addOptionLabel}}\n </mat-option>\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"multiDimension ? option[optionDisplay] : option\">\n {{multiDimension ? option[optionDisplay] : option}}\n </mat-option>\n </mat-autocomplete>\n <div matSuffix class=\"suffix-icons\">\n <!-- Changed: Added Add button for creating new items via detailsConfig -->\n <button mat-icon-button *ngIf=\"detailsConfig && canCreate() && isHovered\" (click)=\"onPeekClick($event, 'create')\" matTooltip=\"Add\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" style=\"color: green;\">add</mat-icon>\n </button>\n <!-- Changed: Added View button for viewing selected item via detailsConfig -->\n <button mat-icon-button *ngIf=\"detailsConfig && canView() && isHovered && value\" (click)=\"onPeekClick($event, 'view')\" matTooltip=\"View\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" color=\"primary\">launch</mat-icon>\n </button>\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <!-- Added (WS-1 Phase 3): the house's own offline mark, the same cloud_off spa-offline-indicator and spa-select-common already use, so the state is recognisable without reading \u2014 which is what carries it at phone width, where the hint line has least room. The refresh button below is left exactly as it was and stays reachable on purpose: it is the one action that fixes this once the connection is back, and getData() clears the flag on the first successful load. -->\n <mat-icon *ngIf=\"optionsUnavailableOffline\" class=\"tinyIcon spa-options-unavailable-icon\" [matTooltip]=\"computedHint\" matTooltipPosition=\"above\">cloud_off</mat-icon>\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [".spa-add-new-option{border-bottom:1px solid rgba(0,0,0,.12);font-weight:500;color:green}.spa-add-new-icon{color:green;vertical-align:middle;margin-right:4px}.spa-options-unavailable ::ng-deep .mat-mdc-form-field-hint{color:#ef6c00}.spa-options-unavailable-icon{color:#ef6c00;vertical-align:middle}\n"] }]
|
|
17152
|
+
args: [{ selector: 'spa-text-single', standalone: false, template: "<!-- Autocomplete text input -->\n<!-- Changed (WS-1 Phase 3): hintLabel now reads computedHint, plus one class \u2014 no structural change. It tints the hint line the field already renders. Same treatment as spa-select-common; see the .css for why the state is spoken on the field rather than in a toast. -->\n<!-- Changed (placeholder conventions): floatLabel=\"always\" \u2014 with 'auto' the resting label covers the placeholder, so \"Enter or select Breed\" was never visible in the empty state it exists to explain. The floated label stays the CLEAN label ({{display}}) \u2014 the verb lives only in the placeholder. -->\n<mat-form-field floatLabel=\"always\" [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [hintLabel]=\"computedHint\" [class.spa-options-unavailable]=\"optionsUnavailableOffline\" [appearance]=\"appearance ?? 'fill'\" [ngStyle]=\"{'width':width ?? '100%'}\" style=\"margin-right: 5px;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <!-- Changed: (input) and (blur) added. This input had NO event binding of any kind, and leaved() in the .ts was bound to nothing, so (optionSelected) below was the only route to the model \u2014 every keystroke, including emptying the box, was discarded while the screen showed otherwise. (input) is a DOM event, so it fires on real typing only and never on a programmatic setValue. -->\n <input [placeholder]=\"computedPlaceholder\" [formControl]=\"myControl\" matInput [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\" [required]=\"required\" [readonly]=\"readonly\" (input)=\"typed()\" (blur)=\"leaved()\">\n <mat-error *ngIf=\"myControl.invalid && (myControl.dirty || myControl.touched)\">{{validate(myControl)}}</mat-error>\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"onOptionSelected($event)\"> <!-- Changed: route through onOptionSelected so the \"add new\" option opens the create dialog -->\n <!-- Added (dropdown quick-add): always-first create option \u2014 outside the filter, so it shows even when nothing matches -->\n <mat-option *ngIf=\"addOptionLabel\" [value]=\"ADD_NEW_OPTION\" class=\"spa-add-new-option\">\n <mat-icon class=\"tinyIcon spa-add-new-icon\">add</mat-icon>{{addOptionLabel}}\n </mat-option>\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"multiDimension ? option[optionDisplay] : option\">\n {{multiDimension ? option[optionDisplay] : option}}\n </mat-option>\n </mat-autocomplete>\n <div matSuffix class=\"suffix-icons\">\n <!-- Changed: Added Add button for creating new items via detailsConfig -->\n <button mat-icon-button *ngIf=\"detailsConfig && canCreate() && isHovered\" (click)=\"onPeekClick($event, 'create')\" matTooltip=\"Add\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" style=\"color: green;\">add</mat-icon>\n </button>\n <!-- Changed: Added View button for viewing selected item via detailsConfig -->\n <button mat-icon-button *ngIf=\"detailsConfig && canView() && isHovered && value\" (click)=\"onPeekClick($event, 'view')\" matTooltip=\"View\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" color=\"primary\">launch</mat-icon>\n </button>\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <!-- Added (WS-1 Phase 3): the house's own offline mark, the same cloud_off spa-offline-indicator and spa-select-common already use, so the state is recognisable without reading \u2014 which is what carries it at phone width, where the hint line has least room. The refresh button below is left exactly as it was and stays reachable on purpose: it is the one action that fixes this once the connection is back, and getData() clears the flag on the first successful load. -->\n <mat-icon *ngIf=\"optionsUnavailableOffline\" class=\"tinyIcon spa-options-unavailable-icon\" [matTooltip]=\"computedHint\" matTooltipPosition=\"above\">cloud_off</mat-icon>\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [".spa-add-new-option{border-bottom:1px solid rgba(0,0,0,.12);font-weight:500;color:green}.spa-add-new-icon{color:green;vertical-align:middle;margin-right:4px}.spa-options-unavailable ::ng-deep .mat-mdc-form-field-hint{color:#ef6c00}.spa-options-unavailable-icon{color:#ef6c00;vertical-align:middle}\n"] }]
|
|
17046
17153
|
}], ctorParameters: () => [{ type: DataServiceLib }, { type: DialogService }, { type: ButtonService }, { type: MessageService }, { type: ApiErrorService }, { type: OfflineService }], propDecorators: { appearance: [{
|
|
17047
17154
|
type: Input
|
|
17048
17155
|
}], readonly: [{
|
|
@@ -17265,11 +17372,11 @@ class DateComponent {
|
|
|
17265
17372
|
return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
|
|
17266
17373
|
}
|
|
17267
17374
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
17268
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: DateComponent, isStandalone: false, selector: "spa-date", inputs: { required: "required", min: "min", max: "max", boundContext: "boundContext", readonly: "readonly", hint: "hint", value: "value", display: "display", placeholder: "placeholder", width: "width", suffix: "suffix", infoMessage: "infoMessage", copyContent: "copyContent", clearContent: "clearContent" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: "<!-- Changed: Added mouse events for hover tracking, click handler for opening picker, and spa-suffix component -->\n<!-- Changed (placeholder conventions): floatLabel=\"always\" so the \"Pick X\" affordance is visible in the EMPTY state, which is the only state it is for. The floated label stays the clean label ({{display}}). -->\n<!-- Changed (2026-08-19, owner \"make sure you don't miss any\"): [hintLabel] was NEVER BOUND on this component,\n so `@Input() hint` (date.component.ts:22) was a property that did nothing \u2014 a hint set on a date field\n rendered no line at all. Found while sweeping every hint render site for the italic treatment: this one\n had no render site to style. SIX date fields in the estate already set a hint and showed nothing \u2014\n piggery.service.ts:666 and :1000 (\"Leave blank if the birth date is not known\", both on the Add Pig and\n Record Purchase forms), datalib.service.ts:718, payroll.service.ts:392 and :393. grip.service.ts:647 had\n already DIAGNOSED this in a comment and worked around it by using infoMessage instead. Binding hintLabel\n is the same idiom every sibling uses (text:2, number:2, money:3, text-area:2, text-mask:2), so the hint\n now lands in the standard subscript and picks up the italic rule with everything else. -->\n<mat-form-field floatLabel=\"always\" [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [hintLabel]=\"hint\" [ngStyle]=\"{'width':width ?? '100%'}\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\"><!-- Changed: [hideRequiredMarker] was absent, so a required date never showed the asterisk that text.component.html:2 has always shown -->\n <mat-label>{{display}}</mat-label>\n <input [formControl]=\"control\" [required]=\"required\" [min]=\"minDate.value\" [max]=\"maxDate.value\" matInput [matDatepicker]=\"picker_date\" (dateInput)=\"onChangeEvent()\" [placeholder]=\"computedPlaceholder\" [readonly]=\"true\" (click)=\"onInputClick(picker_date)\"><!-- Changed: [required] was never bound, so the input rendered aria-required=\"false\" on a field the form treated as mandatory -->\n <mat-datepicker #picker_date></mat-datepicker><!-- Note (SBD-1): (opened)=\"refreshBounds()\" was tried here FIRST and MEASURED NOT TO FIRE \u2014 opening by the toggle icon called refreshBounds 0 times, opening by the input called it 1. The refresh is done on the toggle itself instead, below. -->\n <mat-error *ngIf=\"control.invalid && (control.dirty || control.touched)\">{{validate(control)}}</mat-error><!-- Changed: gated on dirty/touched for parity with text.component.html:6 \u2014 an untouched field must not scold the user on open -->\n <div matSuffix class=\"suffix-icons\">\n <mat-datepicker-toggle [for]=\"picker_date\" (mousedown)=\"refreshBounds()\" (keydown)=\"refreshBounds()\"></mat-datepicker-toggle><!-- Added (SBD-1): THE CALENDAR ICON IS HOW PEOPLE ACTUALLY OPEN THIS, and it bypassed every refresh there was \u2014 onInputClick only fires for a click on the text box, and (opened) on the datepicker was measured not to fire at all. So a bound needing a re-read at open time (a function bound, or 'today' after midnight) was correct when opened one way and stale when opened the other. mousedown/keydown are used rather than click deliberately: BOTH FIRE BEFORE the activation that opens the picker, so the window is already correct when the calendar is built, instead of depending on an open calendar re-rendering when its bounds change underneath it. -->\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix><!-- Changed: (clearClick) was missing, so the X wrote the suffix's own dead value and the clear never reached the form -->\n </div>\n</mat-form-field>\n", styles: [""], dependencies: [{ 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: i2.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i5$1.MatDatepicker, selector: "mat-datepicker", exportAs: ["matDatepicker"] }, { kind: "directive", type: i5$1.MatDatepickerInput, selector: "input[matDatepicker]", inputs: ["matDatepicker", "min", "max", "matDatepickerFilter"], exportAs: ["matDatepickerInput"] }, { kind: "component", type: i5$1.MatDatepickerToggle, selector: "mat-datepicker-toggle", inputs: ["for", "tabIndex", "aria-label", "disabled", "disableRipple"], exportAs: ["matDatepickerToggle"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }] }); }
|
|
17375
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: DateComponent, isStandalone: false, selector: "spa-date", inputs: { required: "required", min: "min", max: "max", boundContext: "boundContext", readonly: "readonly", hint: "hint", value: "value", display: "display", placeholder: "placeholder", width: "width", suffix: "suffix", infoMessage: "infoMessage", copyContent: "copyContent", clearContent: "clearContent" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: "<!-- Changed: Added mouse events for hover tracking, click handler for opening picker, and spa-suffix component -->\n<!-- Changed (placeholder conventions): floatLabel=\"always\" so the \"Pick X\" affordance is visible in the EMPTY state, which is the only state it is for. The floated label stays the clean label ({{display}}). -->\n<!-- Changed (2026-08-19, owner \"make sure you don't miss any\"): [hintLabel] was NEVER BOUND on this component,\n so `@Input() hint` (date.component.ts:22) was a property that did nothing \u2014 a hint set on a date field\n rendered no line at all. Found while sweeping every hint render site for the italic treatment: this one\n had no render site to style. SIX date fields in the estate already set a hint and showed nothing \u2014\n piggery.service.ts:666 and :1000 (\"Leave blank if the birth date is not known\", both on the Add Pig and\n Record Purchase forms), datalib.service.ts:718, payroll.service.ts:392 and :393. grip.service.ts:647 had\n already DIAGNOSED this in a comment and worked around it by using infoMessage instead. Binding hintLabel\n is the same idiom every sibling uses (text:2, number:2, money:3, text-area:2, text-mask:2), so the hint\n now lands in the standard subscript and picks up the italic rule with everything else. -->\n<mat-form-field floatLabel=\"always\" [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [hintLabel]=\"hint\" [ngStyle]=\"{'width':width ?? '100%'}\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\"><!-- Changed: [hideRequiredMarker] was absent, so a required date never showed the asterisk that text.component.html:2 has always shown -->\n <mat-label>{{display}}</mat-label>\n <input [formControl]=\"control\" [required]=\"required\" [min]=\"minDate.value\" [max]=\"maxDate.value\" matInput [matDatepicker]=\"picker_date\" (dateInput)=\"onChangeEvent()\" [placeholder]=\"computedPlaceholder\" [readonly]=\"true\" (click)=\"onInputClick(picker_date)\"><!-- Changed: [required] was never bound, so the input rendered aria-required=\"false\" on a field the form treated as mandatory -->\n <mat-datepicker #picker_date></mat-datepicker><!-- Note (SBD-1): (opened)=\"refreshBounds()\" was tried here FIRST and MEASURED NOT TO FIRE \u2014 opening by the toggle icon called refreshBounds 0 times, opening by the input called it 1. The refresh is done on the toggle itself instead, below. -->\n <mat-error *ngIf=\"control.invalid && (control.dirty || control.touched)\">{{validate(control)}}</mat-error><!-- Changed: gated on dirty/touched for parity with text.component.html:6 \u2014 an untouched field must not scold the user on open -->\n <div matSuffix class=\"suffix-icons\">\n <mat-datepicker-toggle [for]=\"picker_date\" (mousedown)=\"refreshBounds()\" (keydown)=\"refreshBounds()\"></mat-datepicker-toggle><!-- Added (SBD-1): THE CALENDAR ICON IS HOW PEOPLE ACTUALLY OPEN THIS, and it bypassed every refresh there was \u2014 onInputClick only fires for a click on the text box, and (opened) on the datepicker was measured not to fire at all. So a bound needing a re-read at open time (a function bound, or 'today' after midnight) was correct when opened one way and stale when opened the other. mousedown/keydown are used rather than click deliberately: BOTH FIRE BEFORE the activation that opens the picker, so the window is already correct when the calendar is built, instead of depending on an open calendar re-rendering when its bounds change underneath it. -->\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix><!-- Changed: (clearClick) was missing, so the X wrote the suffix's own dead value and the clear never reached the form -->\n </div>\n</mat-form-field>\n", styles: [""], dependencies: [{ 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: i2.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i5$1.MatDatepicker, selector: "mat-datepicker", exportAs: ["matDatepicker"] }, { kind: "directive", type: i5$1.MatDatepickerInput, selector: "input[matDatepicker]", inputs: ["matDatepicker", "min", "max", "matDatepickerFilter"], exportAs: ["matDatepickerInput"] }, { kind: "component", type: i5$1.MatDatepickerToggle, selector: "mat-datepicker-toggle", inputs: ["for", "tabIndex", "aria-label", "disabled", "disableRipple"], exportAs: ["matDatepickerToggle"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "readonly", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }] }); }
|
|
17269
17376
|
}
|
|
17270
17377
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DateComponent, decorators: [{
|
|
17271
17378
|
type: Component,
|
|
17272
|
-
args: [{ selector: 'spa-date', standalone: false, template: "<!-- Changed: Added mouse events for hover tracking, click handler for opening picker, and spa-suffix component -->\n<!-- Changed (placeholder conventions): floatLabel=\"always\" so the \"Pick X\" affordance is visible in the EMPTY state, which is the only state it is for. The floated label stays the clean label ({{display}}). -->\n<!-- Changed (2026-08-19, owner \"make sure you don't miss any\"): [hintLabel] was NEVER BOUND on this component,\n so `@Input() hint` (date.component.ts:22) was a property that did nothing \u2014 a hint set on a date field\n rendered no line at all. Found while sweeping every hint render site for the italic treatment: this one\n had no render site to style. SIX date fields in the estate already set a hint and showed nothing \u2014\n piggery.service.ts:666 and :1000 (\"Leave blank if the birth date is not known\", both on the Add Pig and\n Record Purchase forms), datalib.service.ts:718, payroll.service.ts:392 and :393. grip.service.ts:647 had\n already DIAGNOSED this in a comment and worked around it by using infoMessage instead. Binding hintLabel\n is the same idiom every sibling uses (text:2, number:2, money:3, text-area:2, text-mask:2), so the hint\n now lands in the standard subscript and picks up the italic rule with everything else. -->\n<mat-form-field floatLabel=\"always\" [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [hintLabel]=\"hint\" [ngStyle]=\"{'width':width ?? '100%'}\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\"><!-- Changed: [hideRequiredMarker] was absent, so a required date never showed the asterisk that text.component.html:2 has always shown -->\n <mat-label>{{display}}</mat-label>\n <input [formControl]=\"control\" [required]=\"required\" [min]=\"minDate.value\" [max]=\"maxDate.value\" matInput [matDatepicker]=\"picker_date\" (dateInput)=\"onChangeEvent()\" [placeholder]=\"computedPlaceholder\" [readonly]=\"true\" (click)=\"onInputClick(picker_date)\"><!-- Changed: [required] was never bound, so the input rendered aria-required=\"false\" on a field the form treated as mandatory -->\n <mat-datepicker #picker_date></mat-datepicker><!-- Note (SBD-1): (opened)=\"refreshBounds()\" was tried here FIRST and MEASURED NOT TO FIRE \u2014 opening by the toggle icon called refreshBounds 0 times, opening by the input called it 1. The refresh is done on the toggle itself instead, below. -->\n <mat-error *ngIf=\"control.invalid && (control.dirty || control.touched)\">{{validate(control)}}</mat-error><!-- Changed: gated on dirty/touched for parity with text.component.html:6 \u2014 an untouched field must not scold the user on open -->\n <div matSuffix class=\"suffix-icons\">\n <mat-datepicker-toggle [for]=\"picker_date\" (mousedown)=\"refreshBounds()\" (keydown)=\"refreshBounds()\"></mat-datepicker-toggle><!-- Added (SBD-1): THE CALENDAR ICON IS HOW PEOPLE ACTUALLY OPEN THIS, and it bypassed every refresh there was \u2014 onInputClick only fires for a click on the text box, and (opened) on the datepicker was measured not to fire at all. So a bound needing a re-read at open time (a function bound, or 'today' after midnight) was correct when opened one way and stale when opened the other. mousedown/keydown are used rather than click deliberately: BOTH FIRE BEFORE the activation that opens the picker, so the window is already correct when the calendar is built, instead of depending on an open calendar re-rendering when its bounds change underneath it. -->\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix><!-- Changed: (clearClick) was missing, so the X wrote the suffix's own dead value and the clear never reached the form -->\n </div>\n</mat-form-field>\n" }]
|
|
17379
|
+
args: [{ selector: 'spa-date', standalone: false, template: "<!-- Changed: Added mouse events for hover tracking, click handler for opening picker, and spa-suffix component -->\n<!-- Changed (placeholder conventions): floatLabel=\"always\" so the \"Pick X\" affordance is visible in the EMPTY state, which is the only state it is for. The floated label stays the clean label ({{display}}). -->\n<!-- Changed (2026-08-19, owner \"make sure you don't miss any\"): [hintLabel] was NEVER BOUND on this component,\n so `@Input() hint` (date.component.ts:22) was a property that did nothing \u2014 a hint set on a date field\n rendered no line at all. Found while sweeping every hint render site for the italic treatment: this one\n had no render site to style. SIX date fields in the estate already set a hint and showed nothing \u2014\n piggery.service.ts:666 and :1000 (\"Leave blank if the birth date is not known\", both on the Add Pig and\n Record Purchase forms), datalib.service.ts:718, payroll.service.ts:392 and :393. grip.service.ts:647 had\n already DIAGNOSED this in a comment and worked around it by using infoMessage instead. Binding hintLabel\n is the same idiom every sibling uses (text:2, number:2, money:3, text-area:2, text-mask:2), so the hint\n now lands in the standard subscript and picks up the italic rule with everything else. -->\n<mat-form-field floatLabel=\"always\" [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [hintLabel]=\"hint\" [ngStyle]=\"{'width':width ?? '100%'}\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\"><!-- Changed: [hideRequiredMarker] was absent, so a required date never showed the asterisk that text.component.html:2 has always shown -->\n <mat-label>{{display}}</mat-label>\n <input [formControl]=\"control\" [required]=\"required\" [min]=\"minDate.value\" [max]=\"maxDate.value\" matInput [matDatepicker]=\"picker_date\" (dateInput)=\"onChangeEvent()\" [placeholder]=\"computedPlaceholder\" [readonly]=\"true\" (click)=\"onInputClick(picker_date)\"><!-- Changed: [required] was never bound, so the input rendered aria-required=\"false\" on a field the form treated as mandatory -->\n <mat-datepicker #picker_date></mat-datepicker><!-- Note (SBD-1): (opened)=\"refreshBounds()\" was tried here FIRST and MEASURED NOT TO FIRE \u2014 opening by the toggle icon called refreshBounds 0 times, opening by the input called it 1. The refresh is done on the toggle itself instead, below. -->\n <mat-error *ngIf=\"control.invalid && (control.dirty || control.touched)\">{{validate(control)}}</mat-error><!-- Changed: gated on dirty/touched for parity with text.component.html:6 \u2014 an untouched field must not scold the user on open -->\n <div matSuffix class=\"suffix-icons\">\n <mat-datepicker-toggle [for]=\"picker_date\" (mousedown)=\"refreshBounds()\" (keydown)=\"refreshBounds()\"></mat-datepicker-toggle><!-- Added (SBD-1): THE CALENDAR ICON IS HOW PEOPLE ACTUALLY OPEN THIS, and it bypassed every refresh there was \u2014 onInputClick only fires for a click on the text box, and (opened) on the datepicker was measured not to fire at all. So a bound needing a re-read at open time (a function bound, or 'today' after midnight) was correct when opened one way and stale when opened the other. mousedown/keydown are used rather than click deliberately: BOTH FIRE BEFORE the activation that opens the picker, so the window is already correct when the calendar is built, instead of depending on an open calendar re-rendering when its bounds change underneath it. -->\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix><!-- Changed: (clearClick) was missing, so the X wrote the suffix's own dead value and the clear never reached the form -->\n </div>\n</mat-form-field>\n" }]
|
|
17273
17380
|
}], propDecorators: { required: [{
|
|
17274
17381
|
type: Input
|
|
17275
17382
|
}], min: [{
|
|
@@ -17494,7 +17601,7 @@ class SelectCommonComponent {
|
|
|
17494
17601
|
// Added: Call onSelectChange callback if provided
|
|
17495
17602
|
if (this.field?.onSelectChange && this.data) {
|
|
17496
17603
|
const selectedOption = this.options?.find(opt => opt[this.optionValue] === this.value);
|
|
17497
|
-
this.field.onSelectChange(this.value, this.data, selectedOption);
|
|
17604
|
+
this.field.onSelectChange(this.value, this.data, selectedOption, this.field.formConfig); // Changed (addpig-label-leak): forward the LIVE FormConfig spa-form stamped on the field. A select fires this hook from HERE as well as from form.component.selectChanged, so patching only that one left every select still handing its handler `undefined` — which fell back to the app's singleton and kept the leak alive for stage/pen/quantity pickers
|
|
17498
17605
|
}
|
|
17499
17606
|
}
|
|
17500
17607
|
updateSelectedOptionHint() {
|
|
@@ -17649,7 +17756,7 @@ class SelectCommonComponent {
|
|
|
17649
17756
|
this.loadIndicator.destroy(); // Added: clear any pending delay/hold timer on a field torn down mid-read
|
|
17650
17757
|
}
|
|
17651
17758
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SelectCommonComponent, deps: [{ token: MessageService }, { token: DataServiceLib }, { token: ApiErrorService }, { token: OfflineService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
17652
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SelectCommonComponent, isStandalone: false, selector: "spa-select-common", inputs: { optionsUnavailableOffline: "optionsUnavailableOffline", width: "width", readonly: "readonly", required: "required", defaultFirstValue: "defaultFirstValue", readonlyMode: "readonlyMode", hint: "hint", placeholder: "placeholder", display: "display", value: "value", options: "options", masterOptions: "masterOptions", masterField: "masterField", optionValue: "optionValue", optionDisplay: "optionDisplay", optionDisplayExtra: "optionDisplayExtra", nullable: "nullable", infoMessage: "infoMessage", copyContent: "copyContent", loadAction: "loadAction", loadIDField: "loadIDField", field: "field", data: "data", addOptionLabel: "addOptionLabel" }, outputs: { valueChange: "valueChange", hoverChange: "hoverChange", addNew: "addNew" }, viewQueries: [{ propertyName: "matSelect", first: true, predicate: MatSelect, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!-- Changed (WS-1 Phase 3): one class, no structural change \u2014 it tints the hint line the field already renders. See the .css for why the state is spoken on the field rather than in a toast. -->\n<mat-form-field *ngIf=\"readonlyMode==''\" floatLabel=\"always\" [hintLabel]=\"computedHint\" [class.spa-options-unavailable]=\"offlineUnavailable\" [ngStyle]=\"{'width':width ?? '100%'}\" [hideRequiredMarker]=\"false\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n\n <mat-label>{{display}}</mat-label>\n <!-- Removed: [multiple]=\"multiple\" binding - component now only supports single selection -->\n <mat-select [(value)]=\"value\" (selectionChange)=\"changed()\" [disabled]=\"readonly\" [placeholder]=\"computedPlaceholder\" [required]=\"required\">\n <!-- Added (dropdown quick-add): always-first create option \u2014 shown even when the list is empty; picking it opens the create dialog -->\n <mat-option *ngIf=\"addOptionLabel\" [value]=\"ADD_NEW_OPTION\" class=\"spa-add-new-option\">\n <mat-icon class=\"tinyIcon spa-add-new-icon\">add</mat-icon>{{addOptionLabel}}\n </mat-option>\n <mat-option *ngFor=\"let row of options\" [value]=\"row[optionValue]\">\n {{row[optionDisplay]}} <label *ngIf=\"optionDisplayExtra!='' && row[optionDisplayExtra] && row[optionDisplayExtra] != ''\">({{row[optionDisplayExtra]}})</label>\n </mat-option>\n </mat-select>\n\n <div matSuffix class=\"suffix-icons\">\n <ng-content select=\"[additionalButtons]\"></ng-content>\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <!-- Added (WS-1 Phase 3): the house's own offline mark, the same cloud_off spa-offline-indicator uses, so the state is recognisable without reading \u2014 which is what carries it at phone width, where the hint line has least room. The refresh button is left exactly as it was and stays reachable on purpose: it is the one action that fixes this once the connection is back, and getData() clears the flag on the first successful load. -->\n <mat-icon *ngIf=\"offlineUnavailable\" class=\"tinyIcon spa-options-unavailable-icon\" [matTooltip]=\"computedHint\" matTooltipPosition=\"above\">cloud_off</mat-icon>\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning) && !masterField\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n\n</mat-form-field>\n", styles: [".spa-add-new-option{border-bottom:1px solid rgba(0,0,0,.12);font-weight:500;color:green}.spa-add-new-icon{color:green;vertical-align:middle;margin-right:4px}.spa-options-unavailable ::ng-deep .mat-mdc-form-field-hint{color:#ef6c00}.spa-options-unavailable-icon{color:#ef6c00;vertical-align:middle}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i7.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: i7$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }] }); }
|
|
17759
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SelectCommonComponent, isStandalone: false, selector: "spa-select-common", inputs: { optionsUnavailableOffline: "optionsUnavailableOffline", width: "width", readonly: "readonly", required: "required", defaultFirstValue: "defaultFirstValue", readonlyMode: "readonlyMode", hint: "hint", placeholder: "placeholder", display: "display", value: "value", options: "options", masterOptions: "masterOptions", masterField: "masterField", optionValue: "optionValue", optionDisplay: "optionDisplay", optionDisplayExtra: "optionDisplayExtra", nullable: "nullable", infoMessage: "infoMessage", copyContent: "copyContent", loadAction: "loadAction", loadIDField: "loadIDField", field: "field", data: "data", addOptionLabel: "addOptionLabel" }, outputs: { valueChange: "valueChange", hoverChange: "hoverChange", addNew: "addNew" }, viewQueries: [{ propertyName: "matSelect", first: true, predicate: MatSelect, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!-- Changed (WS-1 Phase 3): one class, no structural change \u2014 it tints the hint line the field already renders. See the .css for why the state is spoken on the field rather than in a toast. -->\n<mat-form-field *ngIf=\"readonlyMode==''\" floatLabel=\"always\" [hintLabel]=\"computedHint\" [class.spa-options-unavailable]=\"offlineUnavailable\" [ngStyle]=\"{'width':width ?? '100%'}\" [hideRequiredMarker]=\"false\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n\n <mat-label>{{display}}</mat-label>\n <!-- Removed: [multiple]=\"multiple\" binding - component now only supports single selection -->\n <mat-select [(value)]=\"value\" (selectionChange)=\"changed()\" [disabled]=\"readonly\" [placeholder]=\"computedPlaceholder\" [required]=\"required\">\n <!-- Added (dropdown quick-add): always-first create option \u2014 shown even when the list is empty; picking it opens the create dialog -->\n <mat-option *ngIf=\"addOptionLabel\" [value]=\"ADD_NEW_OPTION\" class=\"spa-add-new-option\">\n <mat-icon class=\"tinyIcon spa-add-new-icon\">add</mat-icon>{{addOptionLabel}}\n </mat-option>\n <mat-option *ngFor=\"let row of options\" [value]=\"row[optionValue]\">\n {{row[optionDisplay]}} <label *ngIf=\"optionDisplayExtra!='' && row[optionDisplayExtra] && row[optionDisplayExtra] != ''\">({{row[optionDisplayExtra]}})</label>\n </mat-option>\n </mat-select>\n\n <div matSuffix class=\"suffix-icons\">\n <ng-content select=\"[additionalButtons]\"></ng-content>\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <!-- Added (WS-1 Phase 3): the house's own offline mark, the same cloud_off spa-offline-indicator uses, so the state is recognisable without reading \u2014 which is what carries it at phone width, where the hint line has least room. The refresh button is left exactly as it was and stays reachable on purpose: it is the one action that fixes this once the connection is back, and getData() clears the flag on the first successful load. -->\n <mat-icon *ngIf=\"offlineUnavailable\" class=\"tinyIcon spa-options-unavailable-icon\" [matTooltip]=\"computedHint\" matTooltipPosition=\"above\">cloud_off</mat-icon>\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning) && !masterField\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n\n</mat-form-field>\n", styles: [".spa-add-new-option{border-bottom:1px solid rgba(0,0,0,.12);font-weight:500;color:green}.spa-add-new-icon{color:green;vertical-align:middle;margin-right:4px}.spa-options-unavailable ::ng-deep .mat-mdc-form-field-hint{color:#ef6c00}.spa-options-unavailable-icon{color:#ef6c00;vertical-align:middle}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i7.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: i7$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "readonly", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }] }); }
|
|
17653
17760
|
}
|
|
17654
17761
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SelectCommonComponent, decorators: [{
|
|
17655
17762
|
type: Component,
|
|
@@ -17961,11 +18068,11 @@ class TextMultiComponent {
|
|
|
17961
18068
|
this.loadIndicator.destroy(); // Added: clear any pending delay/hold timer on a field torn down mid-read
|
|
17962
18069
|
}
|
|
17963
18070
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TextMultiComponent, deps: [{ token: MessageService }, { token: DataServiceLib }, { token: ApiErrorService }, { token: OfflineService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
17964
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TextMultiComponent, isStandalone: false, selector: "spa-text-multi", inputs: { display: "display", placeholder: "placeholder", value: "value", readonly: "readonly", required: "required", hint: "hint", strict: "strict", suffix: "suffix", infoMessage: "infoMessage", copyContent: "copyContent", clearContent: "clearContent", options: "options", optionDisplay: "optionDisplay", optionValue: "optionValue", loadAction: "loadAction" }, outputs: { valueChange: "valueChange", hoverChange: "hoverChange" }, viewQueries: [{ propertyName: "textInput", first: true, predicate: ["textInput"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!-- Changed (WS-1 Phase 3): one class added \u2014 no structural change. It tints the hint line the field already renders. Same treatment as spa-select-common and spa-text-single; see the .css for why the state is spoken on the field rather than in a toast. -->\n<mat-form-field [class.spa-readonly]=\"readonly\" [class.spa-options-unavailable]=\"optionsUnavailableOffline\" style=\"width: 100%;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <mat-chip-grid #chipList>\n <mat-chip-row *ngFor=\"let value of values\" [removable]=\"!readonly\" (removed)=\"remove(value)\">\n {{getDisplayValue(value)}}\n <button matChipRemove *ngIf=\"!readonly\"><mat-icon class=\"tinyIcon\">cancel</mat-icon></button>\n </mat-chip-row>\n <input #textInput autocomplete=\"off\" [placeholder]=\"readonly ? '' : computedPlaceholder\" [matChipInputFor]=\"chipList\" [matChipInputSeparatorKeyCodes]=\"[13, 186]\" [matChipInputAddOnBlur]=\"true\" (matChipInputTokenEnd)=\"add($event)\" [formControl]=\"control\" [readonly]=\"readonly\" [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\">\n </mat-chip-grid>\n\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"optionSelected($event)\">\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"option\">\n {{option[optionDisplay]}}\n </mat-option>\n </mat-autocomplete>\n\n <mat-error *ngIf=\"errorState\">{{hint}}</mat-error>\n <!-- Changed (WS-1 Phase 3): the hint now reads computedHint, and \u2014 the part that matters \u2014 it is GATED on computedHint too, not on hint.\n This differs from spa-text-single only because the surface differs: text-single uses [hintLabel], which renders whenever it is non-empty,\n whereas this template renders an explicit <mat-hint> behind an *ngIf. Left gated on `hint`, the offline notice would never appear in the\n filter/option context at all, because option.component.html binds [loadAction] but never binds [hint] \u2014 the exact context where a lookup\n can fail with nothing else on screen to explain it. Both render the same .mat-mdc-form-field-hint element, so the css rule is unchanged.\n The mat-error above is untouched and still wins when errorState is set, as before. -->\n <mat-hint *ngIf=\"computedHint && !errorState\">{{computedHint}}</mat-hint>\n\n <div matSuffix class=\"suffix-icons\">\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <!-- Added (WS-1 Phase 3): the house's own offline mark, the same cloud_off spa-offline-indicator, spa-select-common and spa-text-single already use, so the state is recognisable without reading \u2014 which is what carries it at phone width, where the hint line has least room. The refresh button below is left exactly as it was and stays reachable on purpose: it is the one action that fixes this once the connection is back, and getData() clears the flag on the first successful load. -->\n <mat-icon *ngIf=\"optionsUnavailableOffline\" class=\"tinyIcon spa-options-unavailable-icon\" [matTooltip]=\"computedHint\" matTooltipPosition=\"above\">cloud_off</mat-icon>\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"true\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [".spa-options-unavailable ::ng-deep .mat-mdc-form-field-hint{color:#ef6c00}.spa-options-unavailable-icon{color:#ef6c00;vertical-align:middle}\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: i2.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i6.MatChipGrid, selector: "mat-chip-grid", inputs: ["disabled", "placeholder", "required", "value", "errorStateMatcher"], outputs: ["change", "valueChange"] }, { kind: "directive", type: i6.MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputFor", "matChipInputAddOnBlur", "matChipInputSeparatorKeyCodes", "placeholder", "id", "disabled", "readonly", "matChipInputDisabledInteractive"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { kind: "directive", type: i6.MatChipRemove, selector: "[matChipRemove]" }, { kind: "component", type: i6.MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i14.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "directive", type: i14.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "directive", type: i7$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }] }); }
|
|
18071
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TextMultiComponent, isStandalone: false, selector: "spa-text-multi", inputs: { display: "display", placeholder: "placeholder", value: "value", readonly: "readonly", required: "required", hint: "hint", strict: "strict", suffix: "suffix", infoMessage: "infoMessage", copyContent: "copyContent", clearContent: "clearContent", options: "options", optionDisplay: "optionDisplay", optionValue: "optionValue", loadAction: "loadAction" }, outputs: { valueChange: "valueChange", hoverChange: "hoverChange" }, viewQueries: [{ propertyName: "textInput", first: true, predicate: ["textInput"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!-- Changed (WS-1 Phase 3): one class added \u2014 no structural change. It tints the hint line the field already renders. Same treatment as spa-select-common and spa-text-single; see the .css for why the state is spoken on the field rather than in a toast. -->\n<mat-form-field [class.spa-readonly]=\"readonly\" [class.spa-options-unavailable]=\"optionsUnavailableOffline\" style=\"width: 100%;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <mat-chip-grid #chipList>\n <mat-chip-row *ngFor=\"let value of values\" [removable]=\"!readonly\" (removed)=\"remove(value)\">\n {{getDisplayValue(value)}}\n <button matChipRemove *ngIf=\"!readonly\"><mat-icon class=\"tinyIcon\">cancel</mat-icon></button>\n </mat-chip-row>\n <input #textInput autocomplete=\"off\" [placeholder]=\"readonly ? '' : computedPlaceholder\" [matChipInputFor]=\"chipList\" [matChipInputSeparatorKeyCodes]=\"[13, 186]\" [matChipInputAddOnBlur]=\"true\" (matChipInputTokenEnd)=\"add($event)\" [formControl]=\"control\" [readonly]=\"readonly\" [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\">\n </mat-chip-grid>\n\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"optionSelected($event)\">\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"option\">\n {{option[optionDisplay]}}\n </mat-option>\n </mat-autocomplete>\n\n <mat-error *ngIf=\"errorState\">{{hint}}</mat-error>\n <!-- Changed (WS-1 Phase 3): the hint now reads computedHint, and \u2014 the part that matters \u2014 it is GATED on computedHint too, not on hint.\n This differs from spa-text-single only because the surface differs: text-single uses [hintLabel], which renders whenever it is non-empty,\n whereas this template renders an explicit <mat-hint> behind an *ngIf. Left gated on `hint`, the offline notice would never appear in the\n filter/option context at all, because option.component.html binds [loadAction] but never binds [hint] \u2014 the exact context where a lookup\n can fail with nothing else on screen to explain it. Both render the same .mat-mdc-form-field-hint element, so the css rule is unchanged.\n The mat-error above is untouched and still wins when errorState is set, as before. -->\n <mat-hint *ngIf=\"computedHint && !errorState\">{{computedHint}}</mat-hint>\n\n <div matSuffix class=\"suffix-icons\">\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <!-- Added (WS-1 Phase 3): the house's own offline mark, the same cloud_off spa-offline-indicator, spa-select-common and spa-text-single already use, so the state is recognisable without reading \u2014 which is what carries it at phone width, where the hint line has least room. The refresh button below is left exactly as it was and stays reachable on purpose: it is the one action that fixes this once the connection is back, and getData() clears the flag on the first successful load. -->\n <mat-icon *ngIf=\"optionsUnavailableOffline\" class=\"tinyIcon spa-options-unavailable-icon\" [matTooltip]=\"computedHint\" matTooltipPosition=\"above\">cloud_off</mat-icon>\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clear()\" [isHovered]=\"true\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [".spa-options-unavailable ::ng-deep .mat-mdc-form-field-hint{color:#ef6c00}.spa-options-unavailable-icon{color:#ef6c00;vertical-align:middle}\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: i2.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i6.MatChipGrid, selector: "mat-chip-grid", inputs: ["disabled", "placeholder", "required", "value", "errorStateMatcher"], outputs: ["change", "valueChange"] }, { kind: "directive", type: i6.MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputFor", "matChipInputAddOnBlur", "matChipInputSeparatorKeyCodes", "placeholder", "id", "disabled", "readonly", "matChipInputDisabledInteractive"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { kind: "directive", type: i6.MatChipRemove, selector: "[matChipRemove]" }, { kind: "component", type: i6.MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i14.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "directive", type: i14.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "directive", type: i7$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "readonly", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }] }); }
|
|
17965
18072
|
}
|
|
17966
18073
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TextMultiComponent, decorators: [{
|
|
17967
18074
|
type: Component,
|
|
17968
|
-
args: [{ selector: 'spa-text-multi', standalone: false, template: "<!-- Changed (WS-1 Phase 3): one class added \u2014 no structural change. It tints the hint line the field already renders. Same treatment as spa-select-common and spa-text-single; see the .css for why the state is spoken on the field rather than in a toast. -->\n<mat-form-field [class.spa-readonly]=\"readonly\" [class.spa-options-unavailable]=\"optionsUnavailableOffline\" style=\"width: 100%;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <mat-chip-grid #chipList>\n <mat-chip-row *ngFor=\"let value of values\" [removable]=\"!readonly\" (removed)=\"remove(value)\">\n {{getDisplayValue(value)}}\n <button matChipRemove *ngIf=\"!readonly\"><mat-icon class=\"tinyIcon\">cancel</mat-icon></button>\n </mat-chip-row>\n <input #textInput autocomplete=\"off\" [placeholder]=\"readonly ? '' : computedPlaceholder\" [matChipInputFor]=\"chipList\" [matChipInputSeparatorKeyCodes]=\"[13, 186]\" [matChipInputAddOnBlur]=\"true\" (matChipInputTokenEnd)=\"add($event)\" [formControl]=\"control\" [readonly]=\"readonly\" [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\">\n </mat-chip-grid>\n\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"optionSelected($event)\">\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"option\">\n {{option[optionDisplay]}}\n </mat-option>\n </mat-autocomplete>\n\n <mat-error *ngIf=\"errorState\">{{hint}}</mat-error>\n <!-- Changed (WS-1 Phase 3): the hint now reads computedHint, and \u2014 the part that matters \u2014 it is GATED on computedHint too, not on hint.\n This differs from spa-text-single only because the surface differs: text-single uses [hintLabel], which renders whenever it is non-empty,\n whereas this template renders an explicit <mat-hint> behind an *ngIf. Left gated on `hint`, the offline notice would never appear in the\n filter/option context at all, because option.component.html binds [loadAction] but never binds [hint] \u2014 the exact context where a lookup\n can fail with nothing else on screen to explain it. Both render the same .mat-mdc-form-field-hint element, so the css rule is unchanged.\n The mat-error above is untouched and still wins when errorState is set, as before. -->\n <mat-hint *ngIf=\"computedHint && !errorState\">{{computedHint}}</mat-hint>\n\n <div matSuffix class=\"suffix-icons\">\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <!-- Added (WS-1 Phase 3): the house's own offline mark, the same cloud_off spa-offline-indicator, spa-select-common and spa-text-single already use, so the state is recognisable without reading \u2014 which is what carries it at phone width, where the hint line has least room. The refresh button below is left exactly as it was and stays reachable on purpose: it is the one action that fixes this once the connection is back, and getData() clears the flag on the first successful load. -->\n <mat-icon *ngIf=\"optionsUnavailableOffline\" class=\"tinyIcon spa-options-unavailable-icon\" [matTooltip]=\"computedHint\" matTooltipPosition=\"above\">cloud_off</mat-icon>\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"true\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [".spa-options-unavailable ::ng-deep .mat-mdc-form-field-hint{color:#ef6c00}.spa-options-unavailable-icon{color:#ef6c00;vertical-align:middle}\n"] }]
|
|
18075
|
+
args: [{ selector: 'spa-text-multi', standalone: false, template: "<!-- Changed (WS-1 Phase 3): one class added \u2014 no structural change. It tints the hint line the field already renders. Same treatment as spa-select-common and spa-text-single; see the .css for why the state is spoken on the field rather than in a toast. -->\n<mat-form-field [class.spa-readonly]=\"readonly\" [class.spa-options-unavailable]=\"optionsUnavailableOffline\" style=\"width: 100%;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <mat-chip-grid #chipList>\n <mat-chip-row *ngFor=\"let value of values\" [removable]=\"!readonly\" (removed)=\"remove(value)\">\n {{getDisplayValue(value)}}\n <button matChipRemove *ngIf=\"!readonly\"><mat-icon class=\"tinyIcon\">cancel</mat-icon></button>\n </mat-chip-row>\n <input #textInput autocomplete=\"off\" [placeholder]=\"readonly ? '' : computedPlaceholder\" [matChipInputFor]=\"chipList\" [matChipInputSeparatorKeyCodes]=\"[13, 186]\" [matChipInputAddOnBlur]=\"true\" (matChipInputTokenEnd)=\"add($event)\" [formControl]=\"control\" [readonly]=\"readonly\" [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\">\n </mat-chip-grid>\n\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"optionSelected($event)\">\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"option\">\n {{option[optionDisplay]}}\n </mat-option>\n </mat-autocomplete>\n\n <mat-error *ngIf=\"errorState\">{{hint}}</mat-error>\n <!-- Changed (WS-1 Phase 3): the hint now reads computedHint, and \u2014 the part that matters \u2014 it is GATED on computedHint too, not on hint.\n This differs from spa-text-single only because the surface differs: text-single uses [hintLabel], which renders whenever it is non-empty,\n whereas this template renders an explicit <mat-hint> behind an *ngIf. Left gated on `hint`, the offline notice would never appear in the\n filter/option context at all, because option.component.html binds [loadAction] but never binds [hint] \u2014 the exact context where a lookup\n can fail with nothing else on screen to explain it. Both render the same .mat-mdc-form-field-hint element, so the css rule is unchanged.\n The mat-error above is untouched and still wins when errorState is set, as before. -->\n <mat-hint *ngIf=\"computedHint && !errorState\">{{computedHint}}</mat-hint>\n\n <div matSuffix class=\"suffix-icons\">\n <!-- Changed: the icon now also shows while loading (not hover-only), and spins \u2014 same .tin-spin idiom as the table/filter refresh button -->\n <!-- Added (WS-1 Phase 3): the house's own offline mark, the same cloud_off spa-offline-indicator, spa-select-common and spa-text-single already use, so the state is recognisable without reading \u2014 which is what carries it at phone width, where the hint line has least room. The refresh button below is left exactly as it was and stays reachable on purpose: it is the one action that fixes this once the connection is back, and getData() clears the flag on the first successful load. -->\n <mat-icon *ngIf=\"optionsUnavailableOffline\" class=\"tinyIcon spa-options-unavailable-icon\" [matTooltip]=\"computedHint\" matTooltipPosition=\"above\">cloud_off</mat-icon>\n <button mat-icon-button *ngIf=\"loadAction && (isHovered || loadIndicator.spinning)\" (click)=\"refresh($event)\" [matTooltip]=\"loadIndicator.spinning ? 'Loading' : 'Refresh'\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" [class.tin-spin]=\"loadIndicator.spinning\" color=\"primary\">cached</mat-icon>\n </button>\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clear()\" [isHovered]=\"true\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [".spa-options-unavailable ::ng-deep .mat-mdc-form-field-hint{color:#ef6c00}.spa-options-unavailable-icon{color:#ef6c00;vertical-align:middle}\n"] }]
|
|
17969
18076
|
}], ctorParameters: () => [{ type: MessageService }, { type: DataServiceLib }, { type: ApiErrorService }, { type: OfflineService }], propDecorators: { textInput: [{
|
|
17970
18077
|
type: ViewChild,
|
|
17971
18078
|
args: ['textInput']
|
|
@@ -19939,11 +20046,11 @@ class TextAreaComponent {
|
|
|
19939
20046
|
return "";
|
|
19940
20047
|
}
|
|
19941
20048
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TextAreaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
19942
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TextAreaComponent, isStandalone: false, selector: "spa-text-area", inputs: { appearance: "appearance", readonly: "readonly", hint: "hint", display: "display", placeholder: "placeholder", value: "value", rows: "rows", width: "width", copyContent: "copyContent", clearContent: "clearContent", required: "required", min: "min", max: "max", regex: "regex", suffix: "suffix", infoMessage: "infoMessage" }, outputs: { valueChange: "valueChange", leave: "leave", enterPress: "enterPress" }, usesOnChanges: true, ngImport: i0, template: "<!-- Multi-line textarea input -->\n<mat-form-field [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [hintLabel]=\"hint\" [ngStyle]=\"{'width':width ?? '100%'}\" style=\"margin-right: 5px;\" [appearance]=\"appearance ?? 'fill'\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <textarea matInput autocomplete=\"off\" [rows]=\"rows\" (keyup.enter)=\"enterPressed()\" [placeholder]=\"computedPlaceholder\" [formControl]=\"control\" [required]=\"required\" [readonly]=\"readonly\"></textarea>\n <mat-error *ngIf=\"control.invalid && (control.dirty || control.touched)\">{{validate(control)}}</mat-error>\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [""], dependencies: [{ 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: i2.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }] }); }
|
|
20049
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TextAreaComponent, isStandalone: false, selector: "spa-text-area", inputs: { appearance: "appearance", readonly: "readonly", hint: "hint", display: "display", placeholder: "placeholder", value: "value", rows: "rows", width: "width", copyContent: "copyContent", clearContent: "clearContent", required: "required", min: "min", max: "max", regex: "regex", suffix: "suffix", infoMessage: "infoMessage" }, outputs: { valueChange: "valueChange", leave: "leave", enterPress: "enterPress" }, usesOnChanges: true, ngImport: i0, template: "<!-- Multi-line textarea input -->\n<mat-form-field [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [hintLabel]=\"hint\" [ngStyle]=\"{'width':width ?? '100%'}\" style=\"margin-right: 5px;\" [appearance]=\"appearance ?? 'fill'\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <textarea matInput autocomplete=\"off\" [rows]=\"rows\" (keyup.enter)=\"enterPressed()\" [placeholder]=\"computedPlaceholder\" [formControl]=\"control\" [required]=\"required\" [readonly]=\"readonly\"></textarea>\n <mat-error *ngIf=\"control.invalid && (control.dirty || control.touched)\">{{validate(control)}}</mat-error>\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [""], dependencies: [{ 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: i2.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "readonly", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }] }); }
|
|
19943
20050
|
}
|
|
19944
20051
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TextAreaComponent, decorators: [{
|
|
19945
20052
|
type: Component,
|
|
19946
|
-
args: [{ selector: 'spa-text-area', standalone: false, template: "<!-- Multi-line textarea input -->\n<mat-form-field [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [hintLabel]=\"hint\" [ngStyle]=\"{'width':width ?? '100%'}\" style=\"margin-right: 5px;\" [appearance]=\"appearance ?? 'fill'\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <textarea matInput autocomplete=\"off\" [rows]=\"rows\" (keyup.enter)=\"enterPressed()\" [placeholder]=\"computedPlaceholder\" [formControl]=\"control\" [required]=\"required\" [readonly]=\"readonly\"></textarea>\n <mat-error *ngIf=\"control.invalid && (control.dirty || control.touched)\">{{validate(control)}}</mat-error>\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n" }]
|
|
20053
|
+
args: [{ selector: 'spa-text-area', standalone: false, template: "<!-- Multi-line textarea input -->\n<mat-form-field [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [hintLabel]=\"hint\" [ngStyle]=\"{'width':width ?? '100%'}\" style=\"margin-right: 5px;\" [appearance]=\"appearance ?? 'fill'\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\n <mat-label>{{display}}</mat-label>\n <textarea matInput autocomplete=\"off\" [rows]=\"rows\" (keyup.enter)=\"enterPressed()\" [placeholder]=\"computedPlaceholder\" [formControl]=\"control\" [required]=\"required\" [readonly]=\"readonly\"></textarea>\n <mat-error *ngIf=\"control.invalid && (control.dirty || control.touched)\">{{validate(control)}}</mat-error>\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n" }]
|
|
19947
20054
|
}], ctorParameters: () => [], propDecorators: { appearance: [{
|
|
19948
20055
|
type: Input
|
|
19949
20056
|
}], readonly: [{
|
|
@@ -20017,12 +20124,17 @@ class DatetimeComponent {
|
|
|
20017
20124
|
// which is what keeps a field declaring no bounds byte-for-byte unchanged. Once a bound has been enforced the
|
|
20018
20125
|
// component must keep managing the set, or releasing that bound would leave it in force for ever.
|
|
20019
20126
|
this.boundsApplied = false;
|
|
20127
|
+
// Added (DTM-1): what the READ-ONLY text box displays. A plain field, deliberately not a getter — this
|
|
20128
|
+
// formats a date, and a template-bound getter doing that allocates on every change-detection pass, which is
|
|
20129
|
+
// banned here. Same reasoning, and the same shape, as minAttr/maxAttr above.
|
|
20130
|
+
this.readonlyText = "";
|
|
20020
20131
|
this.copyContent = false;
|
|
20021
20132
|
this.clearContent = false;
|
|
20022
20133
|
this.isHovered = false;
|
|
20023
20134
|
}
|
|
20024
20135
|
ngOnInit() {
|
|
20025
20136
|
this.refreshBounds();
|
|
20137
|
+
this.refreshReadonlyText(); // Added (DTM-1): a field that opens already read-only must render its value on the first pass, not wait for a change
|
|
20026
20138
|
}
|
|
20027
20139
|
// Added (G20-1): the control does not exist until the view is built, so this is the first point at which a
|
|
20028
20140
|
// declared bound can reach it. refreshBounds() has already run in ngOnInit and returned early for that reason.
|
|
@@ -20035,6 +20147,8 @@ class DatetimeComponent {
|
|
|
20035
20147
|
this.refreshBounds(); // Changed (SBD-1): boundContext joins them — a function bound keeps one reference, so the row it reads is the thing that changes
|
|
20036
20148
|
if (changes['required'])
|
|
20037
20149
|
this.applyValidators(); // Added (G20-1): requiredCondition flips this at runtime, exactly as date.component.ts:78 notes
|
|
20150
|
+
if (changes['value'] || changes['readonly'])
|
|
20151
|
+
this.refreshReadonlyText(); // Added (DTM-1): the read-only branch renders a plain field, so it has to be recomputed wherever the value or the lock can move
|
|
20038
20152
|
}
|
|
20039
20153
|
get computedPlaceholder() { return derivePlaceholder('pick', this.display, this.placeholder, this.readonly); }
|
|
20040
20154
|
// Re-resolves both bounds. Also called on focus, so a form left open across midnight cannot go on offering
|
|
@@ -20135,8 +20249,31 @@ class DatetimeComponent {
|
|
|
20135
20249
|
// Emits null, never '' — "" is not a DateTime and fails model binding on arrival.
|
|
20136
20250
|
clear() {
|
|
20137
20251
|
this.value = null;
|
|
20252
|
+
this.refreshReadonlyText(); // Added (DTM-1): keep the read-only rendering in step with an internally-driven change
|
|
20138
20253
|
this.valueChange.emit(this.value);
|
|
20139
20254
|
}
|
|
20255
|
+
refreshReadonlyText() {
|
|
20256
|
+
this.readonlyText = this.toReadable(this.value);
|
|
20257
|
+
}
|
|
20258
|
+
// Added (DTM-1): 'YYYY-MM-DDTHH:mm' → 'dd/MM/yyyy HH:mm', the SAME form spa-label and spa-table already
|
|
20259
|
+
// render a datetime in (label.component.html:34, table-row.component.html:59), so a field reads identically
|
|
20260
|
+
// whether it is locked in a form or listed in a table. Read straight off the string when it is already a
|
|
20261
|
+
// LOCAL datetime-local value — parsing and reformatting that would risk a timezone shift for no gain. A
|
|
20262
|
+
// string carrying an explicit zone ('Z' or ±hh:mm) is the one case that must go through Date, because its
|
|
20263
|
+
// literal prefix is UTC and would display an hour that is not the user's.
|
|
20264
|
+
toReadable(v) {
|
|
20265
|
+
if (!v)
|
|
20266
|
+
return "";
|
|
20267
|
+
const raw = String(v);
|
|
20268
|
+
const zoned = /(Z|[+-]\d{2}:?\d{2})$/.test(raw);
|
|
20269
|
+
const local = zoned ? this.toLocalOrEmpty(raw) : raw;
|
|
20270
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/.exec(local);
|
|
20271
|
+
return m ? `${m[3]}/${m[2]}/${m[1]} ${m[4]}:${m[5]}` : raw; // an unrecognised string is shown verbatim rather than blanked — hiding a value we cannot parse is worse than showing it
|
|
20272
|
+
}
|
|
20273
|
+
toLocalOrEmpty(raw) {
|
|
20274
|
+
const d = new Date(raw);
|
|
20275
|
+
return isNaN(d.getTime()) ? "" : DateBounds.toLocalInput(d);
|
|
20276
|
+
}
|
|
20140
20277
|
onMouseEnter() {
|
|
20141
20278
|
this.isHovered = true;
|
|
20142
20279
|
}
|
|
@@ -20144,11 +20281,11 @@ class DatetimeComponent {
|
|
|
20144
20281
|
this.isHovered = false;
|
|
20145
20282
|
}
|
|
20146
20283
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DatetimeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
20147
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: DatetimeComponent, isStandalone: false, selector: "spa-datetime", inputs: { display: "display", placeholder: "placeholder", value: "value", readonly: "readonly", width: "width", required: "required", min: "min", max: "max", boundContext: "boundContext", suffix: "suffix", infoMessage: "infoMessage", copyContent: "copyContent", clearContent: "clearContent" }, outputs: { valueChange: "valueChange" }, viewQueries: [{ propertyName: "dtControl", first: true, predicate: ["dtControl"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!-- Changed: Added mouse events for hover tracking and spa-suffix component -->\n<mat-form-field [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [ngStyle]=\"{'width':width ?? '100%'}\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\"><!-- Changed: [hideRequiredMarker] added so a required datetime shows the asterisk every text field shows -->\n<mat-label>{{display}}</mat-label>\n<input matInput autocomplete=\"off\" type=\"datetime-local\" #dtControl=\"ngModel\" [(ngModel)]=\"value\" [required]=\"required\" [min]=\"minAttr\" [max]=\"maxAttr\" (focus)=\"refreshBounds()\" (change)=\"changed()\" [placeholder]=\"computedPlaceholder\"
|
|
20284
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: DatetimeComponent, isStandalone: false, selector: "spa-datetime", inputs: { display: "display", placeholder: "placeholder", value: "value", readonly: "readonly", width: "width", required: "required", min: "min", max: "max", boundContext: "boundContext", suffix: "suffix", infoMessage: "infoMessage", copyContent: "copyContent", clearContent: "clearContent" }, outputs: { valueChange: "valueChange" }, viewQueries: [{ propertyName: "dtControl", first: true, predicate: ["dtControl"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!-- Changed: Added mouse events for hover tracking and spa-suffix component -->\n<mat-form-field [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [ngStyle]=\"{'width':width ?? '100%'}\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\"><!-- Changed: [hideRequiredMarker] added so a required datetime shows the asterisk every text field shows -->\n<mat-label>{{display}}</mat-label>\n<!-- Changed (DTM-1): the editable control and its error are now wrapped together, because a template reference variable is scoped to the embedded view that declares it \u2014 #dtControl inside an *ngIf is invisible to a sibling mat-error outside it. Nothing about this branch itself changed. -->\n<ng-container *ngIf=\"!readonly\">\n <input matInput autocomplete=\"off\" type=\"datetime-local\" #dtControl=\"ngModel\" [(ngModel)]=\"value\" [required]=\"required\" [min]=\"minAttr\" [max]=\"maxAttr\" (focus)=\"refreshBounds()\" (change)=\"changed()\" [placeholder]=\"computedPlaceholder\" /><!-- Changed: [required] bound \u2014 Angular's RequiredValidator then attaches to this ngModel, which is what supplies both aria-required and the invalid state the mat-error below reads. Changed (DTM-1): [readonly] dropped \u2014 this branch only ever renders when the field IS editable, so the binding was always false here. -->\n <mat-error *ngIf=\"dtControl.invalid && (dtControl.dirty || dtControl.touched)\">{{validate(dtControl)}}</mat-error><!-- Added: there was no mat-error element on this component at all. Changed (G20-1): was the literal word \"Required\", which was the only error that could occur while min/max were inert; now that a declared bound validates, the message has to say WHICH rule was broken. Same dynamic subscript, same single line, no layout change. -->\n</ng-container>\n<!-- Added (DTM-1): the READ-ONLY branch is a plain text box, and that is the whole fix. A native <input type=\"datetime-local\"> ALWAYS paints its own \"dd/mm/yyyy --:--\" mask \u2014 the browser owns that glyph, it is not the placeholder attribute \u2014 so a locked, EMPTY datetime showed a mask where rop-1 (c7c36ba) had just made every other field type render blank, and it read like a value that failed to load. It also kept a live calendar picker on a field nobody may edit. A text input has no mask and no picker, so empty reads blank and a set value reads dd/MM/yyyy HH:mm \u2014 the same form spa-label and spa-table already render a datetime in. No ngModel here on purpose: this branch must never write. -->\n<input *ngIf=\"readonly\" matInput autocomplete=\"off\" type=\"text\" readonly [value]=\"readonlyText\" [placeholder]=\"computedPlaceholder\" />\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix><!-- Changed: (clearClick) was missing, so the X emptied the box on screen but the old datetime still saved -->\n </div>\n</mat-form-field>\n", styles: [""], dependencies: [{ 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: i2.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "readonly", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }] }); }
|
|
20148
20285
|
}
|
|
20149
20286
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DatetimeComponent, decorators: [{
|
|
20150
20287
|
type: Component,
|
|
20151
|
-
args: [{ selector: 'spa-datetime', standalone: false, template: "<!-- Changed: Added mouse events for hover tracking and spa-suffix component -->\n<mat-form-field [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [ngStyle]=\"{'width':width ?? '100%'}\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\"><!-- Changed: [hideRequiredMarker] added so a required datetime shows the asterisk every text field shows -->\n<mat-label>{{display}}</mat-label>\n<input matInput autocomplete=\"off\" type=\"datetime-local\" #dtControl=\"ngModel\" [(ngModel)]=\"value\" [required]=\"required\" [min]=\"minAttr\" [max]=\"maxAttr\" (focus)=\"refreshBounds()\" (change)=\"changed()\" [placeholder]=\"computedPlaceholder\"
|
|
20288
|
+
args: [{ selector: 'spa-datetime', standalone: false, template: "<!-- Changed: Added mouse events for hover tracking and spa-suffix component -->\n<mat-form-field [class.spa-readonly]=\"readonly\" [hideRequiredMarker]=\"!required\" [ngStyle]=\"{'width':width ?? '100%'}\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\"><!-- Changed: [hideRequiredMarker] added so a required datetime shows the asterisk every text field shows -->\n<mat-label>{{display}}</mat-label>\n<!-- Changed (DTM-1): the editable control and its error are now wrapped together, because a template reference variable is scoped to the embedded view that declares it \u2014 #dtControl inside an *ngIf is invisible to a sibling mat-error outside it. Nothing about this branch itself changed. -->\n<ng-container *ngIf=\"!readonly\">\n <input matInput autocomplete=\"off\" type=\"datetime-local\" #dtControl=\"ngModel\" [(ngModel)]=\"value\" [required]=\"required\" [min]=\"minAttr\" [max]=\"maxAttr\" (focus)=\"refreshBounds()\" (change)=\"changed()\" [placeholder]=\"computedPlaceholder\" /><!-- Changed: [required] bound \u2014 Angular's RequiredValidator then attaches to this ngModel, which is what supplies both aria-required and the invalid state the mat-error below reads. Changed (DTM-1): [readonly] dropped \u2014 this branch only ever renders when the field IS editable, so the binding was always false here. -->\n <mat-error *ngIf=\"dtControl.invalid && (dtControl.dirty || dtControl.touched)\">{{validate(dtControl)}}</mat-error><!-- Added: there was no mat-error element on this component at all. Changed (G20-1): was the literal word \"Required\", which was the only error that could occur while min/max were inert; now that a declared bound validates, the message has to say WHICH rule was broken. Same dynamic subscript, same single line, no layout change. -->\n</ng-container>\n<!-- Added (DTM-1): the READ-ONLY branch is a plain text box, and that is the whole fix. A native <input type=\"datetime-local\"> ALWAYS paints its own \"dd/mm/yyyy --:--\" mask \u2014 the browser owns that glyph, it is not the placeholder attribute \u2014 so a locked, EMPTY datetime showed a mask where rop-1 (c7c36ba) had just made every other field type render blank, and it read like a value that failed to load. It also kept a live calendar picker on a field nobody may edit. A text input has no mask and no picker, so empty reads blank and a set value reads dd/MM/yyyy HH:mm \u2014 the same form spa-label and spa-table already render a datetime in. No ngModel here on purpose: this branch must never write. -->\n<input *ngIf=\"readonly\" matInput autocomplete=\"off\" type=\"text\" readonly [value]=\"readonlyText\" [placeholder]=\"computedPlaceholder\" />\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix><!-- Changed: (clearClick) was missing, so the X emptied the box on screen but the old datetime still saved -->\n </div>\n</mat-form-field>\n" }]
|
|
20152
20289
|
}], ctorParameters: () => [], propDecorators: { dtControl: [{
|
|
20153
20290
|
type: ViewChild,
|
|
20154
20291
|
args: ['dtControl']
|
|
@@ -20673,11 +20810,11 @@ class MoneyComponent {
|
|
|
20673
20810
|
return "";
|
|
20674
20811
|
}
|
|
20675
20812
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: MoneyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
20676
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: MoneyComponent, isStandalone: false, selector: "spa-money", inputs: { readonly: "readonly", hint: "hint", display: "display", placeholder: "placeholder", value: "value", width: "width", currency: "currency", required: "required", min: "min", max: "max", infoMessage: "infoMessage", copyContent: "copyContent", clearContent: "clearContent", suffix: "suffix" }, outputs: { valueChange: "valueChange", leave: "leave", enterPress: "enterPress", infoClick: "infoClick" }, usesOnChanges: true, ngImport: i0, template: "\n\n<mat-form-field [class.spa-readonly]=\"readonly\" hideRequiredMarker=\"true\" [hintLabel]=\"hint\" [hideRequiredMarker]=\"!required\" [ngStyle]=\"{'width':width ?? '100%'}\" hideRequiredMarker=\"true\" style=\"margin-right: 5px;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\"><!-- Changed: onMouseEnter/onMouseLeave existed in the .ts but NOTHING bound them, so isHovered was permanently false and the spa-suffix copy/clear icons (*ngIf=\"...&&isHovered\") could never render on a money field at all. Same hover idiom as spa-date/spa-text/spa-text-area. -->\n <mat-label>{{display}}</mat-label>\n <!-- Changed: added inputmode=\"decimal\" so a phone raises the numeric keypad. The input stays type=\"text\" on\n purpose \u2014 it must hold the thousands-separated mask (\"1,234.56\"), which type=\"number\" refuses to display.\n Without inputmode the field defaulted to the full alphanumeric keyboard, which is why this fell apart on\n mobile while behaving on desktop. -->\n <input matInput appCurrencyInputMask autocomplete=\"off\" inputmode=\"decimal\" style=\"text-align: right;\"\n [min]=\"min\" [max]=\"max\"\n (keyup.enter)=\"enterPressed()\" (blur)=\"leaved()\" [placeholder]=\"computedPlaceholder\" [formControl]=\"control\" [readonly]=\"readonly\" />\n <span *ngIf=\"currency!=''\" matPrefix>{{currency}} </span>\n <mat-error *ngIf=\"control.invalid\">{{validate(control)}}</mat-error>\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n\n</mat-form-field>\n\n", styles: ["input.example-right-align{-moz-appearance:textfield}.example-right-align{text-align:right}input.example-right-align::-webkit-outer-spin-button,input.example-right-align::-webkit-inner-spin-button{display:none}.curr{background-color:red}\n"], dependencies: [{ 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: i2.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "directive", type: CurrencyInputMaskDirective, selector: "[appCurrencyInputMask]" }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }] }); }
|
|
20813
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: MoneyComponent, isStandalone: false, selector: "spa-money", inputs: { readonly: "readonly", hint: "hint", display: "display", placeholder: "placeholder", value: "value", width: "width", currency: "currency", required: "required", min: "min", max: "max", infoMessage: "infoMessage", copyContent: "copyContent", clearContent: "clearContent", suffix: "suffix" }, outputs: { valueChange: "valueChange", leave: "leave", enterPress: "enterPress", infoClick: "infoClick" }, usesOnChanges: true, ngImport: i0, template: "\n\n<mat-form-field [class.spa-readonly]=\"readonly\" hideRequiredMarker=\"true\" [hintLabel]=\"hint\" [hideRequiredMarker]=\"!required\" [ngStyle]=\"{'width':width ?? '100%'}\" hideRequiredMarker=\"true\" style=\"margin-right: 5px;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\"><!-- Changed: onMouseEnter/onMouseLeave existed in the .ts but NOTHING bound them, so isHovered was permanently false and the spa-suffix copy/clear icons (*ngIf=\"...&&isHovered\") could never render on a money field at all. Same hover idiom as spa-date/spa-text/spa-text-area. -->\n <mat-label>{{display}}</mat-label>\n <!-- Changed: added inputmode=\"decimal\" so a phone raises the numeric keypad. The input stays type=\"text\" on\n purpose \u2014 it must hold the thousands-separated mask (\"1,234.56\"), which type=\"number\" refuses to display.\n Without inputmode the field defaulted to the full alphanumeric keyboard, which is why this fell apart on\n mobile while behaving on desktop. -->\n <input matInput appCurrencyInputMask autocomplete=\"off\" inputmode=\"decimal\" style=\"text-align: right;\"\n [min]=\"min\" [max]=\"max\"\n (keyup.enter)=\"enterPressed()\" (blur)=\"leaved()\" [placeholder]=\"computedPlaceholder\" [formControl]=\"control\" [readonly]=\"readonly\" />\n <span *ngIf=\"currency!=''\" matPrefix>{{currency}} </span>\n <mat-error *ngIf=\"control.invalid\">{{validate(control)}}</mat-error>\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n\n</mat-form-field>\n\n", styles: ["input.example-right-align{-moz-appearance:textfield}.example-right-align{text-align:right}input.example-right-align::-webkit-outer-spin-button,input.example-right-align::-webkit-inner-spin-button{display:none}.curr{background-color:red}\n"], dependencies: [{ 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: i2.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "directive", type: CurrencyInputMaskDirective, selector: "[appCurrencyInputMask]" }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "readonly", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }] }); }
|
|
20677
20814
|
}
|
|
20678
20815
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: MoneyComponent, decorators: [{
|
|
20679
20816
|
type: Component,
|
|
20680
|
-
args: [{ selector: 'spa-money', standalone: false, template: "\n\n<mat-form-field [class.spa-readonly]=\"readonly\" hideRequiredMarker=\"true\" [hintLabel]=\"hint\" [hideRequiredMarker]=\"!required\" [ngStyle]=\"{'width':width ?? '100%'}\" hideRequiredMarker=\"true\" style=\"margin-right: 5px;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\"><!-- Changed: onMouseEnter/onMouseLeave existed in the .ts but NOTHING bound them, so isHovered was permanently false and the spa-suffix copy/clear icons (*ngIf=\"...&&isHovered\") could never render on a money field at all. Same hover idiom as spa-date/spa-text/spa-text-area. -->\n <mat-label>{{display}}</mat-label>\n <!-- Changed: added inputmode=\"decimal\" so a phone raises the numeric keypad. The input stays type=\"text\" on\n purpose \u2014 it must hold the thousands-separated mask (\"1,234.56\"), which type=\"number\" refuses to display.\n Without inputmode the field defaulted to the full alphanumeric keyboard, which is why this fell apart on\n mobile while behaving on desktop. -->\n <input matInput appCurrencyInputMask autocomplete=\"off\" inputmode=\"decimal\" style=\"text-align: right;\"\n [min]=\"min\" [max]=\"max\"\n (keyup.enter)=\"enterPressed()\" (blur)=\"leaved()\" [placeholder]=\"computedPlaceholder\" [formControl]=\"control\" [readonly]=\"readonly\" />\n <span *ngIf=\"currency!=''\" matPrefix>{{currency}} </span>\n <mat-error *ngIf=\"control.invalid\">{{validate(control)}}</mat-error>\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n\n</mat-form-field>\n\n", styles: ["input.example-right-align{-moz-appearance:textfield}.example-right-align{text-align:right}input.example-right-align::-webkit-outer-spin-button,input.example-right-align::-webkit-inner-spin-button{display:none}.curr{background-color:red}\n"] }]
|
|
20817
|
+
args: [{ selector: 'spa-money', standalone: false, template: "\n\n<mat-form-field [class.spa-readonly]=\"readonly\" hideRequiredMarker=\"true\" [hintLabel]=\"hint\" [hideRequiredMarker]=\"!required\" [ngStyle]=\"{'width':width ?? '100%'}\" hideRequiredMarker=\"true\" style=\"margin-right: 5px;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\"><!-- Changed: onMouseEnter/onMouseLeave existed in the .ts but NOTHING bound them, so isHovered was permanently false and the spa-suffix copy/clear icons (*ngIf=\"...&&isHovered\") could never render on a money field at all. Same hover idiom as spa-date/spa-text/spa-text-area. -->\n <mat-label>{{display}}</mat-label>\n <!-- Changed: added inputmode=\"decimal\" so a phone raises the numeric keypad. The input stays type=\"text\" on\n purpose \u2014 it must hold the thousands-separated mask (\"1,234.56\"), which type=\"number\" refuses to display.\n Without inputmode the field defaulted to the full alphanumeric keyboard, which is why this fell apart on\n mobile while behaving on desktop. -->\n <input matInput appCurrencyInputMask autocomplete=\"off\" inputmode=\"decimal\" style=\"text-align: right;\"\n [min]=\"min\" [max]=\"max\"\n (keyup.enter)=\"enterPressed()\" (blur)=\"leaved()\" [placeholder]=\"computedPlaceholder\" [formControl]=\"control\" [readonly]=\"readonly\" />\n <span *ngIf=\"currency!=''\" matPrefix>{{currency}} </span>\n <mat-error *ngIf=\"control.invalid\">{{validate(control)}}</mat-error>\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n\n</mat-form-field>\n\n", styles: ["input.example-right-align{-moz-appearance:textfield}.example-right-align{text-align:right}input.example-right-align::-webkit-outer-spin-button,input.example-right-align::-webkit-inner-spin-button{display:none}.curr{background-color:red}\n"] }]
|
|
20681
20818
|
}], ctorParameters: () => [], propDecorators: { readonly: [{
|
|
20682
20819
|
type: Input
|
|
20683
20820
|
}], hint: [{
|
|
@@ -20920,11 +21057,11 @@ class NumberComponent {
|
|
|
20920
21057
|
return "";
|
|
20921
21058
|
}
|
|
20922
21059
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: NumberComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
20923
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: NumberComponent, isStandalone: false, selector: "spa-number", inputs: { readonly: "readonly", hint: "hint", display: "display", placeholder: "placeholder", value: "value", width: "width", required: "required", min: "min", max: "max", step: "step", suffix: "suffix", infoMessage: "infoMessage", copyContent: "copyContent", clearContent: "clearContent" }, outputs: { valueChange: "valueChange", leave: "leave", enterPress: "enterPress", infoClick: "infoClick" }, usesOnChanges: true, ngImport: i0, template: "\n<mat-form-field [class.spa-readonly]=\"readonly\" hideRequiredMarker=\"true\" [hintLabel]=\"hint\" [hideRequiredMarker]=\"!required\" [ngStyle]=\"{'width':width ?? '100%'}\" style=\"margin-right: 5px;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\"><!-- Changed: same omission as spa-money \u2014 the hover handlers existed in the .ts and nothing bound them, so isHovered never became true and the copy/clear icons could never render on a number field. -->\n <mat-label>{{display}}</mat-label>\n <input matInput autocomplete=\"off\" type=\"number\" [attr.inputmode]=\"inputMode\" [min]=\"min\" [max]=\"max\" [step]=\"step\" (keydown)=\"blockNonNumericKeys($event)\" (focus)=\"selectAllOnFocus($event)\" (blur)=\"leaved()\" (keyup.enter)=\"enterPressed()\" [placeholder]=\"computedPlaceholder\" [formControl]=\"control\" [required]=\"required\" [readonly]=\"readonly\" />\n <mat-error *ngIf=\"control.invalid\">{{validate(control)}}</mat-error>\n\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [""], dependencies: [{ 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: i2.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.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }] }); }
|
|
21060
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: NumberComponent, isStandalone: false, selector: "spa-number", inputs: { readonly: "readonly", hint: "hint", display: "display", placeholder: "placeholder", value: "value", width: "width", required: "required", min: "min", max: "max", step: "step", suffix: "suffix", infoMessage: "infoMessage", copyContent: "copyContent", clearContent: "clearContent" }, outputs: { valueChange: "valueChange", leave: "leave", enterPress: "enterPress", infoClick: "infoClick" }, usesOnChanges: true, ngImport: i0, template: "\n<mat-form-field [class.spa-readonly]=\"readonly\" hideRequiredMarker=\"true\" [hintLabel]=\"hint\" [hideRequiredMarker]=\"!required\" [ngStyle]=\"{'width':width ?? '100%'}\" style=\"margin-right: 5px;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\"><!-- Changed: same omission as spa-money \u2014 the hover handlers existed in the .ts and nothing bound them, so isHovered never became true and the copy/clear icons could never render on a number field. -->\n <mat-label>{{display}}</mat-label>\n <input matInput autocomplete=\"off\" type=\"number\" [attr.inputmode]=\"inputMode\" [min]=\"min\" [max]=\"max\" [step]=\"step\" (keydown)=\"blockNonNumericKeys($event)\" (focus)=\"selectAllOnFocus($event)\" (blur)=\"leaved()\" (keyup.enter)=\"enterPressed()\" [placeholder]=\"computedPlaceholder\" [formControl]=\"control\" [required]=\"required\" [readonly]=\"readonly\" />\n <mat-error *ngIf=\"control.invalid\">{{validate(control)}}</mat-error>\n\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [""], dependencies: [{ 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: i2.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.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "readonly", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }] }); }
|
|
20924
21061
|
}
|
|
20925
21062
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: NumberComponent, decorators: [{
|
|
20926
21063
|
type: Component,
|
|
20927
|
-
args: [{ selector: 'spa-number', standalone: false, template: "\n<mat-form-field [class.spa-readonly]=\"readonly\" hideRequiredMarker=\"true\" [hintLabel]=\"hint\" [hideRequiredMarker]=\"!required\" [ngStyle]=\"{'width':width ?? '100%'}\" style=\"margin-right: 5px;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\"><!-- Changed: same omission as spa-money \u2014 the hover handlers existed in the .ts and nothing bound them, so isHovered never became true and the copy/clear icons could never render on a number field. -->\n <mat-label>{{display}}</mat-label>\n <input matInput autocomplete=\"off\" type=\"number\" [attr.inputmode]=\"inputMode\" [min]=\"min\" [max]=\"max\" [step]=\"step\" (keydown)=\"blockNonNumericKeys($event)\" (focus)=\"selectAllOnFocus($event)\" (blur)=\"leaved()\" (keyup.enter)=\"enterPressed()\" [placeholder]=\"computedPlaceholder\" [formControl]=\"control\" [required]=\"required\" [readonly]=\"readonly\" />\n <mat-error *ngIf=\"control.invalid\">{{validate(control)}}</mat-error>\n\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n" }]
|
|
21064
|
+
args: [{ selector: 'spa-number', standalone: false, template: "\n<mat-form-field [class.spa-readonly]=\"readonly\" hideRequiredMarker=\"true\" [hintLabel]=\"hint\" [hideRequiredMarker]=\"!required\" [ngStyle]=\"{'width':width ?? '100%'}\" style=\"margin-right: 5px;\" subscriptSizing=\"dynamic\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\"><!-- Changed: same omission as spa-money \u2014 the hover handlers existed in the .ts and nothing bound them, so isHovered never became true and the copy/clear icons could never render on a number field. -->\n <mat-label>{{display}}</mat-label>\n <input matInput autocomplete=\"off\" type=\"number\" [attr.inputmode]=\"inputMode\" [min]=\"min\" [max]=\"max\" [step]=\"step\" (keydown)=\"blockNonNumericKeys($event)\" (focus)=\"selectAllOnFocus($event)\" (blur)=\"leaved()\" (keyup.enter)=\"enterPressed()\" [placeholder]=\"computedPlaceholder\" [formControl]=\"control\" [required]=\"required\" [readonly]=\"readonly\" />\n <mat-error *ngIf=\"control.invalid\">{{validate(control)}}</mat-error>\n\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clear()\" [isHovered]=\"isHovered\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n" }]
|
|
20928
21065
|
}], ctorParameters: () => [], propDecorators: { readonly: [{
|
|
20929
21066
|
type: Input
|
|
20930
21067
|
}], hint: [{
|
|
@@ -21056,11 +21193,11 @@ class EmailComponent {
|
|
|
21056
21193
|
});
|
|
21057
21194
|
}
|
|
21058
21195
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: EmailComponent, deps: [{ token: MessageService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
21059
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: EmailComponent, isStandalone: false, selector: "spa-email", inputs: { display: "display", value: "value", readonly: "readonly", required: "required", hint: "hint", suffix: "suffix", infoMessage: "infoMessage", copyContent: "copyContent", clearContent: "clearContent", options: "options", optionValue: "optionValue" }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<mat-form-field [class.spa-readonly]=\"readonly\" style=\"width: 100%;\" subscriptSizing=\"dynamic\">\n <mat-label>{{display}}</mat-label>\n <mat-chip-grid #chipList>\n <mat-chip-row *ngFor=\"let email of emails\" [removable]=\"!readonly\" (removed)=\"removeEmail(email)\">\n {{email}}\n <button matChipRemove *ngIf=\"!readonly\"><mat-icon class=\"tinyIcon\">cancel</mat-icon></button>\n </mat-chip-row>\n <input autocomplete=\"off\" #emailInput [placeholder]=\"readonly ? '' : 'Enter email addresses'\" [matChipInputFor]=\"chipList\" [matChipInputSeparatorKeyCodes]=\"[13, 186]\" [matChipInputAddOnBlur]=\"true\" (matChipInputTokenEnd)=\"addEmail($event)\" [formControl]=\"emailControl\" (paste)=\"handlePaste($event)\" [readonly]=\"readonly\" [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\">\n </mat-chip-grid>\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"optionSelected($event)\">\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"optionValue ? option[optionValue] : option\">\n {{optionValue ? option[optionValue] : option}}\n </mat-option>\n </mat-autocomplete>\n <mat-error *ngIf=\"emailControl.invalid && emailControl.touched\">Invalid email format</mat-error>\n <mat-hint *ngIf=\"hint\">{{hint}}</mat-hint>\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clearEmails()\" [isHovered]=\"true\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [":host ::ng-deep .mat-form-field-flex{align-items:flex-start}:host ::ng-deep .mat-form-field-infix{max-height:170px;overflow-y:auto;padding-top:.25em;padding-bottom:.25em;overflow-x:hidden}:host ::ng-deep mat-chip{flex:0 0 auto}\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: i2.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i6.MatChipGrid, selector: "mat-chip-grid", inputs: ["disabled", "placeholder", "required", "value", "errorStateMatcher"], outputs: ["change", "valueChange"] }, { kind: "directive", type: i6.MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputFor", "matChipInputAddOnBlur", "matChipInputSeparatorKeyCodes", "placeholder", "id", "disabled", "readonly", "matChipInputDisabledInteractive"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { kind: "directive", type: i6.MatChipRemove, selector: "[matChipRemove]" }, { kind: "component", type: i6.MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i14.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "directive", type: i14.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }] }); }
|
|
21196
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: EmailComponent, isStandalone: false, selector: "spa-email", inputs: { display: "display", value: "value", readonly: "readonly", required: "required", hint: "hint", suffix: "suffix", infoMessage: "infoMessage", copyContent: "copyContent", clearContent: "clearContent", options: "options", optionValue: "optionValue" }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<mat-form-field [class.spa-readonly]=\"readonly\" style=\"width: 100%;\" subscriptSizing=\"dynamic\">\n <mat-label>{{display}}</mat-label>\n <mat-chip-grid #chipList>\n <mat-chip-row *ngFor=\"let email of emails\" [removable]=\"!readonly\" (removed)=\"removeEmail(email)\">\n {{email}}\n <button matChipRemove *ngIf=\"!readonly\"><mat-icon class=\"tinyIcon\">cancel</mat-icon></button>\n </mat-chip-row>\n <input autocomplete=\"off\" #emailInput [placeholder]=\"readonly ? '' : 'Enter email addresses'\" [matChipInputFor]=\"chipList\" [matChipInputSeparatorKeyCodes]=\"[13, 186]\" [matChipInputAddOnBlur]=\"true\" (matChipInputTokenEnd)=\"addEmail($event)\" [formControl]=\"emailControl\" (paste)=\"handlePaste($event)\" [readonly]=\"readonly\" [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\">\n </mat-chip-grid>\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"optionSelected($event)\">\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"optionValue ? option[optionValue] : option\">\n {{optionValue ? option[optionValue] : option}}\n </mat-option>\n </mat-autocomplete>\n <mat-error *ngIf=\"emailControl.invalid && emailControl.touched\">Invalid email format</mat-error>\n <mat-hint *ngIf=\"hint\">{{hint}}</mat-hint>\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clearEmails()\" [isHovered]=\"true\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [":host ::ng-deep .mat-form-field-flex{align-items:flex-start}:host ::ng-deep .mat-form-field-infix{max-height:170px;overflow-y:auto;padding-top:.25em;padding-bottom:.25em;overflow-x:hidden}:host ::ng-deep mat-chip{flex:0 0 auto}\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: i2.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i6.MatChipGrid, selector: "mat-chip-grid", inputs: ["disabled", "placeholder", "required", "value", "errorStateMatcher"], outputs: ["change", "valueChange"] }, { kind: "directive", type: i6.MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputFor", "matChipInputAddOnBlur", "matChipInputSeparatorKeyCodes", "placeholder", "id", "disabled", "readonly", "matChipInputDisabledInteractive"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { kind: "directive", type: i6.MatChipRemove, selector: "[matChipRemove]" }, { kind: "component", type: i6.MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i14.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "directive", type: i14.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "component", type: SuffixComponent, selector: "spa-suffix", inputs: ["label", "infoMessage", "copyContent", "isHovered", "clearContent", "readonly", "value"], outputs: ["infoClick", "copyClick", "clearClick", "valueChange"] }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }] }); }
|
|
21060
21197
|
}
|
|
21061
21198
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: EmailComponent, decorators: [{
|
|
21062
21199
|
type: Component,
|
|
21063
|
-
args: [{ selector: 'spa-email', standalone: false, template: "<mat-form-field [class.spa-readonly]=\"readonly\" style=\"width: 100%;\" subscriptSizing=\"dynamic\">\n <mat-label>{{display}}</mat-label>\n <mat-chip-grid #chipList>\n <mat-chip-row *ngFor=\"let email of emails\" [removable]=\"!readonly\" (removed)=\"removeEmail(email)\">\n {{email}}\n <button matChipRemove *ngIf=\"!readonly\"><mat-icon class=\"tinyIcon\">cancel</mat-icon></button>\n </mat-chip-row>\n <input autocomplete=\"off\" #emailInput [placeholder]=\"readonly ? '' : 'Enter email addresses'\" [matChipInputFor]=\"chipList\" [matChipInputSeparatorKeyCodes]=\"[13, 186]\" [matChipInputAddOnBlur]=\"true\" (matChipInputTokenEnd)=\"addEmail($event)\" [formControl]=\"emailControl\" (paste)=\"handlePaste($event)\" [readonly]=\"readonly\" [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\">\n </mat-chip-grid>\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"optionSelected($event)\">\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"optionValue ? option[optionValue] : option\">\n {{optionValue ? option[optionValue] : option}}\n </mat-option>\n </mat-autocomplete>\n <mat-error *ngIf=\"emailControl.invalid && emailControl.touched\">Invalid email format</mat-error>\n <mat-hint *ngIf=\"hint\">{{hint}}</mat-hint>\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" (clearClick)=\"clearEmails()\" [isHovered]=\"true\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [":host ::ng-deep .mat-form-field-flex{align-items:flex-start}:host ::ng-deep .mat-form-field-infix{max-height:170px;overflow-y:auto;padding-top:.25em;padding-bottom:.25em;overflow-x:hidden}:host ::ng-deep mat-chip{flex:0 0 auto}\n"] }]
|
|
21200
|
+
args: [{ selector: 'spa-email', standalone: false, template: "<mat-form-field [class.spa-readonly]=\"readonly\" style=\"width: 100%;\" subscriptSizing=\"dynamic\">\n <mat-label>{{display}}</mat-label>\n <mat-chip-grid #chipList>\n <mat-chip-row *ngFor=\"let email of emails\" [removable]=\"!readonly\" (removed)=\"removeEmail(email)\">\n {{email}}\n <button matChipRemove *ngIf=\"!readonly\"><mat-icon class=\"tinyIcon\">cancel</mat-icon></button>\n </mat-chip-row>\n <input autocomplete=\"off\" #emailInput [placeholder]=\"readonly ? '' : 'Enter email addresses'\" [matChipInputFor]=\"chipList\" [matChipInputSeparatorKeyCodes]=\"[13, 186]\" [matChipInputAddOnBlur]=\"true\" (matChipInputTokenEnd)=\"addEmail($event)\" [formControl]=\"emailControl\" (paste)=\"handlePaste($event)\" [readonly]=\"readonly\" [matAutocomplete]=\"auto\" [matAutocompleteDisabled]=\"readonly\">\n </mat-chip-grid>\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"optionSelected($event)\">\n <mat-option *ngFor=\"let option of filteredOptions | async\" [value]=\"optionValue ? option[optionValue] : option\">\n {{optionValue ? option[optionValue] : option}}\n </mat-option>\n </mat-autocomplete>\n <mat-error *ngIf=\"emailControl.invalid && emailControl.touched\">Invalid email format</mat-error>\n <mat-hint *ngIf=\"hint\">{{hint}}</mat-hint>\n <div matSuffix class=\"suffix-icons\">\n <spa-suffix [label]=\"suffix\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [clearContent]=\"clearContent\" [readonly]=\"readonly\" (clearClick)=\"clearEmails()\" [isHovered]=\"true\" [(value)]=\"value\"></spa-suffix>\n </div>\n</mat-form-field>\n", styles: [":host ::ng-deep .mat-form-field-flex{align-items:flex-start}:host ::ng-deep .mat-form-field-infix{max-height:170px;overflow-y:auto;padding-top:.25em;padding-bottom:.25em;overflow-x:hidden}:host ::ng-deep mat-chip{flex:0 0 auto}\n"] }]
|
|
21064
21201
|
}], ctorParameters: () => [{ type: MessageService }], propDecorators: { display: [{
|
|
21065
21202
|
type: Input
|
|
21066
21203
|
}], value: [{
|
|
@@ -21346,6 +21483,25 @@ class GroupsComponent {
|
|
|
21346
21483
|
this.dataService = dataService;
|
|
21347
21484
|
this.snackBar = snackBar;
|
|
21348
21485
|
this.apiErrorService = apiErrorService;
|
|
21486
|
+
//---------------- Row reordering within a group (opt-in via config.orderable) ----------------
|
|
21487
|
+
// Added (grouped-orderable): the grouped view's answer to the flat table's drag-to-reorder. Everything below is
|
|
21488
|
+
// inert — and every affordance absent from the DOM — unless a config declares `orderable`. It is a SEPARATE
|
|
21489
|
+
// feature from groupConfig.dragEnabled/dropAction, which moves an item BETWEEN groups; this one sequences items
|
|
21490
|
+
// WITHIN one, and the group's own key is the scope.
|
|
21491
|
+
// A FIELD, not a getter: `orderable: true` normalises to an object, and allocating that on every
|
|
21492
|
+
// change-detection tick is the template-bound-allocating-getter livelock this library is scarred by.
|
|
21493
|
+
this.orderableConfig = null;
|
|
21494
|
+
// Built lazily for the same reason as in spa-table — see RowOrderService's own note.
|
|
21495
|
+
this.rowOrderService = null;
|
|
21496
|
+
// Refuses a drop coming from a DIFFERENT group unless the cross-group move feature is actually switched on.
|
|
21497
|
+
// The container is wrapped in cdkDropListGroup, so without this an orderable-only grouped view would happily
|
|
21498
|
+
// offer a cross-group drag that has nowhere to persist to — the scope is the group, and moving between scopes
|
|
21499
|
+
// is a different write. An arrow property because the template binds it unbound.
|
|
21500
|
+
this.cdkNoCrossGroup = (drag, drop) => {
|
|
21501
|
+
if (this.config?.groupConfig?.dragEnabled)
|
|
21502
|
+
return true; // the pre-existing feature is on — behave exactly as before
|
|
21503
|
+
return drag?.dropContainer === drop; // orderable-only: a drop is accepted only from its own list
|
|
21504
|
+
};
|
|
21349
21505
|
// Added: stable-reference caches — all three of these are bound to *ngFor (per group and per item), and
|
|
21350
21506
|
// each call re-filtered into a NEW array on every change-detection cycle.
|
|
21351
21507
|
this.iconCache = new WeakMap();
|
|
@@ -21366,8 +21522,41 @@ class GroupsComponent {
|
|
|
21366
21522
|
this.highlightedGroup = null; // Changed: Track which group is being hovered during drag
|
|
21367
21523
|
}
|
|
21368
21524
|
ngOnInit() {
|
|
21525
|
+
this.setupOrderable(); // Added (grouped-orderable): resolve the opt-in ONCE, never in a template getter
|
|
21369
21526
|
this.mapServerData();
|
|
21370
21527
|
}
|
|
21528
|
+
get rowOrder() {
|
|
21529
|
+
return this.rowOrderService ??= new RowOrderService(this.dataService, this.apiErrorService);
|
|
21530
|
+
}
|
|
21531
|
+
setupOrderable() {
|
|
21532
|
+
this.orderableConfig = this.rowOrder.normalize(this.config?.orderable); // false/absent → null → zero affordances
|
|
21533
|
+
}
|
|
21534
|
+
// Whether items can be picked up at ALL. The pre-existing cross-group feature and the new within-group one are
|
|
21535
|
+
// independent opt-ins, and either is enough to make an item draggable. A config that declares NEITHER is
|
|
21536
|
+
// byte-identical to before: cdkDropListDisabled stays true and cdkDrag stays disabled, exactly as it was.
|
|
21537
|
+
get itemsDraggable() {
|
|
21538
|
+
return !!this.config?.groupConfig?.dragEnabled || !!this.orderableConfig;
|
|
21539
|
+
}
|
|
21540
|
+
// The filter rewrites a group's items into a NEW, SHORTER array (applyFilter builds {...group, items: matching}),
|
|
21541
|
+
// so what is on screen is not the complete scope — and the reorder contract requires every id in it. Same
|
|
21542
|
+
// refusal the flat table makes, for the same reason, worded the same way. There is no paginator or column sort
|
|
21543
|
+
// in the grouped view, so those two flat rules have no counterpart here.
|
|
21544
|
+
get orderDisabledReason() {
|
|
21545
|
+
if (!this.orderableConfig)
|
|
21546
|
+
return null;
|
|
21547
|
+
if (this.filterText?.trim())
|
|
21548
|
+
return 'Clear the filter to reorder';
|
|
21549
|
+
return null;
|
|
21550
|
+
}
|
|
21551
|
+
get orderEnabled() {
|
|
21552
|
+
return !!this.orderableConfig && !this.orderDisabledReason;
|
|
21553
|
+
}
|
|
21554
|
+
// Whether a drag may currently HAPPEN, as opposed to whether items are draggable at all. Bound to
|
|
21555
|
+
// cdkDropListDisabled and read by the grip's greying, so a dead drop list and a bright grip cannot disagree —
|
|
21556
|
+
// the same single-source rule the flat table applies.
|
|
21557
|
+
get dragActive() {
|
|
21558
|
+
return !!this.config?.groupConfig?.dragEnabled || this.orderEnabled;
|
|
21559
|
+
}
|
|
21371
21560
|
invalidateVisibilityCaches() {
|
|
21372
21561
|
this.iconCache = new WeakMap();
|
|
21373
21562
|
this.headerButtonCache = new WeakMap();
|
|
@@ -21375,6 +21564,8 @@ class GroupsComponent {
|
|
|
21375
21564
|
}
|
|
21376
21565
|
ngOnChanges(changes) {
|
|
21377
21566
|
this.invalidateVisibilityCaches(); // Added: inputs changed — visibility predicates must be re-evaluated
|
|
21567
|
+
if (changes['config'])
|
|
21568
|
+
this.setupOrderable(); // Added (grouped-orderable): a swapped config must re-resolve the opt-in
|
|
21378
21569
|
if (changes['dataSource']) {
|
|
21379
21570
|
this.mapServerData();
|
|
21380
21571
|
}
|
|
@@ -21462,8 +21653,16 @@ class GroupsComponent {
|
|
|
21462
21653
|
onDrop(event) {
|
|
21463
21654
|
this.highlightedGroup = null; // Changed: Clear highlight on drop
|
|
21464
21655
|
if (event.previousContainer === event.container) {
|
|
21465
|
-
//
|
|
21656
|
+
// Changed (grouped-orderable): a within-group move used to be an optimistic shuffle that PERSISTED NOTHING —
|
|
21657
|
+
// the array moved, the server never heard about it, and the next load put everything back. It now posts the
|
|
21658
|
+
// same reorder request the flat table does, scoped to this group. A grouped view WITHOUT `orderable` keeps
|
|
21659
|
+
// the old behaviour exactly: the move stays local, because that is all it ever asked for.
|
|
21660
|
+
if (event.previousIndex === event.currentIndex)
|
|
21661
|
+
return; // landed where it started — not a write
|
|
21466
21662
|
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
|
|
21663
|
+
if (!this.orderEnabled)
|
|
21664
|
+
return;
|
|
21665
|
+
this.persistGroupOrder(event.container.data, event.previousIndex, event.currentIndex);
|
|
21467
21666
|
return;
|
|
21468
21667
|
}
|
|
21469
21668
|
const item = event.previousContainer.data[event.previousIndex];
|
|
@@ -21491,6 +21690,35 @@ class GroupsComponent {
|
|
|
21491
21690
|
});
|
|
21492
21691
|
}
|
|
21493
21692
|
}
|
|
21693
|
+
// Added (grouped-orderable): POST the complete ordered id list for THIS group, revert the optimistic move if the
|
|
21694
|
+
// server refuses. The scope is the group's own key unless the config names one explicitly — scopeValue and
|
|
21695
|
+
// scopeField still win, in that order, which is what "honours scopeField" means here.
|
|
21696
|
+
persistGroupOrder(items, previousIndex, currentIndex) {
|
|
21697
|
+
const revert = () => { moveItemInArray(items, currentIndex, previousIndex); };
|
|
21698
|
+
const ids = items.map(i => this.rowOrderId(i)).filter(id => id !== null && id !== undefined);
|
|
21699
|
+
const refusal = this.rowOrder.validateIds(ids, items.length);
|
|
21700
|
+
const url = this.rowOrder.action(this.orderableConfig, this.config?.loadAction).url;
|
|
21701
|
+
if (refusal) {
|
|
21702
|
+
revert();
|
|
21703
|
+
this.rowOrder.presentLocalRefusal(refusal, url);
|
|
21704
|
+
return;
|
|
21705
|
+
}
|
|
21706
|
+
const group = this.groupedData.find(g => g.items === items);
|
|
21707
|
+
const scope = this.rowOrder.scope(this.orderableConfig, items, group?.groupValue);
|
|
21708
|
+
this.rowOrder.persist(this.orderableConfig, this.config?.loadAction, ids, scope, { revert });
|
|
21709
|
+
}
|
|
21710
|
+
// The item's identity for the posted id list. idField first — the sharp edge banked by reorder-demo-1 is that a
|
|
21711
|
+
// config's loadIDField can name the PARENT's id and beat the ...ID convention, so every row reports the same id
|
|
21712
|
+
// and the server refuses the lot. No heroField/trackBy cache here: the grouped view has neither.
|
|
21713
|
+
rowOrderId(item) {
|
|
21714
|
+
return this.rowOrder.resolveId(item, this.orderableConfig, [this.findIdKey(item)]);
|
|
21715
|
+
}
|
|
21716
|
+
// Same ...ID convention spa-table uses for trackBy, and TenantID is excluded there for the same reason.
|
|
21717
|
+
findIdKey(item) {
|
|
21718
|
+
if (!item)
|
|
21719
|
+
return null;
|
|
21720
|
+
return Object.keys(item).find(k => k.endsWith('ID') && k !== 'TenantID') || null;
|
|
21721
|
+
}
|
|
21494
21722
|
getItemText(item) {
|
|
21495
21723
|
const textField = this.config.groupConfig.itemTextField;
|
|
21496
21724
|
return item[textField] ?? '';
|
|
@@ -21618,11 +21846,11 @@ class GroupsComponent {
|
|
|
21618
21846
|
});
|
|
21619
21847
|
}
|
|
21620
21848
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: GroupsComponent, deps: [{ token: ConditionService }, { token: ButtonService }, { token: DataServiceLib }, { token: i1$4.MatSnackBar }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
21621
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: GroupsComponent, isStandalone: false, selector: "spa-groups", inputs: { config: "config", dataSource: "dataSource", displayedButtons: "displayedButtons", showOwnFilter: "showOwnFilter", filterText: "filterText" }, outputs: { actionClick: "actionClick" }, usesOnChanges: true, ngImport: i0, template: "<!-- Changed: this filter row was REMOVED from the grouped view's own markup and now lives in the table header,\n beside the action buttons, where every other table config puts it. It used to sit on a row of its own\n directly under the buttons row, leaving both rows half empty and visibly misaligned.\n The filtering logic here is untouched \u2014 only the input that drives it moved, and its text arrives via the\n filterText @Input relayed from the header. Kept as a fallback for a grouped view rendered WITHOUT the\n standard header (spa-groups can be used directly), so filtering is never simply lost. -->\n<div class=\"groups-filter\" *ngIf=\"showOwnFilter\">\n <mat-form-field appearance=\"outline\" class=\"filter-field\">\n <mat-icon matPrefix>search</mat-icon>\n <input matInput placeholder=\"Filter\" [(ngModel)]=\"filterText\" (ngModelChange)=\"applyFilter()\">\n <button *ngIf=\"filterText\" mat-icon-button matSuffix (click)=\"filterText = ''; applyFilter()\">\n <mat-icon>close</mat-icon>\n </button>\n </mat-form-field>\n</div>\n\n<div class=\"groups-container\" cdkDropListGroup>\n\n <mat-card *ngFor=\"let group of groupedData\" class=\"group-card\">\n\n <div class=\"group-header\">\n <div class=\"header-left\">\n <mat-icon *ngIf=\"group.icon\" [style.color]=\"group.color\" class=\"group-icon\">{{group.icon}}</mat-icon>\n <!-- Changed: added the native title attribute. On a phone the title now truncates with an ellipsis rather\n than wrapping to a second line, so the full name has to stay reachable somewhere \u2014 this is the\n zero-cost way to keep it. No tooltip directive, so nothing changes on touch. -->\n <label class=\"group-title\" [title]=\"group.displayName\">{{group.displayName}}</label>\n <label *ngIf=\"config.groupConfig.showGroupCount !== false\" class=\"group-count\">({{group.items.length}})</label>\n </div>\n\n <div class=\"header-right\">\n <button\n *ngFor=\"let button of getVisibleHeaderButtons(group)\"\n mat-icon-button\n [matTooltip]=\"getHeaderButtonTooltip(button)\"\n matTooltipPosition=\"above\"\n [style.color]=\"getHeaderButtonColor(button, group)\"\n [disabled]=\"isHeaderButtonDisabled(button, group)\"\n (click)=\"headerButtonClicked(button, group)\">\n <mat-icon>{{getHeaderButtonIcon(button)}}</mat-icon>\n </button>\n </div>\n </div>\n\n <hr class=\"group-divider\" />\n\n <!-- Empty state: show when no items and drag is disabled -->\n <div *ngIf=\"group.items.length === 0 && !config.groupConfig.dragEnabled\" class=\"empty-state\">\n <label>{{config.groupConfig.emptyGroupMessage ?? 'Empty'}}</label>\n </div>\n\n <!-- Changed: Replaced mat-chip-set with flex container of mat-stroked-button for cleaner button style -->\n <div\n *ngIf=\"group.items.length > 0 || config.groupConfig.dragEnabled\"\n cdkDropList\n [cdkDropListData]=\"group.items\"\n [cdkDropListDisabled]=\"!config.groupConfig.dragEnabled\"\n (cdkDropListDropped)=\"onDrop($event)\"\n (cdkDropListEntered)=\"onDropListEntered($event)\"\n (cdkDropListExited)=\"onDropListExited($event)\"\n [class.drop-highlight]=\"group === highlightedGroup\"\n class=\"drop-list items-container\">\n\n <!-- Empty state inside drop list when drag is enabled -->\n <div *ngIf=\"group.items.length === 0\" class=\"empty-state drop-empty\">\n <label>{{config.groupConfig.emptyGroupMessage ?? 'Empty'}}</label>\n </div>\n\n <button\n mat-stroked-button\n *ngFor=\"let item of group.items\"\n cdkDrag [cdkDragData]=\"item\"\n [cdkDragDisabled]=\"!config.groupConfig.dragEnabled\"\n [matMenuTriggerFor]=\"config.groupConfig.contextMenuEnabled !== false ? itemMenu : null\"\n [matMenuTriggerData]=\"{item: item, group: group}\"\n class=\"item-button\">\n\n <mat-icon\n *ngIf=\"getItemIcon(item)\"\n [style.color]=\"getItemIconColor(item)\"\n [matTooltip]=\"getItemIconTip(item)\"\n matTooltipPosition=\"above\"\n class=\"item-icon\">\n {{getItemIcon(item)}}\n </mat-icon>\n\n <!-- Changed: the item text moved into its own span so a runaway name can be capped and ellipsised without\n also squeezing the icons, which share this projection slot with it. The native title attribute keeps the\n full name recoverable on a truncated chip \u2014 same treatment already used on the group title above, and no\n tooltip directive, so touch behaviour is unchanged. -->\n <span class=\"item-label\" [title]=\"getItemText(item)\">{{getItemText(item)}}</span>\n\n <ng-container *ngFor=\"let additionalIcon of getVisibleAdditionalIcons(item)\">\n <mat-icon\n [style.color]=\"additionalIcon.color\"\n [matTooltip]=\"getIconTooltip(additionalIcon, item)\"\n matTooltipPosition=\"above\"\n class=\"item-additional-icon\">\n {{additionalIcon.name}}\n </mat-icon>\n </ng-container>\n\n </button>\n </div>\n\n </mat-card>\n\n</div>\n\n<mat-menu #itemMenu=\"matMenu\">\n <ng-template matMenuContent let-item=\"item\" let-group=\"group\">\n <!-- Changed: the click below hands itemActionClicked the Button object this *ngFor is already rendering rather than its name. displayedButtons can carry two buttons under one name (table-config.service unshifts the framework approvals buttons onto a table's own array), and the name lookup on the far side always returned the first \u2014 so a grouped item menu drew the app's button and ran the framework's, posting api/null/approve. Every other call on this element already passes the object; only the click did not. Inert where there is no collision: the rendered button IS the first name match there. -->\n <button\n *ngFor=\"let button of getVisibleButtons(item)\"\n mat-menu-item\n [disabled]=\"isButtonDisabled(button, item)\"\n (click)=\"itemActionClicked(button, item, group)\">\n <mat-icon [style.color]=\"getButtonIconColor(button, item)\">{{getButtonIcon(button)}}</mat-icon>\n <span>{{button.display ?? button.tip ?? (button.name | titlecase)}}</span>\n </button>\n </ng-template>\n</mat-menu>\n", styles: [".groups-filter{display:flex;justify-content:flex-end;margin-bottom:8px;padding-right:10px}.filter-field{width:300px;font-size:13px}.filter-field ::ng-deep .mat-mdc-form-field-infix{padding-top:8px!important;padding-bottom:8px!important;min-height:36px}.groups-container{display:flex;flex-direction:column;gap:20px;padding-right:10px;margin-left:0}.group-card{width:100%;margin-bottom:0;padding:8px 16px 16px}.group-header{display:flex;justify-content:space-between;align-items:center;padding:0;min-height:32px}.header-left{display:flex;align-items:center;gap:5px}.header-right{display:flex;align-items:center;gap:4px}.group-icon{margin-right:5px;font-size:24px;width:24px;height:24px;line-height:24px}.group-title{font-size:24px;font-weight:300;line-height:1.2}.group-count{font-size:12px;font-weight:300;margin-left:5px;line-height:1.2}.group-divider{margin-top:8px;margin-bottom:10px}.empty-state{display:flex;justify-content:center;align-items:center;padding:10px}.empty-state label{color:#757575;font-style:italic}.items-container{display:flex;flex-wrap:wrap;gap:10px;padding:8px 0;align-items:center}.item-button{cursor:pointer;font-weight:400;letter-spacing:.01em;transition:box-shadow .2s ease,background-color .2s ease}.item-button:hover{box-shadow:0 1px 4px #00000026;background-color:#00000005}.item-label{display:inline-block;max-width:260px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}.item-icon{font-size:18px;width:18px;height:18px;margin-right:4px;vertical-align:middle}.item-additional-icon{font-size:16px;width:16px;height:16px;margin-left:6px;vertical-align:middle;opacity:.85}button[mat-icon-button]{width:32px;height:32px}button[mat-icon-button] mat-icon{font-size:18px;margin-top:-3px}.header-right button[mat-icon-button]{display:inline-flex;align-items:center;justify-content:center;padding:0;line-height:normal}.header-right button[mat-icon-button] mat-icon{margin:0;font-size:20px;width:20px;height:20px;line-height:20px;display:block}.header-right button[mat-icon-button] .mat-mdc-button-touch-target{display:none}@media (max-width: 700px){.groups-container{gap:12px;padding-right:0}.group-card{padding:8px 10px 10px}.header-left{flex:1 1 auto;min-width:0;gap:4px}.group-icon{flex:0 0 auto;margin-right:2px;font-size:20px;width:20px;height:20px;line-height:20px}.group-title{font-size:16px;font-weight:400;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.group-count{flex:0 0 auto;margin-left:2px;font-size:11px}.header-right{flex:0 0 auto;gap:0}.header-right button[mat-icon-button] mat-icon{font-size:18px;width:18px;height:18px;line-height:18px}.group-header{min-height:28px}.group-divider{margin-top:6px;margin-bottom:6px}.items-container{gap:6px;padding:4px 0}.item-button{min-width:0;height:32px;padding:0 9px;font-size:12.5px;letter-spacing:normal;border-radius:16px;border-color:#0000001f;--mdc-outlined-button-container-height: 32px;--mdc-outlined-button-container-shape: 16px;--mdc-outlined-button-label-text-size: 12.5px}.item-button ::ng-deep .mdc-button__label{letter-spacing:normal;display:inline-flex;align-items:center}.item-button ::ng-deep .mat-mdc-button-touch-target{height:100%}.item-label{max-width:104px}.item-icon{font-size:14px;width:14px;height:14px;margin-right:3px}.item-additional-icon{font-size:14px;width:14px;height:14px;margin-left:3px}.drop-list{min-height:32px}.empty-state{padding:4px 10px}}.drop-list{min-height:40px}.drop-empty{padding:5px 10px}.cdk-drag-preview{box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;opacity:.9;border-radius:4px}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.drop-highlight{border:2px dashed #90caf9;border-radius:8px;background:#90caf90d}.cdk-drop-list-dragging .item-button:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}\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: i2.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i4$2.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i4$2.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i4$2.MatMenuContent, selector: "ng-template[matMenuContent]" }, { kind: "directive", type: i4$2.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i2$1.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: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i7$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: i20.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i20.CdkDropListGroup, selector: "[cdkDropListGroup]", inputs: ["cdkDropListGroupDisabled"], exportAs: ["cdkDropListGroup"] }, { kind: "directive", type: i20.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "pipe", type: i1$2.TitleCasePipe, name: "titlecase" }] }); }
|
|
21849
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: GroupsComponent, isStandalone: false, selector: "spa-groups", inputs: { config: "config", dataSource: "dataSource", displayedButtons: "displayedButtons", showOwnFilter: "showOwnFilter", filterText: "filterText" }, outputs: { actionClick: "actionClick" }, usesOnChanges: true, ngImport: i0, template: "<!-- Changed: this filter row was REMOVED from the grouped view's own markup and now lives in the table header,\n beside the action buttons, where every other table config puts it. It used to sit on a row of its own\n directly under the buttons row, leaving both rows half empty and visibly misaligned.\n The filtering logic here is untouched \u2014 only the input that drives it moved, and its text arrives via the\n filterText @Input relayed from the header. Kept as a fallback for a grouped view rendered WITHOUT the\n standard header (spa-groups can be used directly), so filtering is never simply lost. -->\n<div class=\"groups-filter\" *ngIf=\"showOwnFilter\">\n <mat-form-field appearance=\"outline\" class=\"filter-field\">\n <mat-icon matPrefix>search</mat-icon>\n <input matInput placeholder=\"Filter\" [(ngModel)]=\"filterText\" (ngModelChange)=\"applyFilter()\">\n <button *ngIf=\"filterText\" mat-icon-button matSuffix (click)=\"filterText = ''; applyFilter()\">\n <mat-icon>close</mat-icon>\n </button>\n </mat-form-field>\n</div>\n\n<div class=\"groups-container\" cdkDropListGroup>\n\n <mat-card *ngFor=\"let group of groupedData\" class=\"group-card\">\n\n <div class=\"group-header\">\n <div class=\"header-left\">\n <mat-icon *ngIf=\"group.icon\" [style.color]=\"group.color\" class=\"group-icon\">{{group.icon}}</mat-icon>\n <!-- Changed: added the native title attribute. On a phone the title now truncates with an ellipsis rather\n than wrapping to a second line, so the full name has to stay reachable somewhere \u2014 this is the\n zero-cost way to keep it. No tooltip directive, so nothing changes on touch. -->\n <label class=\"group-title\" [title]=\"group.displayName\">{{group.displayName}}</label>\n <label *ngIf=\"config.groupConfig.showGroupCount !== false\" class=\"group-count\">({{group.items.length}})</label>\n </div>\n\n <div class=\"header-right\">\n <button\n *ngFor=\"let button of getVisibleHeaderButtons(group)\"\n mat-icon-button\n [matTooltip]=\"getHeaderButtonTooltip(button)\"\n matTooltipPosition=\"above\"\n [style.color]=\"getHeaderButtonColor(button, group)\"\n [disabled]=\"isHeaderButtonDisabled(button, group)\"\n (click)=\"headerButtonClicked(button, group)\">\n <mat-icon>{{getHeaderButtonIcon(button)}}</mat-icon>\n </button>\n </div>\n </div>\n\n <hr class=\"group-divider\" />\n\n <!-- Empty state: show when no items and drag is disabled -->\n <div *ngIf=\"group.items.length === 0 && !config.groupConfig.dragEnabled\" class=\"empty-state\">\n <label>{{config.groupConfig.emptyGroupMessage ?? 'Empty'}}</label>\n </div>\n\n <!-- Changed: Replaced mat-chip-set with flex container of mat-stroked-button for cleaner button style -->\n <div\n *ngIf=\"group.items.length > 0 || config.groupConfig.dragEnabled\"\n cdkDropList\n [cdkDropListData]=\"group.items\"\n [cdkDropListDisabled]=\"!dragActive\"\n [cdkDropListEnterPredicate]=\"cdkNoCrossGroup\"\n (cdkDropListDropped)=\"onDrop($event)\"\n (cdkDropListEntered)=\"onDropListEntered($event)\"\n (cdkDropListExited)=\"onDropListExited($event)\"\n [class.drop-highlight]=\"group === highlightedGroup\"\n class=\"drop-list items-container\">\n\n <!-- Empty state inside drop list when drag is enabled -->\n <div *ngIf=\"group.items.length === 0\" class=\"empty-state drop-empty\">\n <label>{{config.groupConfig.emptyGroupMessage ?? 'Empty'}}</label>\n </div>\n\n <button\n mat-stroked-button\n *ngFor=\"let item of group.items\"\n cdkDrag [cdkDragData]=\"item\"\n [cdkDragDisabled]=\"!dragActive\"\n [matMenuTriggerFor]=\"config.groupConfig.contextMenuEnabled !== false ? itemMenu : null\"\n [matMenuTriggerData]=\"{item: item, group: group}\"\n class=\"item-button\">\n\n <!-- Added (grouped-orderable): the drag grip, rendered ONLY for a config that declared `orderable`. Same\n glyph, same sizing and the same quiet-until-hover treatment as the flat table's grip \u2014 this is the\n library's existing drag affordance, reused, not a second visual language. handle: true restricts the\n drag to the grip itself; two branches because cdkDragHandle is a directive presence, not a bindable. -->\n <ng-container *ngIf=\"orderableConfig\">\n <mat-icon *ngIf=\"orderableConfig.handle; else plainGrip\" cdkDragHandle class=\"tin-grip\" [class.tin-grip-disabled]=\"!orderEnabled\"\n [matTooltip]=\"orderDisabledReason ?? 'Drag to reorder'\" matTooltipPosition=\"above\">drag_indicator</mat-icon>\n <ng-template #plainGrip>\n <mat-icon class=\"tin-grip\" [class.tin-grip-disabled]=\"!orderEnabled\"\n [matTooltip]=\"orderDisabledReason ?? 'Drag to reorder'\" matTooltipPosition=\"above\">drag_indicator</mat-icon>\n </ng-template>\n </ng-container>\n\n <mat-icon\n *ngIf=\"getItemIcon(item)\"\n [style.color]=\"getItemIconColor(item)\"\n [matTooltip]=\"getItemIconTip(item)\"\n matTooltipPosition=\"above\"\n class=\"item-icon\">\n {{getItemIcon(item)}}\n </mat-icon>\n\n <!-- Changed: the item text moved into its own span so a runaway name can be capped and ellipsised without\n also squeezing the icons, which share this projection slot with it. The native title attribute keeps the\n full name recoverable on a truncated chip \u2014 same treatment already used on the group title above, and no\n tooltip directive, so touch behaviour is unchanged. -->\n <span class=\"item-label\" [title]=\"getItemText(item)\">{{getItemText(item)}}</span>\n\n <ng-container *ngFor=\"let additionalIcon of getVisibleAdditionalIcons(item)\">\n <mat-icon\n [style.color]=\"additionalIcon.color\"\n [matTooltip]=\"getIconTooltip(additionalIcon, item)\"\n matTooltipPosition=\"above\"\n class=\"item-additional-icon\">\n {{additionalIcon.name}}\n </mat-icon>\n </ng-container>\n\n </button>\n </div>\n\n </mat-card>\n\n</div>\n\n<mat-menu #itemMenu=\"matMenu\">\n <ng-template matMenuContent let-item=\"item\" let-group=\"group\">\n <!-- Changed: the click below hands itemActionClicked the Button object this *ngFor is already rendering rather than its name. displayedButtons can carry two buttons under one name (table-config.service unshifts the framework approvals buttons onto a table's own array), and the name lookup on the far side always returned the first \u2014 so a grouped item menu drew the app's button and ran the framework's, posting api/null/approve. Every other call on this element already passes the object; only the click did not. Inert where there is no collision: the rendered button IS the first name match there. -->\n <button\n *ngFor=\"let button of getVisibleButtons(item)\"\n mat-menu-item\n [disabled]=\"isButtonDisabled(button, item)\"\n (click)=\"itemActionClicked(button, item, group)\">\n <mat-icon [style.color]=\"getButtonIconColor(button, item)\">{{getButtonIcon(button)}}</mat-icon>\n <span>{{button.display ?? button.tip ?? (button.name | titlecase)}}</span>\n </button>\n </ng-template>\n</mat-menu>\n", styles: [".groups-filter{display:flex;justify-content:flex-end;margin-bottom:8px;padding-right:10px}.filter-field{width:300px;font-size:13px}.filter-field ::ng-deep .mat-mdc-form-field-infix{padding-top:8px!important;padding-bottom:8px!important;min-height:36px}.groups-container{display:flex;flex-direction:column;gap:20px;padding-right:10px;margin-left:0}.group-card{width:100%;margin-bottom:0;padding:8px 16px 16px}.group-header{display:flex;justify-content:space-between;align-items:center;padding:0;min-height:32px}.header-left{display:flex;align-items:center;gap:5px}.header-right{display:flex;align-items:center;gap:4px}.group-icon{margin-right:5px;font-size:24px;width:24px;height:24px;line-height:24px}.group-title{font-size:24px;font-weight:300;line-height:1.2}.group-count{font-size:12px;font-weight:300;margin-left:5px;line-height:1.2}.group-divider{margin-top:8px;margin-bottom:10px}.empty-state{display:flex;justify-content:center;align-items:center;padding:10px}.empty-state label{color:#757575;font-style:italic}.items-container{display:flex;flex-wrap:wrap;gap:10px;padding:8px 0;align-items:center}.item-button{cursor:pointer;font-weight:400;letter-spacing:.01em;transition:box-shadow .2s ease,background-color .2s ease}.item-button:hover{box-shadow:0 1px 4px #00000026;background-color:#00000005}.item-label{display:inline-block;max-width:260px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}.item-icon{font-size:18px;width:18px;height:18px;margin-right:4px;vertical-align:middle}.item-additional-icon{font-size:16px;width:16px;height:16px;margin-left:6px;vertical-align:middle;opacity:.85}button[mat-icon-button]{width:32px;height:32px}button[mat-icon-button] mat-icon{font-size:18px;margin-top:-3px}.header-right button[mat-icon-button]{display:inline-flex;align-items:center;justify-content:center;padding:0;line-height:normal}.header-right button[mat-icon-button] mat-icon{margin:0;font-size:20px;width:20px;height:20px;line-height:20px;display:block}.header-right button[mat-icon-button] .mat-mdc-button-touch-target{display:none}@media (max-width: 700px){.groups-container{gap:12px;padding-right:0}.group-card{padding:8px 10px 10px}.header-left{flex:1 1 auto;min-width:0;gap:4px}.group-icon{flex:0 0 auto;margin-right:2px;font-size:20px;width:20px;height:20px;line-height:20px}.group-title{font-size:16px;font-weight:400;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.group-count{flex:0 0 auto;margin-left:2px;font-size:11px}.header-right{flex:0 0 auto;gap:0}.header-right button[mat-icon-button] mat-icon{font-size:18px;width:18px;height:18px;line-height:18px}.group-header{min-height:28px}.group-divider{margin-top:6px;margin-bottom:6px}.items-container{gap:6px;padding:4px 0}.item-button{min-width:0;height:32px;padding:0 9px;font-size:12.5px;letter-spacing:normal;border-radius:16px;border-color:#0000001f;--mdc-outlined-button-container-height: 32px;--mdc-outlined-button-container-shape: 16px;--mdc-outlined-button-label-text-size: 12.5px}.item-button ::ng-deep .mdc-button__label{letter-spacing:normal;display:inline-flex;align-items:center}.item-button ::ng-deep .mat-mdc-button-touch-target{height:100%}.item-label{max-width:104px}.item-icon{font-size:14px;width:14px;height:14px;margin-right:3px}.item-additional-icon{font-size:14px;width:14px;height:14px;margin-left:3px}.drop-list{min-height:32px}.empty-state{padding:4px 10px}}.drop-list{min-height:40px}.drop-empty{padding:5px 10px}.cdk-drag-preview{box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;opacity:.9;border-radius:4px}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.drop-highlight{border:2px dashed #90caf9;border-radius:8px;background:#90caf90d}.cdk-drop-list-dragging .item-button:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}.tin-grip{font-size:18px;height:18px;width:18px;vertical-align:middle;cursor:grab;opacity:.28;margin-right:2px;transition:opacity .14s ease,color .14s ease}.item-button:hover .tin-grip{opacity:.7}.tin-grip:active{cursor:grabbing}.tin-grip-disabled,.item-button:hover .tin-grip-disabled{cursor:not-allowed;opacity:.22}\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: i2.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i4$2.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i4$2.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i4$2.MatMenuContent, selector: "ng-template[matMenuContent]" }, { kind: "directive", type: i4$2.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i2$1.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: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i7$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: i15.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i15.CdkDropListGroup, selector: "[cdkDropListGroup]", inputs: ["cdkDropListGroupDisabled"], exportAs: ["cdkDropListGroup"] }, { kind: "directive", type: i15.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i15.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "pipe", type: i1$2.TitleCasePipe, name: "titlecase" }] }); }
|
|
21622
21850
|
}
|
|
21623
21851
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: GroupsComponent, decorators: [{
|
|
21624
21852
|
type: Component,
|
|
21625
|
-
args: [{ selector: 'spa-groups', standalone: false, template: "<!-- Changed: this filter row was REMOVED from the grouped view's own markup and now lives in the table header,\n beside the action buttons, where every other table config puts it. It used to sit on a row of its own\n directly under the buttons row, leaving both rows half empty and visibly misaligned.\n The filtering logic here is untouched \u2014 only the input that drives it moved, and its text arrives via the\n filterText @Input relayed from the header. Kept as a fallback for a grouped view rendered WITHOUT the\n standard header (spa-groups can be used directly), so filtering is never simply lost. -->\n<div class=\"groups-filter\" *ngIf=\"showOwnFilter\">\n <mat-form-field appearance=\"outline\" class=\"filter-field\">\n <mat-icon matPrefix>search</mat-icon>\n <input matInput placeholder=\"Filter\" [(ngModel)]=\"filterText\" (ngModelChange)=\"applyFilter()\">\n <button *ngIf=\"filterText\" mat-icon-button matSuffix (click)=\"filterText = ''; applyFilter()\">\n <mat-icon>close</mat-icon>\n </button>\n </mat-form-field>\n</div>\n\n<div class=\"groups-container\" cdkDropListGroup>\n\n <mat-card *ngFor=\"let group of groupedData\" class=\"group-card\">\n\n <div class=\"group-header\">\n <div class=\"header-left\">\n <mat-icon *ngIf=\"group.icon\" [style.color]=\"group.color\" class=\"group-icon\">{{group.icon}}</mat-icon>\n <!-- Changed: added the native title attribute. On a phone the title now truncates with an ellipsis rather\n than wrapping to a second line, so the full name has to stay reachable somewhere \u2014 this is the\n zero-cost way to keep it. No tooltip directive, so nothing changes on touch. -->\n <label class=\"group-title\" [title]=\"group.displayName\">{{group.displayName}}</label>\n <label *ngIf=\"config.groupConfig.showGroupCount !== false\" class=\"group-count\">({{group.items.length}})</label>\n </div>\n\n <div class=\"header-right\">\n <button\n *ngFor=\"let button of getVisibleHeaderButtons(group)\"\n mat-icon-button\n [matTooltip]=\"getHeaderButtonTooltip(button)\"\n matTooltipPosition=\"above\"\n [style.color]=\"getHeaderButtonColor(button, group)\"\n [disabled]=\"isHeaderButtonDisabled(button, group)\"\n (click)=\"headerButtonClicked(button, group)\">\n <mat-icon>{{getHeaderButtonIcon(button)}}</mat-icon>\n </button>\n </div>\n </div>\n\n <hr class=\"group-divider\" />\n\n <!-- Empty state: show when no items and drag is disabled -->\n <div *ngIf=\"group.items.length === 0 && !config.groupConfig.dragEnabled\" class=\"empty-state\">\n <label>{{config.groupConfig.emptyGroupMessage ?? 'Empty'}}</label>\n </div>\n\n <!-- Changed: Replaced mat-chip-set with flex container of mat-stroked-button for cleaner button style -->\n <div\n *ngIf=\"group.items.length > 0 || config.groupConfig.dragEnabled\"\n cdkDropList\n [cdkDropListData]=\"group.items\"\n [cdkDropListDisabled]=\"!config.groupConfig.dragEnabled\"\n (cdkDropListDropped)=\"onDrop($event)\"\n (cdkDropListEntered)=\"onDropListEntered($event)\"\n (cdkDropListExited)=\"onDropListExited($event)\"\n [class.drop-highlight]=\"group === highlightedGroup\"\n class=\"drop-list items-container\">\n\n <!-- Empty state inside drop list when drag is enabled -->\n <div *ngIf=\"group.items.length === 0\" class=\"empty-state drop-empty\">\n <label>{{config.groupConfig.emptyGroupMessage ?? 'Empty'}}</label>\n </div>\n\n <button\n mat-stroked-button\n *ngFor=\"let item of group.items\"\n cdkDrag [cdkDragData]=\"item\"\n [cdkDragDisabled]=\"!config.groupConfig.dragEnabled\"\n [matMenuTriggerFor]=\"config.groupConfig.contextMenuEnabled !== false ? itemMenu : null\"\n [matMenuTriggerData]=\"{item: item, group: group}\"\n class=\"item-button\">\n\n <mat-icon\n *ngIf=\"getItemIcon(item)\"\n [style.color]=\"getItemIconColor(item)\"\n [matTooltip]=\"getItemIconTip(item)\"\n matTooltipPosition=\"above\"\n class=\"item-icon\">\n {{getItemIcon(item)}}\n </mat-icon>\n\n <!-- Changed: the item text moved into its own span so a runaway name can be capped and ellipsised without\n also squeezing the icons, which share this projection slot with it. The native title attribute keeps the\n full name recoverable on a truncated chip \u2014 same treatment already used on the group title above, and no\n tooltip directive, so touch behaviour is unchanged. -->\n <span class=\"item-label\" [title]=\"getItemText(item)\">{{getItemText(item)}}</span>\n\n <ng-container *ngFor=\"let additionalIcon of getVisibleAdditionalIcons(item)\">\n <mat-icon\n [style.color]=\"additionalIcon.color\"\n [matTooltip]=\"getIconTooltip(additionalIcon, item)\"\n matTooltipPosition=\"above\"\n class=\"item-additional-icon\">\n {{additionalIcon.name}}\n </mat-icon>\n </ng-container>\n\n </button>\n </div>\n\n </mat-card>\n\n</div>\n\n<mat-menu #itemMenu=\"matMenu\">\n <ng-template matMenuContent let-item=\"item\" let-group=\"group\">\n <!-- Changed: the click below hands itemActionClicked the Button object this *ngFor is already rendering rather than its name. displayedButtons can carry two buttons under one name (table-config.service unshifts the framework approvals buttons onto a table's own array), and the name lookup on the far side always returned the first \u2014 so a grouped item menu drew the app's button and ran the framework's, posting api/null/approve. Every other call on this element already passes the object; only the click did not. Inert where there is no collision: the rendered button IS the first name match there. -->\n <button\n *ngFor=\"let button of getVisibleButtons(item)\"\n mat-menu-item\n [disabled]=\"isButtonDisabled(button, item)\"\n (click)=\"itemActionClicked(button, item, group)\">\n <mat-icon [style.color]=\"getButtonIconColor(button, item)\">{{getButtonIcon(button)}}</mat-icon>\n <span>{{button.display ?? button.tip ?? (button.name | titlecase)}}</span>\n </button>\n </ng-template>\n</mat-menu>\n", styles: [".groups-filter{display:flex;justify-content:flex-end;margin-bottom:8px;padding-right:10px}.filter-field{width:300px;font-size:13px}.filter-field ::ng-deep .mat-mdc-form-field-infix{padding-top:8px!important;padding-bottom:8px!important;min-height:36px}.groups-container{display:flex;flex-direction:column;gap:20px;padding-right:10px;margin-left:0}.group-card{width:100%;margin-bottom:0;padding:8px 16px 16px}.group-header{display:flex;justify-content:space-between;align-items:center;padding:0;min-height:32px}.header-left{display:flex;align-items:center;gap:5px}.header-right{display:flex;align-items:center;gap:4px}.group-icon{margin-right:5px;font-size:24px;width:24px;height:24px;line-height:24px}.group-title{font-size:24px;font-weight:300;line-height:1.2}.group-count{font-size:12px;font-weight:300;margin-left:5px;line-height:1.2}.group-divider{margin-top:8px;margin-bottom:10px}.empty-state{display:flex;justify-content:center;align-items:center;padding:10px}.empty-state label{color:#757575;font-style:italic}.items-container{display:flex;flex-wrap:wrap;gap:10px;padding:8px 0;align-items:center}.item-button{cursor:pointer;font-weight:400;letter-spacing:.01em;transition:box-shadow .2s ease,background-color .2s ease}.item-button:hover{box-shadow:0 1px 4px #00000026;background-color:#00000005}.item-label{display:inline-block;max-width:260px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}.item-icon{font-size:18px;width:18px;height:18px;margin-right:4px;vertical-align:middle}.item-additional-icon{font-size:16px;width:16px;height:16px;margin-left:6px;vertical-align:middle;opacity:.85}button[mat-icon-button]{width:32px;height:32px}button[mat-icon-button] mat-icon{font-size:18px;margin-top:-3px}.header-right button[mat-icon-button]{display:inline-flex;align-items:center;justify-content:center;padding:0;line-height:normal}.header-right button[mat-icon-button] mat-icon{margin:0;font-size:20px;width:20px;height:20px;line-height:20px;display:block}.header-right button[mat-icon-button] .mat-mdc-button-touch-target{display:none}@media (max-width: 700px){.groups-container{gap:12px;padding-right:0}.group-card{padding:8px 10px 10px}.header-left{flex:1 1 auto;min-width:0;gap:4px}.group-icon{flex:0 0 auto;margin-right:2px;font-size:20px;width:20px;height:20px;line-height:20px}.group-title{font-size:16px;font-weight:400;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.group-count{flex:0 0 auto;margin-left:2px;font-size:11px}.header-right{flex:0 0 auto;gap:0}.header-right button[mat-icon-button] mat-icon{font-size:18px;width:18px;height:18px;line-height:18px}.group-header{min-height:28px}.group-divider{margin-top:6px;margin-bottom:6px}.items-container{gap:6px;padding:4px 0}.item-button{min-width:0;height:32px;padding:0 9px;font-size:12.5px;letter-spacing:normal;border-radius:16px;border-color:#0000001f;--mdc-outlined-button-container-height: 32px;--mdc-outlined-button-container-shape: 16px;--mdc-outlined-button-label-text-size: 12.5px}.item-button ::ng-deep .mdc-button__label{letter-spacing:normal;display:inline-flex;align-items:center}.item-button ::ng-deep .mat-mdc-button-touch-target{height:100%}.item-label{max-width:104px}.item-icon{font-size:14px;width:14px;height:14px;margin-right:3px}.item-additional-icon{font-size:14px;width:14px;height:14px;margin-left:3px}.drop-list{min-height:32px}.empty-state{padding:4px 10px}}.drop-list{min-height:40px}.drop-empty{padding:5px 10px}.cdk-drag-preview{box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;opacity:.9;border-radius:4px}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.drop-highlight{border:2px dashed #90caf9;border-radius:8px;background:#90caf90d}.cdk-drop-list-dragging .item-button:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}\n"] }]
|
|
21853
|
+
args: [{ selector: 'spa-groups', standalone: false, template: "<!-- Changed: this filter row was REMOVED from the grouped view's own markup and now lives in the table header,\n beside the action buttons, where every other table config puts it. It used to sit on a row of its own\n directly under the buttons row, leaving both rows half empty and visibly misaligned.\n The filtering logic here is untouched \u2014 only the input that drives it moved, and its text arrives via the\n filterText @Input relayed from the header. Kept as a fallback for a grouped view rendered WITHOUT the\n standard header (spa-groups can be used directly), so filtering is never simply lost. -->\n<div class=\"groups-filter\" *ngIf=\"showOwnFilter\">\n <mat-form-field appearance=\"outline\" class=\"filter-field\">\n <mat-icon matPrefix>search</mat-icon>\n <input matInput placeholder=\"Filter\" [(ngModel)]=\"filterText\" (ngModelChange)=\"applyFilter()\">\n <button *ngIf=\"filterText\" mat-icon-button matSuffix (click)=\"filterText = ''; applyFilter()\">\n <mat-icon>close</mat-icon>\n </button>\n </mat-form-field>\n</div>\n\n<div class=\"groups-container\" cdkDropListGroup>\n\n <mat-card *ngFor=\"let group of groupedData\" class=\"group-card\">\n\n <div class=\"group-header\">\n <div class=\"header-left\">\n <mat-icon *ngIf=\"group.icon\" [style.color]=\"group.color\" class=\"group-icon\">{{group.icon}}</mat-icon>\n <!-- Changed: added the native title attribute. On a phone the title now truncates with an ellipsis rather\n than wrapping to a second line, so the full name has to stay reachable somewhere \u2014 this is the\n zero-cost way to keep it. No tooltip directive, so nothing changes on touch. -->\n <label class=\"group-title\" [title]=\"group.displayName\">{{group.displayName}}</label>\n <label *ngIf=\"config.groupConfig.showGroupCount !== false\" class=\"group-count\">({{group.items.length}})</label>\n </div>\n\n <div class=\"header-right\">\n <button\n *ngFor=\"let button of getVisibleHeaderButtons(group)\"\n mat-icon-button\n [matTooltip]=\"getHeaderButtonTooltip(button)\"\n matTooltipPosition=\"above\"\n [style.color]=\"getHeaderButtonColor(button, group)\"\n [disabled]=\"isHeaderButtonDisabled(button, group)\"\n (click)=\"headerButtonClicked(button, group)\">\n <mat-icon>{{getHeaderButtonIcon(button)}}</mat-icon>\n </button>\n </div>\n </div>\n\n <hr class=\"group-divider\" />\n\n <!-- Empty state: show when no items and drag is disabled -->\n <div *ngIf=\"group.items.length === 0 && !config.groupConfig.dragEnabled\" class=\"empty-state\">\n <label>{{config.groupConfig.emptyGroupMessage ?? 'Empty'}}</label>\n </div>\n\n <!-- Changed: Replaced mat-chip-set with flex container of mat-stroked-button for cleaner button style -->\n <div\n *ngIf=\"group.items.length > 0 || config.groupConfig.dragEnabled\"\n cdkDropList\n [cdkDropListData]=\"group.items\"\n [cdkDropListDisabled]=\"!dragActive\"\n [cdkDropListEnterPredicate]=\"cdkNoCrossGroup\"\n (cdkDropListDropped)=\"onDrop($event)\"\n (cdkDropListEntered)=\"onDropListEntered($event)\"\n (cdkDropListExited)=\"onDropListExited($event)\"\n [class.drop-highlight]=\"group === highlightedGroup\"\n class=\"drop-list items-container\">\n\n <!-- Empty state inside drop list when drag is enabled -->\n <div *ngIf=\"group.items.length === 0\" class=\"empty-state drop-empty\">\n <label>{{config.groupConfig.emptyGroupMessage ?? 'Empty'}}</label>\n </div>\n\n <button\n mat-stroked-button\n *ngFor=\"let item of group.items\"\n cdkDrag [cdkDragData]=\"item\"\n [cdkDragDisabled]=\"!dragActive\"\n [matMenuTriggerFor]=\"config.groupConfig.contextMenuEnabled !== false ? itemMenu : null\"\n [matMenuTriggerData]=\"{item: item, group: group}\"\n class=\"item-button\">\n\n <!-- Added (grouped-orderable): the drag grip, rendered ONLY for a config that declared `orderable`. Same\n glyph, same sizing and the same quiet-until-hover treatment as the flat table's grip \u2014 this is the\n library's existing drag affordance, reused, not a second visual language. handle: true restricts the\n drag to the grip itself; two branches because cdkDragHandle is a directive presence, not a bindable. -->\n <ng-container *ngIf=\"orderableConfig\">\n <mat-icon *ngIf=\"orderableConfig.handle; else plainGrip\" cdkDragHandle class=\"tin-grip\" [class.tin-grip-disabled]=\"!orderEnabled\"\n [matTooltip]=\"orderDisabledReason ?? 'Drag to reorder'\" matTooltipPosition=\"above\">drag_indicator</mat-icon>\n <ng-template #plainGrip>\n <mat-icon class=\"tin-grip\" [class.tin-grip-disabled]=\"!orderEnabled\"\n [matTooltip]=\"orderDisabledReason ?? 'Drag to reorder'\" matTooltipPosition=\"above\">drag_indicator</mat-icon>\n </ng-template>\n </ng-container>\n\n <mat-icon\n *ngIf=\"getItemIcon(item)\"\n [style.color]=\"getItemIconColor(item)\"\n [matTooltip]=\"getItemIconTip(item)\"\n matTooltipPosition=\"above\"\n class=\"item-icon\">\n {{getItemIcon(item)}}\n </mat-icon>\n\n <!-- Changed: the item text moved into its own span so a runaway name can be capped and ellipsised without\n also squeezing the icons, which share this projection slot with it. The native title attribute keeps the\n full name recoverable on a truncated chip \u2014 same treatment already used on the group title above, and no\n tooltip directive, so touch behaviour is unchanged. -->\n <span class=\"item-label\" [title]=\"getItemText(item)\">{{getItemText(item)}}</span>\n\n <ng-container *ngFor=\"let additionalIcon of getVisibleAdditionalIcons(item)\">\n <mat-icon\n [style.color]=\"additionalIcon.color\"\n [matTooltip]=\"getIconTooltip(additionalIcon, item)\"\n matTooltipPosition=\"above\"\n class=\"item-additional-icon\">\n {{additionalIcon.name}}\n </mat-icon>\n </ng-container>\n\n </button>\n </div>\n\n </mat-card>\n\n</div>\n\n<mat-menu #itemMenu=\"matMenu\">\n <ng-template matMenuContent let-item=\"item\" let-group=\"group\">\n <!-- Changed: the click below hands itemActionClicked the Button object this *ngFor is already rendering rather than its name. displayedButtons can carry two buttons under one name (table-config.service unshifts the framework approvals buttons onto a table's own array), and the name lookup on the far side always returned the first \u2014 so a grouped item menu drew the app's button and ran the framework's, posting api/null/approve. Every other call on this element already passes the object; only the click did not. Inert where there is no collision: the rendered button IS the first name match there. -->\n <button\n *ngFor=\"let button of getVisibleButtons(item)\"\n mat-menu-item\n [disabled]=\"isButtonDisabled(button, item)\"\n (click)=\"itemActionClicked(button, item, group)\">\n <mat-icon [style.color]=\"getButtonIconColor(button, item)\">{{getButtonIcon(button)}}</mat-icon>\n <span>{{button.display ?? button.tip ?? (button.name | titlecase)}}</span>\n </button>\n </ng-template>\n</mat-menu>\n", styles: [".groups-filter{display:flex;justify-content:flex-end;margin-bottom:8px;padding-right:10px}.filter-field{width:300px;font-size:13px}.filter-field ::ng-deep .mat-mdc-form-field-infix{padding-top:8px!important;padding-bottom:8px!important;min-height:36px}.groups-container{display:flex;flex-direction:column;gap:20px;padding-right:10px;margin-left:0}.group-card{width:100%;margin-bottom:0;padding:8px 16px 16px}.group-header{display:flex;justify-content:space-between;align-items:center;padding:0;min-height:32px}.header-left{display:flex;align-items:center;gap:5px}.header-right{display:flex;align-items:center;gap:4px}.group-icon{margin-right:5px;font-size:24px;width:24px;height:24px;line-height:24px}.group-title{font-size:24px;font-weight:300;line-height:1.2}.group-count{font-size:12px;font-weight:300;margin-left:5px;line-height:1.2}.group-divider{margin-top:8px;margin-bottom:10px}.empty-state{display:flex;justify-content:center;align-items:center;padding:10px}.empty-state label{color:#757575;font-style:italic}.items-container{display:flex;flex-wrap:wrap;gap:10px;padding:8px 0;align-items:center}.item-button{cursor:pointer;font-weight:400;letter-spacing:.01em;transition:box-shadow .2s ease,background-color .2s ease}.item-button:hover{box-shadow:0 1px 4px #00000026;background-color:#00000005}.item-label{display:inline-block;max-width:260px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}.item-icon{font-size:18px;width:18px;height:18px;margin-right:4px;vertical-align:middle}.item-additional-icon{font-size:16px;width:16px;height:16px;margin-left:6px;vertical-align:middle;opacity:.85}button[mat-icon-button]{width:32px;height:32px}button[mat-icon-button] mat-icon{font-size:18px;margin-top:-3px}.header-right button[mat-icon-button]{display:inline-flex;align-items:center;justify-content:center;padding:0;line-height:normal}.header-right button[mat-icon-button] mat-icon{margin:0;font-size:20px;width:20px;height:20px;line-height:20px;display:block}.header-right button[mat-icon-button] .mat-mdc-button-touch-target{display:none}@media (max-width: 700px){.groups-container{gap:12px;padding-right:0}.group-card{padding:8px 10px 10px}.header-left{flex:1 1 auto;min-width:0;gap:4px}.group-icon{flex:0 0 auto;margin-right:2px;font-size:20px;width:20px;height:20px;line-height:20px}.group-title{font-size:16px;font-weight:400;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.group-count{flex:0 0 auto;margin-left:2px;font-size:11px}.header-right{flex:0 0 auto;gap:0}.header-right button[mat-icon-button] mat-icon{font-size:18px;width:18px;height:18px;line-height:18px}.group-header{min-height:28px}.group-divider{margin-top:6px;margin-bottom:6px}.items-container{gap:6px;padding:4px 0}.item-button{min-width:0;height:32px;padding:0 9px;font-size:12.5px;letter-spacing:normal;border-radius:16px;border-color:#0000001f;--mdc-outlined-button-container-height: 32px;--mdc-outlined-button-container-shape: 16px;--mdc-outlined-button-label-text-size: 12.5px}.item-button ::ng-deep .mdc-button__label{letter-spacing:normal;display:inline-flex;align-items:center}.item-button ::ng-deep .mat-mdc-button-touch-target{height:100%}.item-label{max-width:104px}.item-icon{font-size:14px;width:14px;height:14px;margin-right:3px}.item-additional-icon{font-size:14px;width:14px;height:14px;margin-left:3px}.drop-list{min-height:32px}.empty-state{padding:4px 10px}}.drop-list{min-height:40px}.drop-empty{padding:5px 10px}.cdk-drag-preview{box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;opacity:.9;border-radius:4px}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.drop-highlight{border:2px dashed #90caf9;border-radius:8px;background:#90caf90d}.cdk-drop-list-dragging .item-button:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}.tin-grip{font-size:18px;height:18px;width:18px;vertical-align:middle;cursor:grab;opacity:.28;margin-right:2px;transition:opacity .14s ease,color .14s ease}.item-button:hover .tin-grip{opacity:.7}.tin-grip:active{cursor:grabbing}.tin-grip-disabled,.item-button:hover .tin-grip-disabled{cursor:not-allowed;opacity:.22}\n"] }]
|
|
21626
21854
|
}], ctorParameters: () => [{ type: ConditionService }, { type: ButtonService }, { type: DataServiceLib }, { type: i1$4.MatSnackBar }, { type: ApiErrorService }], propDecorators: { config: [{
|
|
21627
21855
|
type: Input
|
|
21628
21856
|
}], dataSource: [{
|
|
@@ -21780,6 +22008,13 @@ class TableComponent {
|
|
|
21780
22008
|
// allocates a new object on every change-detection tick, which is the exact shape of the livelock that
|
|
21781
22009
|
// template-bound allocating getters are banned for in this library.
|
|
21782
22010
|
this.orderableConfig = null;
|
|
22011
|
+
// Changed (grouped-orderable): the reorder rules moved to the shared RowOrderService so the GROUPED view posts
|
|
22012
|
+
// the identical request instead of growing a second copy of the contract. Constructed here rather than injected
|
|
22013
|
+
// ON PURPOSE — eleven spec files construct TableComponent with positional arguments, and adding a constructor
|
|
22014
|
+
// parameter would churn all of them for no behavioural gain. The service has only these two collaborators.
|
|
22015
|
+
// Built LAZILY: the constructor body subscribes to the breakpoint observer, which can call setupOrderable
|
|
22016
|
+
// synchronously, so a field initializer here would not reliably have run yet.
|
|
22017
|
+
this.rowOrderService = null;
|
|
21783
22018
|
// mat-table row-def predicate. Deliberately keyed on the CONFIG, not on row data, so it is constant for a
|
|
21784
22019
|
// given table: rows are draggable-capable or they are not. It gates whether cdkDrag is ATTACHED at all,
|
|
21785
22020
|
// which is a different question from orderEnabled (whether a drag may currently HAPPEN) — a filtered
|
|
@@ -23486,9 +23721,11 @@ class TableComponent {
|
|
|
23486
23721
|
return null;
|
|
23487
23722
|
return Object.keys(row).find(k => k.endsWith('ID') && k !== 'TenantID') || null;
|
|
23488
23723
|
}
|
|
23724
|
+
get rowOrder() {
|
|
23725
|
+
return this.rowOrderService ??= new RowOrderService(this.dataService, this.apiErrorService);
|
|
23726
|
+
}
|
|
23489
23727
|
setupOrderable() {
|
|
23490
|
-
|
|
23491
|
-
this.orderableConfig = raw ? (raw === true ? {} : raw) : null; // `false`/absent → null → zero affordances
|
|
23728
|
+
this.orderableConfig = this.rowOrder.normalize(this.config?.orderable); // `false`/absent → null → zero affordances
|
|
23492
23729
|
// The grip gets its own leading column, added HERE rather than inside TableConfigService.setColumns so that
|
|
23493
23730
|
// a table which never asked to be orderable cannot acquire a column through the shared column builder.
|
|
23494
23731
|
if (this.orderableConfig && this.displayedColumns && !this.displayedColumns.includes('orderHandle')) {
|
|
@@ -23535,28 +23772,21 @@ class TableComponent {
|
|
|
23535
23772
|
// The row's identity for the posted id list: an explicit idField, then the table's heroField, then the
|
|
23536
23773
|
// ...ID convention findIdKey already encodes for trackBy. Reused rather than re-derived.
|
|
23537
23774
|
rowOrderId(row) {
|
|
23538
|
-
|
|
23539
|
-
|
|
23775
|
+
// Changed: same precedence as before (idField, heroField, cached trackBy key, ...ID convention) — the ordering
|
|
23776
|
+
// is now expressed as the service's fallback list so the grouped view resolves ids by the identical rule.
|
|
23777
|
+
return this.rowOrder.resolveId(row, this.orderableConfig, [this.config?.heroField, this.rowIdKey, this.findIdKey(row)]);
|
|
23540
23778
|
}
|
|
23541
23779
|
// Defaults to the FIRST path segment of the load URL + '/reorder', which is the controller route the base
|
|
23542
23780
|
// [HttpPost("reorder")] action hangs off ('product-groups?action=list' → 'product-groups/reorder'). An app
|
|
23543
23781
|
// whose reorder endpoint is not that sets orderable.action explicitly rather than fighting this.
|
|
23544
23782
|
reorderAction() {
|
|
23545
|
-
|
|
23546
|
-
return this.orderableConfig.action;
|
|
23547
|
-
const entity = (this.config?.loadAction?.url || '').split('?')[0].split('/')[0];
|
|
23548
|
-
return { url: `${entity}/reorder`, method: 'post' };
|
|
23783
|
+
return this.rowOrder.action(this.orderableConfig, this.config?.loadAction); // Changed: shared with the grouped view
|
|
23549
23784
|
}
|
|
23550
23785
|
// The scope this drag happened inside. An explicit scopeValue wins, because the scope is often a chip the
|
|
23551
23786
|
// user picked and is not on the rows at all; scopeField reads it off a row; absent both, the sequence is
|
|
23552
23787
|
// flat across the tenant and null is the correct thing to send.
|
|
23553
23788
|
orderScope(rows) {
|
|
23554
|
-
|
|
23555
|
-
if (!cfg)
|
|
23556
|
-
return null;
|
|
23557
|
-
if (cfg.scopeValue !== undefined)
|
|
23558
|
-
return cfg.scopeValue;
|
|
23559
|
-
return cfg.scopeField ? (rows?.[0]?.[cfg.scopeField] ?? null) : null;
|
|
23789
|
+
return this.rowOrder.scope(this.orderableConfig, rows); // Changed: shared. No fallback scope here — a flat table with neither scopeValue nor scopeField is a tenant-wide sequence and null is the correct thing to send
|
|
23560
23790
|
}
|
|
23561
23791
|
// Optimistic move, POST, revert on refusal — the shape proven at groups.component.ts:166-196.
|
|
23562
23792
|
onRowDrop(event) {
|
|
@@ -23576,27 +23806,21 @@ class TableComponent {
|
|
|
23576
23806
|
this.tableDataSource.data = [...rows];
|
|
23577
23807
|
};
|
|
23578
23808
|
const ids = rows.map(r => this.rowOrderId(r)).filter(id => id !== null && id !== undefined);
|
|
23579
|
-
|
|
23809
|
+
// Changed: the short-list refusal is unchanged in effect, but it now also catches a DUPLICATE id list and
|
|
23810
|
+
// SAYS SO. A row with no resolvable id posts a short list, and two rows resolving the same id post a
|
|
23811
|
+
// same-length list the server's set comparison refuses at HTTP 200 — both used to revert in total silence,
|
|
23812
|
+
// which is indistinguishable from a genuine conflict. It is a misconfigured idField every time.
|
|
23813
|
+
const refusal = this.rowOrder.validateIds(ids, rows.length);
|
|
23814
|
+
if (refusal) {
|
|
23580
23815
|
revert();
|
|
23816
|
+
this.rowOrder.presentLocalRefusal(refusal, this.reorderAction().url);
|
|
23581
23817
|
return;
|
|
23582
|
-
}
|
|
23583
|
-
const action = this.reorderAction();
|
|
23818
|
+
}
|
|
23584
23819
|
// The payload is the whole ordered scope and no order numbers — position IS the order (ReorderRequest.cs).
|
|
23585
|
-
this.
|
|
23586
|
-
|
|
23587
|
-
|
|
23588
|
-
|
|
23589
|
-
revert();
|
|
23590
|
-
// A conflict means somebody else changed this scope, so the rows on screen are stale and reverting
|
|
23591
|
-
// alone would leave the user looking at a list the server disagrees with. Refuse AND refresh.
|
|
23592
|
-
if (response?.reason === 'conflict' && this.config?.loadAction)
|
|
23593
|
-
this.loadData(this.config.loadAction, '');
|
|
23594
|
-
this.apiErrorService.presentAppFailure(response, 'action', action.url); // never raw server text on a user surface
|
|
23595
|
-
},
|
|
23596
|
-
error: (err) => {
|
|
23597
|
-
revert();
|
|
23598
|
-
this.apiErrorService.present(err); // transport failure — classified and deduped, never a raw error string
|
|
23599
|
-
}
|
|
23820
|
+
this.rowOrder.persist(this.orderableConfig, this.config?.loadAction, ids, this.orderScope(rows), {
|
|
23821
|
+
revert,
|
|
23822
|
+
refresh: () => { if (this.config?.loadAction)
|
|
23823
|
+
this.loadData(this.config.loadAction, ''); }
|
|
23600
23824
|
});
|
|
23601
23825
|
}
|
|
23602
23826
|
// Changed: Clean up SignalR subscriptions on component destroy
|
|
@@ -23620,7 +23844,7 @@ class TableComponent {
|
|
|
23620
23844
|
this.setPaginator();
|
|
23621
23845
|
}
|
|
23622
23846
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableComponent, deps: [{ token: DataServiceLib }, { token: MessageService }, { token: i1$6.BreakpointObserver }, { token: i1$3.MatDialog }, { token: ButtonService }, { token: DialogService }, { token: TableConfigService }, { token: ConditionService }, { token: AuthService }, { token: SignalRService }, { token: OfflineService }, { token: ApiErrorService }, { token: TIN_SPA_RUNTIME_CONFIG, optional: true }, { token: LogService, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
23623
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TableComponent, isStandalone: false, selector: "spa-table", inputs: { data: "data", tileData: "tileData", config: "config", localMode: "localMode", parentDetails: "parentDetails", reload: "reload", activeTab: "activeTab", inTab: "inTab", nestingLevel: "nestingLevel" }, outputs: { dataLoad: "dataLoad", totalChange: "totalChange", actionSuccess: "actionSuccess", refreshClick: "refreshClick", searchClick: "searchClick", createClick: "createClick", actionClick: "actionClick", inputChange: "inputChange", actionResponse: "actionResponse" }, viewQueries: [{ propertyName: "tablePaginator", first: true, predicate: ["tablePaginator"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "\n<ng-container *ngIf=\"hasFormAccess && !sectionHidden\"> <!-- Changed: sectionConfig.hideWhenEmpty hides the whole table -->\n\n <!-- Added: collapsible flat section header (sectionConfig) \u2014 1px border, no elevation, whole row toggles -->\n <div class=\"tbl-section-header tin-disc-row\" *ngIf=\"config.sectionConfig\" (click)=\"toggleSection()\" [attr.aria-expanded]=\"!sectionCollapsed\" [class.tbl-section-static]=\"config.sectionConfig.collapsible === false\" role=\"button\" tabindex=\"0\" (keydown.enter)=\"toggleSection()\">\n <!-- Changed (2026-08-17): the expand affordance moves to the LEFT, where the eye starts. It used to be a\n single small chevron at the far right edge, and a shut section therefore read as flat text with nothing\n inviting anyone to open it. Same marker, same place, on every folding band in the library \u2014 see\n tin-styles.css \".tin-disc\". A section that cannot collapse shows no marker, exactly as it showed no\n chevron before. -->\n <span class=\"tin-disc\" *ngIf=\"config.sectionConfig.collapsible !== false\" [class.open]=\"!sectionCollapsed\"><mat-icon>chevron_right</mat-icon></span>\n <mat-icon class=\"tbl-section-icon\" *ngIf=\"config.sectionConfig.icon\">{{ config.sectionConfig.icon }}</mat-icon>\n <span class=\"tbl-section-title\">{{ config.sectionConfig.title }}</span>\n <span class=\"tbl-section-count\" *ngIf=\"config.sectionConfig.showCount !== false\">{{ dataSource?.length || 0 }}</span>\n <span class=\"tbl-section-chip\" *ngFor=\"let chip of sectionChips()\" [style.color]=\"chip.color\">{{ chip.text }}</span>\n <span class=\"tbl-section-spacer\"></span>\n <!-- Changed: the label is wrapped so a NARROW screen can drop it and leave an icon-only button. Only a\n button that HAS an icon loses its text \u2014 otherwise it would collapse to a blank square. -->\n <button mat-stroked-button color=\"primary\" *ngFor=\"let btn of sectionButtons()\" (click)=\"sectionButtonClicked(btn, $event)\" [matTooltip]=\"btn.display || btn.name\"><mat-icon *ngIf=\"btn.icon?.name\">{{ btn.icon.name }}</mat-icon><span class=\"tbl-section-btn-text\" [class.has-icon]=\"!!btn.icon?.name\">{{ btn.display || btn.name }}</span></button>\n<!-- Changed (2026-08-17): the trailing chevron is gone \u2014 it is now the leading .tin-disc marker above. Two\n chevrons saying the same thing at both ends of the row is noise, and the right-hand one was the one\n nobody looked at. -->\n </div>\n\n <!-- Added: the section's \"why this list exists\" line. It sits UNDER the title (a reason only makes sense once\n the list has been named) and only while the section is open \u2014 a shut section already has its own summary\n line, and a paragraph over the top of that is noise. -->\n <p class=\"tbl-section-caption\" *ngIf=\"config.sectionConfig?.caption && !sectionCollapsed\">{{ config.sectionConfig.caption }}</p>\n\n <!-- Added (delivered-order-lines-ui-gate): the TABLE's own caption, same element and same class as the section\n caption above, for a table that has no section header to hang one on. It carries the reason a list is\n read-only where the buttons that are now hidden used to be \u2014 hiding them silently leaves the user with no\n guidance at all. Deliberately a quiet 12.5px line, NOT a bordered/iconed notice bar: the unconditional\n read-only annotation bar was removed from every table dialog on 2026-08-19 for being noise, and this must\n not read as its return. -->\n <p class=\"tbl-section-caption\" *ngIf=\"captionText && !sectionCollapsed\">{{ captionText }}</p>\n\n <!-- Added: collapsed section affordance \u2014 mirrors the Day Book \"Show the N\" pattern -->\n <div class=\"tbl-section-more\" *ngIf=\"config.sectionConfig && sectionCollapsed && (dataSource?.length || 0) > 0\">\n <button type=\"button\" class=\"tbl-section-link\" (click)=\"toggleSection()\">Show the {{ dataSource.length }}</button>\n </div>\n\n <ng-container *ngIf=\"!config.sectionConfig || !sectionCollapsed\"> <!-- Added: section collapse hides the table body -->\n\n <!-- Search -->\n <spa-search\n *ngIf=\"config.searchConfig\" [config]=\"config.searchConfig\" [smallScreen]=\"smallScreen\" [tableDataSource]=\"tableDataSource\" style=\"margin-bottom: 20px;\" (searchClick)=\"searchClicked($event)\">\n </spa-search>\n\n <!-- Header -->\n <!-- Changed: [isRealTime] binds the RESOLVED value, not the raw config. It was `config.realTime`, so the live\n indicator only ever appeared on a table that set the flag ITSELF \u2014 a table relying on the app-wide\n `tableDefaults.realTime` was genuinely subscribed to SignalR (setupRealTimeSubscriptions and\n realTimeRefreshOrFallback both resolve through effRealTime) and simply never showed the dot. That is\n how \"Trips and Loads have no real time\" got reported: a status light disagreeing with the system it\n reports on. effRealTime also gets the inverse right \u2014 realTime:true under a global false still lights,\n and realTime:false under a global true stays dark. -->\n <app-table-header\n [config]=\"config\" [data]=\"dataSource\" [tableDataSource]=\"tableDataSource\" [tileConfig]=\"config.tileConfig\" [tileData]=\"tileData\" [tileReload]=\"tileReload\" [lastSearch]=\"lastSearch\" [smallScreen]=\"smallScreen\"\n [showFilterButton]=\"showFilterButton\" [isRealTime]=\"effRealTime\" [isConnected]=\"isSignalRConnected\" [refreshing]=\"loadingStage === 'refresh'\"\n (createClick)=\"newModel()\" (customClick)=\"customModel($event,null)\"\n (refreshClick)=\"refreshClicked()\" (tileClick)=\"tileClicked($event)\" (tileUnClick)=\"tileUnClicked($event)\" (chipClick)=\"chipClicked($event)\" (filterChange)=\"filterChanged($event)\">\n </app-table-header>\n\n <!-- Added (Quiet Loading D4): refresh with data already on screen \u2014 a 2px line flush under the header and the\n spinning refresh icon are the ONLY signals. Rows stay live, clickable and un-dimmed while they swap. -->\n <div *ngIf=\"loadingStage === 'refresh'\" class=\"tin-load-line\" aria-hidden=\"true\">\n <span class=\"tin-load-line-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n\n <!-- Added: paged-mode filter hint \u2014 the client filter only covers rows loaded so far.\n Changed: the condition moved into filterScopePartial so this banner and the empty-state message below\n read the SAME predicate. Stated independently they drifted: this banner said \"only the 20 loaded rows\n of 240\" while the message underneath offered \"all 240 rows\". This banner owns SCOPE (what was searched)\n and the remedy; the message below owns the RESULT; the paginator owns the COUNT. One fact each. -->\n <div *ngIf=\"filterScopePartial\" class=\"paged-filter-hint\">\n <mat-icon>info</mat-icon>\n <span>Filtering only the {{loadedRows.length}} loaded rows of {{serverTotal}}. {{ config.searchConfig ? 'Use Search for complete results.' : 'Refine with search for complete results.' }}</span>\n </div>\n\n\n <!-- Table -->\n <div *ngIf=\"!config.viewType || config?.viewType === 'table'\">\n\n <p *ngIf=\"!config\"><em>Configure Table</em></p>\n <p *ngIf=\"!dataSource && !loadingStage && !loadFailed\"><em>Loading...</em></p> <!-- Changed (Quiet Loading): the bare text is replaced by the stage below while a quiet load is on screen --><!-- Changed: ...and a FAILED load is not a running one \u2014 without the loadFailed guard this line sat there forever once the error dialog was dismissed -->\n\n <!-- Added (Quiet Loading D3): first load, nothing on screen yet. The progress module is the hero (eased bar +\n counting percentage + caption) and the ghost rows hold the exact space the real rows will fill, so the\n table does not jump when data lands. Only ever rendered when quiet loading is on. -->\n <div *ngIf=\"loadingStage === 'initial'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div class=\"tin-load-ghosts\">\n <div class=\"tin-load-ghost-row\" *ngFor=\"let r of ghostRows\">\n <div class=\"tin-load-ghost-cell\" *ngFor=\"let c of ghostColumns; let i = index\">\n <span class=\"tin-skel\" [style.width.%]=\"ghostWidth(i, c)\" [style.animation-delay.ms]=\"r * 120\"></span> <!-- staggered sweep: each row starts 120ms after the one above -->\n </div>\n </div>\n </div>\n\n </div>\n\n <div *ngIf=\"dataSource && loadingStage !== 'initial' && (!smallScreen || (smallScreen && dataSource?.length > 0))\" [class.tin-load-in]=\"effQuietLoading\"> <!-- Changed (Quiet Loading): the empty header-only table is suppressed while the initial stage stands in for it, and the real rows fade in where the ghosts were (D3) -->\n\n <!-- Changed: the row region becomes a CDK drop list ONLY when config.orderable is declared. cdkDropListDisabled\n is bound to the same single reason string the grip's tooltip reads, so a greyed grip and a dead drop\n list can never disagree. A table without `orderable` has orderableConfig === null: no drop list, no\n drag directives, no handle column \u2014 nothing at all. -->\n <table mat-table [dataSource]=\"tableDataSource\" [trackBy]=\"trackByRow\" [ngClass]=\"elevation\" [class.tin-no-col-headers]=\"config.hideColumnHeaders\"\n cdkDropList [cdkDropListDisabled]=\"!orderEnabled\" (cdkDropListDropped)=\"onRowDrop($event)\" [class.tin-orderable]=\"!!orderableConfig\"> <!-- Changed: optional column-header suppression -->\n\n <!-- Added: the drag grip. Its column is pushed onto displayedColumns by setupOrderable(), so it exists\n only for an orderable table. -->\n <ng-container matColumnDef=\"orderHandle\">\n <th mat-header-cell *matHeaderCellDef class=\"tin-grip-cell\"></th>\n <td mat-cell *matCellDef=\"let row\" class=\"tin-grip-cell\">\n <!-- handle: true restricts dragging to the grip itself (cdkDragHandle); absent/false leaves the whole\n row as the drag target, which is the documented default. Two branches because cdkDragHandle is a\n structural directive presence, not a bindable boolean. -->\n <mat-icon *ngIf=\"orderableConfig?.handle; else plainGrip\" cdkDragHandle class=\"tin-grip\" [class.tin-grip-disabled]=\"!orderEnabled\"\n [matTooltip]=\"orderDisabledReason ?? 'Drag to reorder'\" matTooltipPosition=\"above\">drag_indicator</mat-icon>\n <ng-template #plainGrip>\n <mat-icon class=\"tin-grip\" [class.tin-grip-disabled]=\"!orderEnabled\"\n [matTooltip]=\"orderDisabledReason ?? 'Drag to reorder'\" matTooltipPosition=\"above\">drag_indicator</mat-icon>\n </ng-template>\n </td>\n </ng-container>\n\n\n <ng-container *ngFor=\"let column of config.columns\" [matColumnDef]=\"column.name\">\n <th mat-header-cell *matHeaderCellDef >{{ column.alias ?? column.name | camelToWords }}</th>\n <td mat-cell *matCellDef=\"let row;\" class=\"right-padding\" >\n\n <!-- Added: inline edit \u2014 editable cells swap to their form-field editor while the row is in edit mode -->\n <app-inline-cell *ngIf=\"isRowEditing(row) && getInlineField(column); else displayCell\" [field]=\"getInlineField(column)\" [data]=\"editingModel\"></app-inline-cell>\n\n <!-- Rows -->\n <ng-template #displayCell>\n <app-table-row [column]=\"column\" [row]=\"row\" [config]=\"config\" [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked(column.name, row)\" (columnClick)=\"columnClicked($event.column, $event.row)\" (showBannerEvent)=\"showBanner($event)\"> <!-- Changed: take the column and row the CELL emitted instead of re-reading the template's own. Identical for every existing type \u2014 the cell emits the very objects it was passed \u2014 but a 'chips' cell emits the CLICKED ITEM's pseudo-row, and the old binding silently discarded it and opened the row instead. That is the exact failure this codebase has already been bitten by: a drill-in that renders the record you clicked FROM. spa-cards (:230) has always bound it this way. -->\n </app-table-row>\n </ng-template>\n\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"action\">\n <th mat-header-cell *matHeaderCellDef> Action </th>\n <td mat-cell *matCellDef=\"let row\" [ngStyle]=\"{width:false ? '20px' : actionsWidth}\">\n <div class=\"action-buttons-container\">\n\n <!-- Added: inline edit \u2014 while a row edits in place, its actions collapse to submit/cancel -->\n <ng-container *ngIf=\"isRowEditing(row); else rowActions\">\n <button mat-icon-button matTooltip=\"Save\" matTooltipPosition=\"above\" (click)=\"submitInlineEdit()\"><mat-icon class=\"inline-save\">check</mat-icon></button> <!-- Changed: dropped color=\"primary\" \u2014 the icon now carries a green save cue -->\n <button mat-icon-button matTooltip=\"Cancel\" matTooltipPosition=\"above\" (click)=\"cancelInlineEdit()\"><mat-icon class=\"inline-cancel\">close</mat-icon></button> <!-- Changed: red cancel cue -->\n </ng-container>\n\n <!-- Actions -->\n <ng-template #rowActions>\n <app-table-action\n [displayedButtons]=\"displayedButtons\" [config]=\"config\" [smallScreen]=\"smallScreen\" [row]=\"row\" (actionClick)=\"actionClicked($event.name, $event.row, undefined, $event.button)\"> <!-- Changed: pass the Button the row rendered. actionClicked has always taken this optional 4th argument (the group-header binding at :245 already supplies it); the row binding did not, so a name collision between a table's own button and an injected framework one resolved to the framework's \u2014 shift's Trips row ran the approvals 'approve'/'cancel' and posted api/null/approve. `undefined` for group keeps every group-only branch inside actionClicked switched off exactly as before. -->\n </app-table-action>\n </ng-template>\n\n </div>\n </td>\n </ng-container>\n\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <!-- Changed: TWO row definitions, selected by the `when` predicate, so `cdkDrag` is attached ONLY to an\n orderable table's rows. A single row def with [cdkDragDisabled] was measured to still instantiate a\n CDK DragRef \u2014 and stamp .cdk-drag \u2014 on EVERY row of EVERY grid in the estate, which is a real cost\n and makes \"a non-orderable table renders zero drag affordances\" untrue at the DOM level. This is the\n standard mat-table multi-rowDef idiom; the predicate reads config, not row data, so it is constant\n per table. The draggable def MUST come first \u2014 mat-table takes the first def whose `when` matches,\n and the default (no `when`) is the fallback. -->\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns; when: rowsAreOrderable\" [ngClass]=\"{'make-gray': (config.greyOut && config.greyOut(row)) || row.pendingApproval, 'row-editing': isRowEditing(row)}\"\n cdkDrag [cdkDragData]=\"row\" [cdkDragDisabled]=\"!orderEnabled\" cdkDragPreviewContainer=\"parent\" cdkDragLockAxis=\"y\"> <!-- cdkDragPreviewContainer=\"parent\" keeps the cloned <tr>'s real column widths; a body-level preview collapses to zero-width cells -->\n <div class=\"tin-drag-placeholder\" *cdkDragPlaceholder></div>\n </tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\" [ngClass]=\"{'make-gray': (config.greyOut && config.greyOut(row)) || row.pendingApproval, 'row-editing': isRowEditing(row)}\"></tr> <!-- Changed: row-editing flags the row that is open for inline edit -->\n </table>\n\n </div>\n\n <!-- Changed: Removed *ngIf condition to keep paginator always in DOM and maintain ViewChild reference -->\n <!-- Changed: Added CSS class binding to hide when no data instead of conditional rendering -->\n <!-- Changed: Legacy paginator only renders in non-paged mode (pagedMode is constant per instance, set before first render) -->\n <!-- Changed: hidePageSize on a phone. The \"Items per page\" label and its select cost a whole row of a\n narrow screen for a control almost nobody touches there \u2014 the default is what gets used. The range\n (\"1 \u2013 10 of 54\") and the arrows stay, which is the part that is actually navigated. Bound to\n smallScreen, the component's existing breakpoint (max-width 600px, live via BreakpointObserver), so\n the paginator agrees with how this table already decides what \"mobile\" means rather than\n introducing a third breakpoint. -->\n <mat-paginator *ngIf=\"!pagedMode\"\n #tablePaginator\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [hidePageSize]=\"smallScreen\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial' || singlePagePaginatorHidden}\"\n showFirstLastButtons>\n </mat-paginator>\n\n <!-- Added: manual paginator for server-side paged mode \u2014 fully state-bound, never attached to MatTableDataSource. Always visible: when filtering it pages the in-memory filtered subset (length = filtered count); otherwise the server window (length = true total). No first/last jump (would force fetching the whole gap). -->\n <mat-paginator *ngIf=\"pagedMode\"\n [length]=\"filterActive ? filteredRows.length : serverTotal + overlayDelta\"\n [pageIndex]=\"pageIndex\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [hidePageSize]=\"smallScreen\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial' || singlePagePaginatorHidden}\"\n (page)=\"onServerPage($event)\">\n </mat-paginator>\n\n </div>\n \n <!-- Added (Quiet Loading): the initial-load stage for the NON-table views.\n Without this, card/capsule/grouped showed literally nothing during a first load \u2014 the view component\n renders an empty dataSource and the \"No Data\" line is suppressed while the stage owns the space.\n The progress header is identical to the table's so the two feel like one feature; only the ghost\n furniture differs, because column-shaped rows are wrong in a card grid. -->\n <div *ngIf=\"loadingStage === 'initial' && config?.viewType && config?.viewType !== 'table'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div *ngIf=\"config?.viewType === 'capsule'\" class=\"tin-load-ghost-capsules\" aria-hidden=\"true\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let c of ghostCapsules\"></span>\n </div>\n\n <div *ngIf=\"config?.viewType === 'card'\" class=\"tin-load-ghost-cards\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-card\" *ngFor=\"let c of ghostCards\">\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-title\"></span>\n <span class=\"tin-skel tin-skel-text\"></span>\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-short\"></span>\n </div>\n </div>\n\n <div *ngIf=\"config?.viewType === 'grouped'\" class=\"tin-load-ghost-groups\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-group\" *ngFor=\"let g of ghostGroups\">\n <span class=\"tin-skel tin-load-ghost-group-head\"></span>\n <!-- Pills, not rows: a group card's body is a wrap of chips, so full-width bars promised a table\n and the stage did not resemble what replaced it. -->\n <div class=\"tin-load-ghost-group-items\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let r of ghostCards\"></span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Capsules -->\n <spa-capsules *ngIf=\"config?.viewType === 'capsule' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n (actionClick)=\"actionClicked($event.name, $event.row, undefined, $event.button)\"> <!-- Changed: pass the Button spa-capsules rendered into actionClicked's long-existing optional 4th argument, exactly as the row binding at :131 and the group-header binding at :245 do. Without it a colliding table in capsule view still resolved by name and ran the framework's approve/cancel. \"undefined\" for group is load-bearing \u2014 it keeps every group-only branch inside actionClicked switched off exactly as before. -->\n </spa-capsules>\n\n\n <!-- Cards -->\n <!-- Changed: the (actionClick) binding below passes the Button spa-cards rendered into actionClicked's long-existing optional 4th argument, exactly as the row binding at :131 and the group-header binding at :245 do. Without it a colliding table in card view resolved by name and ran the framework's approve/cancel. \"undefined\" for group is load-bearing \u2014 it keeps every group-only branch inside actionClicked switched off exactly as before. -->\n <spa-cards *ngIf=\"config?.viewType === 'card' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked($event.name, $event.row, undefined, $event.button)\"\n (columnClick)=\"columnClicked($event.column, $event.row)\"\n (showBannerEvent)=\"showBanner($event)\">\n </spa-cards>\n\n <!-- Groups - Added: New grouped view type -->\n <!-- Changed: the grouped view no longer renders its own filter field. It used to sit on a row of its own\n beneath the buttons row, which left both rows half empty and, more importantly, was a SECOND filter with\n no refresh button. The standard header filter (which has refresh, like every other table) now drives it,\n with its text relayed in through filterText. -->\n <spa-groups *ngIf=\"config?.viewType === 'grouped' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [showOwnFilter]=\"false\"\n [filterText]=\"groupFilterText\"\n (actionClick)=\"actionClicked($event.name, $event.row, $event.group, $event.button)\">\n </spa-groups>\n\n\n <div class=\"tin-center\">\n <p *ngIf=\"dataSource?.length == 0 && loadingStage !== 'initial' && !loadFailed\"><em>{{config.noDataMessage ?? 'No Data'}}</em></p> <!-- Changed (Quiet Loading): the stage owns the space until it completes, then hands straight over to this message \u2014 no \"No Data\" flashing underneath the ghost rows --><!-- Changed: ...and NOT when the load failed. A failed read leaves dataSource as an empty array, so this line confidently reported \"No Data\" \u2014 an assertion about the data \u2014 when the truth was that we never got any. That state now has its own sentence below. -->\n\n <!-- Added: the load-failed state. \"There is nothing here\" and \"we could not find out\" are different facts and\n this grid stated the first for both. Gated on having no rows, so a failed REFRESH leaves the rows already\n on screen alone \u2014 the error dialog covers that case and an empty state under live rows would be a lie. -->\n <div *ngIf=\"!(dataSource?.length > 0) && !loadingStage && loadFailed\" class=\"tin-filter-empty\" role=\"status\">\n <p class=\"tin-filter-empty-title\"><em>Couldn\u2019t load {{stageEntityName}}</em></p>\n <p class=\"tin-filter-empty-hint\">Check your connection, then use the refresh button to try again.</p>\n </div>\n\n <!-- Added: the OTHER empty state. The line above means \"there is nothing here yet\"; this one means \"nothing\n matches what you typed\", and until now it rendered nothing at all \u2014 a header, a \"0 of 0\" paginator and\n no reason. filteredToNothing is false whenever dataSource is empty, so the two can never both show and\n the line above is reached on exactly the rows it always was. role=\"status\" announces the change to a\n screen reader, which otherwise gets silence when the rows disappear. -->\n <!-- Changed: ONE message became TWO, because \"no matches\" means two different things and only one of them\n was ever true. In paged mode the client filter runs over the loaded WINDOW and issues no fetch, so with\n a partial window \"No matches for X\" overstates what was searched and \"see all 240 rows\" implies the\n filter covered 240 when it covered 20 \u2014 while the banner at the top of the grid was simultaneously\n saying so. The split is driven by filterScopePartial, the same getter that banner binds to, so the two\n cannot contradict each other. Neither variant restates a COUNT the paginator or the banner already owns. -->\n\n <!-- COMPLETE scope: the filter saw every row there is (any unpaged grid, or a paged one holding the full\n set \u2014 search/POST mode and the no-total degrade both land here). The total is honest, so it is offered. -->\n <div *ngIf=\"filteredToNothing && !filterScopePartial\" class=\"tin-filter-empty\" role=\"status\">\n <p class=\"tin-filter-empty-title\"><em>{{filterEmptyTitle}}</em></p>\n <p class=\"tin-filter-empty-hint\">Clear the filter to see all {{filteredTotal}} {{filteredRowNoun}}.</p>\n </div>\n\n <!-- PARTIAL scope: the filter saw only the rows loaded so far. The headline says exactly that, and the hint\n stops at the way back \u2014 the banner above already carries both the counts and the \"use search\" remedy,\n so repeating either here would be the contradiction this split exists to remove. -->\n <div *ngIf=\"filteredToNothing && filterScopePartial\" class=\"tin-filter-empty\" role=\"status\">\n <p class=\"tin-filter-empty-title\"><em>{{filterEmptyTitlePartial}}</em></p>\n <p class=\"tin-filter-empty-hint\">Clear the filter to keep browsing.</p>\n </div>\n </div>\n\n </ng-container> <!-- Added: end section-collapse wrapper -->\n\n</ng-container>\n\n\n<!-- Changed: \"Access Restricted\" named nothing and offered no next step, and as a bare centred italic <p> it read\n as page furniture. A refusal is the one member of this file's empty-state family (:259 no rows, :264 couldn't\n load, :283/:291 filtered to nothing) that was never given a title, a hint and role=\"status\". It is also the\n only one that is NOT about data: it is decided at table.component.ts:65 from the config and the role, before\n any request exists, which is why it can name the permission with certainty \u2014 and why it can never collide\n with the \"no rows\" line, which lives inside the *ngIf=\"hasFormAccess\" container opened at :1.\n The capability DISPLAY is what a user repeats to an administrator; the raw capN key stays in the log line. -->\n<ng-container *ngIf=\"!hasFormAccess\">\n <div class=\"tin-center\">\n <div class=\"tin-filter-empty\" role=\"status\">\n <!-- Changed (Phase 4): both sentences now come from Core via the component, not from literals here. The\n route tier (the no-access page) renders the same two strings from the same source, so the grid and the\n page cannot drift apart. The empty-allow case still lands on the generic hint \u2014 Core.noAccessHint\n returns it whenever there is no capability display to name. -->\n <!-- Changed (Phase 4): tin-no-access-title added ALONGSIDE the shared empty-state class, which is kept so\n the visual styling is untouched. The extra class exists because tin-filter-empty-title is shared with\n \"Couldn't load\" (:265) and both filtered-to-nothing states (:284, :292) \u2014 so it cannot tell a REFUSAL\n from an ordinary empty grid, and a test selecting on it would pass on the wrong state. It is the same\n class the route-tier no-access page uses, so one selector reads a refusal at either tier and reads\n nothing else. -->\n <p class=\"tin-filter-empty-title tin-no-access-title\"><em>{{noAccessTitle}}</em></p>\n <p class=\"tin-filter-empty-hint tin-no-access-hint\">{{noAccessHint}}</p>\n </div>\n </div>\n</ng-container>\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0;padding-top:8px;padding-bottom:16px}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}.dlg-header-classic{padding-left:24px;padding-right:24px}.dlg-title-classic{font-size:20px;font-weight:500;margin-top:10px;margin-bottom:5px}.dialog-header-titles{display:flex;flex-direction:column;justify-content:center;min-width:0}.tin-dlg-head .dialog-header-titles label{margin:0}.tbl-section-header{display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:8px;border:1px solid rgba(0,0,0,.08);border-radius:10px;background:transparent;cursor:pointer;transition:border-color .15s}.tbl-section-header:hover{border-color:#90a4ae}.tbl-section-header.tbl-section-static{cursor:default}.tbl-section-icon{color:#546e7a}.tbl-section-title{font-size:14px;font-weight:600;color:#000000d1}.tbl-section-count{background:#e3f2fd;color:#1565c0;border-radius:12px;padding:2px 10px;font-size:12px}.tbl-section-chip{font-size:12px;font-weight:500;color:#0009}.tbl-section-spacer{margin-left:auto}.tbl-section-chevron{color:#90a4ae}.tbl-section-caption{margin:-4px 12px 10px;font-size:12.5px;line-height:1.45;color:#0000008c;max-width:82ch}.tbl-section-more{padding:0 12px 8px}.tbl-section-link{background:none;border:none;color:#1565c0;cursor:pointer;font-size:13px;padding:0}.tbl-section-link:hover{text-decoration:underline}.tbl-section-icon,.tbl-section-chevron,.tbl-section-count{flex:0 0 auto}.tbl-section-title{flex:1 1 auto;min-width:0}@media (max-width: 700px){.tbl-section-header{gap:8px;padding:10px}.tbl-section-chip,.tbl-section-btn-text.has-icon{display:none}.tbl-section-header button{min-width:0;padding:0 10px}}table.tin-no-col-headers tr.mat-mdc-header-row{display:none}.tin-filter-empty{text-align:center;padding:24px 12px}.tin-filter-empty-title{margin:0}.tin-filter-empty-hint{margin:6px 0 0;font-size:.85em;opacity:.7;overflow-wrap:anywhere}.tin-grip-cell{width:34px;padding-right:0!important;text-align:center}.tin-grip{font-size:18px;height:18px;width:18px;vertical-align:middle;cursor:grab;opacity:.28;transition:opacity .14s ease,color .14s ease}tr.mat-mdc-row:hover .tin-grip{opacity:.7}.tin-grip:active{cursor:grabbing}.tin-grip-disabled,tr.mat-mdc-row:hover .tin-grip-disabled{cursor:not-allowed;opacity:.22}::ng-deep tr.cdk-drag-preview{box-shadow:0 6px 18px #00000038,0 1px 3px #00000024;background:var(--mdc-theme-surface, #fff);opacity:.96;border-radius:4px}::ng-deep tr.cdk-drag-placeholder{background:transparent}::ng-deep .tin-drag-placeholder{height:44px;border:1px dashed rgba(0,0,0,.28);border-radius:4px;background:#00000008;box-sizing:border-box}::ng-deep tr.cdk-drag-animating{transition:transform .22s cubic-bezier(0,0,.2,1)}::ng-deep table.tin-orderable.cdk-drop-list-dragging tr.mat-mdc-row:not(.cdk-drag-placeholder){transition:transform .22s cubic-bezier(0,0,.2,1)}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i15.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i15.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i15.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i15.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i15.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i15.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i15.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i15.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i15.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i15.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i16.MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "component", type: i2$1.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: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: i20.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i20.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i20.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: i20.CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "component", type: SearchComponent, selector: "spa-search", inputs: ["config", "smallScreen", "tableDataSource"], outputs: ["searchClick"] }, { kind: "component", type: TableHeaderComponent, selector: "app-table-header", inputs: ["lastSearch", "config", "hideTitle", "tableDataSource", "tileConfig", "smallScreen", "tileReload", "showFilterButton", "data", "tileData", "isRealTime", "isConnected", "refreshing"], outputs: ["createClick", "customClick", "refreshClick", "tileClick", "tileUnClick", "chipClick", "filterChange"] }, { kind: "component", type: TableRowComponent, selector: "app-table-row", inputs: ["column", "row", "config", "smallScreen"], outputs: ["actionClick", "columnClick", "showBannerEvent"] }, { kind: "component", type: TableActionComponent, selector: "app-table-action", inputs: ["displayedButtons", "config", "row", "smallScreen"], outputs: ["actionClick"] }, { kind: "component", type: InlineCellComponent, selector: "app-inline-cell", inputs: ["field", "data"], outputs: ["valueChange"] }, { kind: "component", type: CapsulesComponent, selector: "spa-capsules", inputs: ["config", "dataSource", "displayedButtons"], outputs: ["actionClick"] }, { kind: "component", type: CardsComponent, selector: "spa-cards", inputs: ["config", "dataSource", "displayedButtons", "smallScreen"], outputs: ["actionClick", "columnClick", "showBannerEvent"] }, { kind: "component", type: GroupsComponent, selector: "spa-groups", inputs: ["config", "dataSource", "displayedButtons", "showOwnFilter", "filterText"], outputs: ["actionClick"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
|
|
23847
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TableComponent, isStandalone: false, selector: "spa-table", inputs: { data: "data", tileData: "tileData", config: "config", localMode: "localMode", parentDetails: "parentDetails", reload: "reload", activeTab: "activeTab", inTab: "inTab", nestingLevel: "nestingLevel" }, outputs: { dataLoad: "dataLoad", totalChange: "totalChange", actionSuccess: "actionSuccess", refreshClick: "refreshClick", searchClick: "searchClick", createClick: "createClick", actionClick: "actionClick", inputChange: "inputChange", actionResponse: "actionResponse" }, viewQueries: [{ propertyName: "tablePaginator", first: true, predicate: ["tablePaginator"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "\n<ng-container *ngIf=\"hasFormAccess && !sectionHidden\"> <!-- Changed: sectionConfig.hideWhenEmpty hides the whole table -->\n\n <!-- Added: collapsible flat section header (sectionConfig) \u2014 1px border, no elevation, whole row toggles -->\n <div class=\"tbl-section-header tin-disc-row\" *ngIf=\"config.sectionConfig\" (click)=\"toggleSection()\" [attr.aria-expanded]=\"!sectionCollapsed\" [class.tbl-section-static]=\"config.sectionConfig.collapsible === false\" role=\"button\" tabindex=\"0\" (keydown.enter)=\"toggleSection()\">\n <!-- Changed (2026-08-17): the expand affordance moves to the LEFT, where the eye starts. It used to be a\n single small chevron at the far right edge, and a shut section therefore read as flat text with nothing\n inviting anyone to open it. Same marker, same place, on every folding band in the library \u2014 see\n tin-styles.css \".tin-disc\". A section that cannot collapse shows no marker, exactly as it showed no\n chevron before. -->\n <span class=\"tin-disc\" *ngIf=\"config.sectionConfig.collapsible !== false\" [class.open]=\"!sectionCollapsed\"><mat-icon>chevron_right</mat-icon></span>\n <mat-icon class=\"tbl-section-icon\" *ngIf=\"config.sectionConfig.icon\">{{ config.sectionConfig.icon }}</mat-icon>\n <span class=\"tbl-section-title\">{{ config.sectionConfig.title }}</span>\n <span class=\"tbl-section-count\" *ngIf=\"config.sectionConfig.showCount !== false\">{{ dataSource?.length || 0 }}</span>\n <span class=\"tbl-section-chip\" *ngFor=\"let chip of sectionChips()\" [style.color]=\"chip.color\">{{ chip.text }}</span>\n <span class=\"tbl-section-spacer\"></span>\n <!-- Changed: the label is wrapped so a NARROW screen can drop it and leave an icon-only button. Only a\n button that HAS an icon loses its text \u2014 otherwise it would collapse to a blank square. -->\n <button mat-stroked-button color=\"primary\" *ngFor=\"let btn of sectionButtons()\" (click)=\"sectionButtonClicked(btn, $event)\" [matTooltip]=\"btn.display || btn.name\"><mat-icon *ngIf=\"btn.icon?.name\">{{ btn.icon.name }}</mat-icon><span class=\"tbl-section-btn-text\" [class.has-icon]=\"!!btn.icon?.name\">{{ btn.display || btn.name }}</span></button>\n<!-- Changed (2026-08-17): the trailing chevron is gone \u2014 it is now the leading .tin-disc marker above. Two\n chevrons saying the same thing at both ends of the row is noise, and the right-hand one was the one\n nobody looked at. -->\n </div>\n\n <!-- Added: the section's \"why this list exists\" line. It sits UNDER the title (a reason only makes sense once\n the list has been named) and only while the section is open \u2014 a shut section already has its own summary\n line, and a paragraph over the top of that is noise. -->\n <p class=\"tbl-section-caption\" *ngIf=\"config.sectionConfig?.caption && !sectionCollapsed\">{{ config.sectionConfig.caption }}</p>\n\n <!-- Added (delivered-order-lines-ui-gate): the TABLE's own caption, same element and same class as the section\n caption above, for a table that has no section header to hang one on. It carries the reason a list is\n read-only where the buttons that are now hidden used to be \u2014 hiding them silently leaves the user with no\n guidance at all. Deliberately a quiet 12.5px line, NOT a bordered/iconed notice bar: the unconditional\n read-only annotation bar was removed from every table dialog on 2026-08-19 for being noise, and this must\n not read as its return. -->\n <p class=\"tbl-section-caption\" *ngIf=\"captionText && !sectionCollapsed\">{{ captionText }}</p>\n\n <!-- Added: collapsed section affordance \u2014 mirrors the Day Book \"Show the N\" pattern -->\n <div class=\"tbl-section-more\" *ngIf=\"config.sectionConfig && sectionCollapsed && (dataSource?.length || 0) > 0\">\n <button type=\"button\" class=\"tbl-section-link\" (click)=\"toggleSection()\">Show the {{ dataSource.length }}</button>\n </div>\n\n <ng-container *ngIf=\"!config.sectionConfig || !sectionCollapsed\"> <!-- Added: section collapse hides the table body -->\n\n <!-- Search -->\n <spa-search\n *ngIf=\"config.searchConfig\" [config]=\"config.searchConfig\" [smallScreen]=\"smallScreen\" [tableDataSource]=\"tableDataSource\" style=\"margin-bottom: 20px;\" (searchClick)=\"searchClicked($event)\">\n </spa-search>\n\n <!-- Header -->\n <!-- Changed: [isRealTime] binds the RESOLVED value, not the raw config. It was `config.realTime`, so the live\n indicator only ever appeared on a table that set the flag ITSELF \u2014 a table relying on the app-wide\n `tableDefaults.realTime` was genuinely subscribed to SignalR (setupRealTimeSubscriptions and\n realTimeRefreshOrFallback both resolve through effRealTime) and simply never showed the dot. That is\n how \"Trips and Loads have no real time\" got reported: a status light disagreeing with the system it\n reports on. effRealTime also gets the inverse right \u2014 realTime:true under a global false still lights,\n and realTime:false under a global true stays dark. -->\n <app-table-header\n [config]=\"config\" [data]=\"dataSource\" [tableDataSource]=\"tableDataSource\" [tileConfig]=\"config.tileConfig\" [tileData]=\"tileData\" [tileReload]=\"tileReload\" [lastSearch]=\"lastSearch\" [smallScreen]=\"smallScreen\"\n [showFilterButton]=\"showFilterButton\" [isRealTime]=\"effRealTime\" [isConnected]=\"isSignalRConnected\" [refreshing]=\"loadingStage === 'refresh'\"\n (createClick)=\"newModel()\" (customClick)=\"customModel($event,null)\"\n (refreshClick)=\"refreshClicked()\" (tileClick)=\"tileClicked($event)\" (tileUnClick)=\"tileUnClicked($event)\" (chipClick)=\"chipClicked($event)\" (filterChange)=\"filterChanged($event)\">\n </app-table-header>\n\n <!-- Added (Quiet Loading D4): refresh with data already on screen \u2014 a 2px line flush under the header and the\n spinning refresh icon are the ONLY signals. Rows stay live, clickable and un-dimmed while they swap. -->\n <div *ngIf=\"loadingStage === 'refresh'\" class=\"tin-load-line\" aria-hidden=\"true\">\n <span class=\"tin-load-line-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n\n <!-- Added: paged-mode filter hint \u2014 the client filter only covers rows loaded so far.\n Changed: the condition moved into filterScopePartial so this banner and the empty-state message below\n read the SAME predicate. Stated independently they drifted: this banner said \"only the 20 loaded rows\n of 240\" while the message underneath offered \"all 240 rows\". This banner owns SCOPE (what was searched)\n and the remedy; the message below owns the RESULT; the paginator owns the COUNT. One fact each. -->\n <div *ngIf=\"filterScopePartial\" class=\"paged-filter-hint\">\n <mat-icon>info</mat-icon>\n <span>Filtering only the {{loadedRows.length}} loaded rows of {{serverTotal}}. {{ config.searchConfig ? 'Use Search for complete results.' : 'Refine with search for complete results.' }}</span>\n </div>\n\n\n <!-- Table -->\n <div *ngIf=\"!config.viewType || config?.viewType === 'table'\">\n\n <p *ngIf=\"!config\"><em>Configure Table</em></p>\n <p *ngIf=\"!dataSource && !loadingStage && !loadFailed\"><em>Loading...</em></p> <!-- Changed (Quiet Loading): the bare text is replaced by the stage below while a quiet load is on screen --><!-- Changed: ...and a FAILED load is not a running one \u2014 without the loadFailed guard this line sat there forever once the error dialog was dismissed -->\n\n <!-- Added (Quiet Loading D3): first load, nothing on screen yet. The progress module is the hero (eased bar +\n counting percentage + caption) and the ghost rows hold the exact space the real rows will fill, so the\n table does not jump when data lands. Only ever rendered when quiet loading is on. -->\n <div *ngIf=\"loadingStage === 'initial'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div class=\"tin-load-ghosts\">\n <div class=\"tin-load-ghost-row\" *ngFor=\"let r of ghostRows\">\n <div class=\"tin-load-ghost-cell\" *ngFor=\"let c of ghostColumns; let i = index\">\n <span class=\"tin-skel\" [style.width.%]=\"ghostWidth(i, c)\" [style.animation-delay.ms]=\"r * 120\"></span> <!-- staggered sweep: each row starts 120ms after the one above -->\n </div>\n </div>\n </div>\n\n </div>\n\n <div *ngIf=\"dataSource && loadingStage !== 'initial' && (!smallScreen || (smallScreen && dataSource?.length > 0))\" [class.tin-load-in]=\"effQuietLoading\"> <!-- Changed (Quiet Loading): the empty header-only table is suppressed while the initial stage stands in for it, and the real rows fade in where the ghosts were (D3) -->\n\n <!-- Changed: the row region becomes a CDK drop list ONLY when config.orderable is declared. cdkDropListDisabled\n is bound to the same single reason string the grip's tooltip reads, so a greyed grip and a dead drop\n list can never disagree. A table without `orderable` has orderableConfig === null: no drop list, no\n drag directives, no handle column \u2014 nothing at all. -->\n <table mat-table [dataSource]=\"tableDataSource\" [trackBy]=\"trackByRow\" [ngClass]=\"elevation\" [class.tin-no-col-headers]=\"config.hideColumnHeaders\"\n cdkDropList [cdkDropListDisabled]=\"!orderEnabled\" (cdkDropListDropped)=\"onRowDrop($event)\" [class.tin-orderable]=\"!!orderableConfig\"> <!-- Changed: optional column-header suppression -->\n\n <!-- Added: the drag grip. Its column is pushed onto displayedColumns by setupOrderable(), so it exists\n only for an orderable table. -->\n <ng-container matColumnDef=\"orderHandle\">\n <th mat-header-cell *matHeaderCellDef class=\"tin-grip-cell\"></th>\n <td mat-cell *matCellDef=\"let row\" class=\"tin-grip-cell\">\n <!-- handle: true restricts dragging to the grip itself (cdkDragHandle); absent/false leaves the whole\n row as the drag target, which is the documented default. Two branches because cdkDragHandle is a\n structural directive presence, not a bindable boolean. -->\n <mat-icon *ngIf=\"orderableConfig?.handle; else plainGrip\" cdkDragHandle class=\"tin-grip\" [class.tin-grip-disabled]=\"!orderEnabled\"\n [matTooltip]=\"orderDisabledReason ?? 'Drag to reorder'\" matTooltipPosition=\"above\">drag_indicator</mat-icon>\n <ng-template #plainGrip>\n <mat-icon class=\"tin-grip\" [class.tin-grip-disabled]=\"!orderEnabled\"\n [matTooltip]=\"orderDisabledReason ?? 'Drag to reorder'\" matTooltipPosition=\"above\">drag_indicator</mat-icon>\n </ng-template>\n </td>\n </ng-container>\n\n\n <ng-container *ngFor=\"let column of config.columns\" [matColumnDef]=\"column.name\">\n <th mat-header-cell *matHeaderCellDef >{{ column.alias ?? column.name | camelToWords }}</th>\n <td mat-cell *matCellDef=\"let row;\" class=\"right-padding\" >\n\n <!-- Added: inline edit \u2014 editable cells swap to their form-field editor while the row is in edit mode -->\n <app-inline-cell *ngIf=\"isRowEditing(row) && getInlineField(column); else displayCell\" [field]=\"getInlineField(column)\" [data]=\"editingModel\"></app-inline-cell>\n\n <!-- Rows -->\n <ng-template #displayCell>\n <app-table-row [column]=\"column\" [row]=\"row\" [config]=\"config\" [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked(column.name, row)\" (columnClick)=\"columnClicked($event.column, $event.row)\" (showBannerEvent)=\"showBanner($event)\"> <!-- Changed: take the column and row the CELL emitted instead of re-reading the template's own. Identical for every existing type \u2014 the cell emits the very objects it was passed \u2014 but a 'chips' cell emits the CLICKED ITEM's pseudo-row, and the old binding silently discarded it and opened the row instead. That is the exact failure this codebase has already been bitten by: a drill-in that renders the record you clicked FROM. spa-cards (:230) has always bound it this way. -->\n </app-table-row>\n </ng-template>\n\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"action\">\n <th mat-header-cell *matHeaderCellDef> Action </th>\n <td mat-cell *matCellDef=\"let row\" [ngStyle]=\"{width:false ? '20px' : actionsWidth}\">\n <div class=\"action-buttons-container\">\n\n <!-- Added: inline edit \u2014 while a row edits in place, its actions collapse to submit/cancel -->\n <ng-container *ngIf=\"isRowEditing(row); else rowActions\">\n <button mat-icon-button matTooltip=\"Save\" matTooltipPosition=\"above\" (click)=\"submitInlineEdit()\"><mat-icon class=\"inline-save\">check</mat-icon></button> <!-- Changed: dropped color=\"primary\" \u2014 the icon now carries a green save cue -->\n <button mat-icon-button matTooltip=\"Cancel\" matTooltipPosition=\"above\" (click)=\"cancelInlineEdit()\"><mat-icon class=\"inline-cancel\">close</mat-icon></button> <!-- Changed: red cancel cue -->\n </ng-container>\n\n <!-- Actions -->\n <ng-template #rowActions>\n <app-table-action\n [displayedButtons]=\"displayedButtons\" [config]=\"config\" [smallScreen]=\"smallScreen\" [row]=\"row\" (actionClick)=\"actionClicked($event.name, $event.row, undefined, $event.button)\"> <!-- Changed: pass the Button the row rendered. actionClicked has always taken this optional 4th argument (the group-header binding at :245 already supplies it); the row binding did not, so a name collision between a table's own button and an injected framework one resolved to the framework's \u2014 shift's Trips row ran the approvals 'approve'/'cancel' and posted api/null/approve. `undefined` for group keeps every group-only branch inside actionClicked switched off exactly as before. -->\n </app-table-action>\n </ng-template>\n\n </div>\n </td>\n </ng-container>\n\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <!-- Changed: TWO row definitions, selected by the `when` predicate, so `cdkDrag` is attached ONLY to an\n orderable table's rows. A single row def with [cdkDragDisabled] was measured to still instantiate a\n CDK DragRef \u2014 and stamp .cdk-drag \u2014 on EVERY row of EVERY grid in the estate, which is a real cost\n and makes \"a non-orderable table renders zero drag affordances\" untrue at the DOM level. This is the\n standard mat-table multi-rowDef idiom; the predicate reads config, not row data, so it is constant\n per table. The draggable def MUST come first \u2014 mat-table takes the first def whose `when` matches,\n and the default (no `when`) is the fallback. -->\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns; when: rowsAreOrderable\" [ngClass]=\"{'make-gray': (config.greyOut && config.greyOut(row)) || row.pendingApproval, 'row-editing': isRowEditing(row)}\"\n cdkDrag [cdkDragData]=\"row\" [cdkDragDisabled]=\"!orderEnabled\" cdkDragPreviewContainer=\"parent\" cdkDragLockAxis=\"y\"> <!-- cdkDragPreviewContainer=\"parent\" keeps the cloned <tr>'s real column widths; a body-level preview collapses to zero-width cells -->\n <div class=\"tin-drag-placeholder\" *cdkDragPlaceholder></div>\n </tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\" [ngClass]=\"{'make-gray': (config.greyOut && config.greyOut(row)) || row.pendingApproval, 'row-editing': isRowEditing(row)}\"></tr> <!-- Changed: row-editing flags the row that is open for inline edit -->\n </table>\n\n </div>\n\n <!-- Changed: Removed *ngIf condition to keep paginator always in DOM and maintain ViewChild reference -->\n <!-- Changed: Added CSS class binding to hide when no data instead of conditional rendering -->\n <!-- Changed: Legacy paginator only renders in non-paged mode (pagedMode is constant per instance, set before first render) -->\n <!-- Changed: hidePageSize on a phone. The \"Items per page\" label and its select cost a whole row of a\n narrow screen for a control almost nobody touches there \u2014 the default is what gets used. The range\n (\"1 \u2013 10 of 54\") and the arrows stay, which is the part that is actually navigated. Bound to\n smallScreen, the component's existing breakpoint (max-width 600px, live via BreakpointObserver), so\n the paginator agrees with how this table already decides what \"mobile\" means rather than\n introducing a third breakpoint. -->\n <mat-paginator *ngIf=\"!pagedMode\"\n #tablePaginator\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [hidePageSize]=\"smallScreen\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial' || singlePagePaginatorHidden}\"\n showFirstLastButtons>\n </mat-paginator>\n\n <!-- Added: manual paginator for server-side paged mode \u2014 fully state-bound, never attached to MatTableDataSource. Always visible: when filtering it pages the in-memory filtered subset (length = filtered count); otherwise the server window (length = true total). No first/last jump (would force fetching the whole gap). -->\n <mat-paginator *ngIf=\"pagedMode\"\n [length]=\"filterActive ? filteredRows.length : serverTotal + overlayDelta\"\n [pageIndex]=\"pageIndex\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [hidePageSize]=\"smallScreen\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0) || loadingStage === 'initial' || singlePagePaginatorHidden}\"\n (page)=\"onServerPage($event)\">\n </mat-paginator>\n\n </div>\n \n <!-- Added (Quiet Loading): the initial-load stage for the NON-table views.\n Without this, card/capsule/grouped showed literally nothing during a first load \u2014 the view component\n renders an empty dataSource and the \"No Data\" line is suppressed while the stage owns the space.\n The progress header is identical to the table's so the two feel like one feature; only the ghost\n furniture differs, because column-shaped rows are wrong in a card grid. -->\n <div *ngIf=\"loadingStage === 'initial' && config?.viewType && config?.viewType !== 'table'\" class=\"tin-load-stage\" role=\"status\" aria-busy=\"true\">\n <div class=\"tin-load-progress\">\n <div class=\"tin-load-bar\">\n <span class=\"tin-load-bar-fill\" [style.width.%]=\"progressPercent\"></span>\n </div>\n <span class=\"tin-load-percent\" aria-hidden=\"true\">{{progressDisplay}}%</span>\n </div>\n <div class=\"tin-load-caption\">Loading {{stageEntityName}}\u2026</div>\n\n <div *ngIf=\"config?.viewType === 'capsule'\" class=\"tin-load-ghost-capsules\" aria-hidden=\"true\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let c of ghostCapsules\"></span>\n </div>\n\n <div *ngIf=\"config?.viewType === 'card'\" class=\"tin-load-ghost-cards\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-card\" *ngFor=\"let c of ghostCards\">\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-title\"></span>\n <span class=\"tin-skel tin-skel-text\"></span>\n <span class=\"tin-skel tin-skel-text tin-load-ghost-card-short\"></span>\n </div>\n </div>\n\n <div *ngIf=\"config?.viewType === 'grouped'\" class=\"tin-load-ghost-groups\" aria-hidden=\"true\">\n <div class=\"tin-load-ghost-group\" *ngFor=\"let g of ghostGroups\">\n <span class=\"tin-skel tin-load-ghost-group-head\"></span>\n <!-- Pills, not rows: a group card's body is a wrap of chips, so full-width bars promised a table\n and the stage did not resemble what replaced it. -->\n <div class=\"tin-load-ghost-group-items\">\n <span class=\"tin-skel tin-load-ghost-capsule\" *ngFor=\"let r of ghostCards\"></span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Capsules -->\n <spa-capsules *ngIf=\"config?.viewType === 'capsule' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n (actionClick)=\"actionClicked($event.name, $event.row, undefined, $event.button)\"> <!-- Changed: pass the Button spa-capsules rendered into actionClicked's long-existing optional 4th argument, exactly as the row binding at :131 and the group-header binding at :245 do. Without it a colliding table in capsule view still resolved by name and ran the framework's approve/cancel. \"undefined\" for group is load-bearing \u2014 it keeps every group-only branch inside actionClicked switched off exactly as before. -->\n </spa-capsules>\n\n\n <!-- Cards -->\n <!-- Changed: the (actionClick) binding below passes the Button spa-cards rendered into actionClicked's long-existing optional 4th argument, exactly as the row binding at :131 and the group-header binding at :245 do. Without it a colliding table in card view resolved by name and ran the framework's approve/cancel. \"undefined\" for group is load-bearing \u2014 it keeps every group-only branch inside actionClicked switched off exactly as before. -->\n <spa-cards *ngIf=\"config?.viewType === 'card' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked($event.name, $event.row, undefined, $event.button)\"\n (columnClick)=\"columnClicked($event.column, $event.row)\"\n (showBannerEvent)=\"showBanner($event)\">\n </spa-cards>\n\n <!-- Groups - Added: New grouped view type -->\n <!-- Changed: the grouped view no longer renders its own filter field. It used to sit on a row of its own\n beneath the buttons row, which left both rows half empty and, more importantly, was a SECOND filter with\n no refresh button. The standard header filter (which has refresh, like every other table) now drives it,\n with its text relayed in through filterText. -->\n <spa-groups *ngIf=\"config?.viewType === 'grouped' && loadingStage !== 'initial'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [showOwnFilter]=\"false\"\n [filterText]=\"groupFilterText\"\n (actionClick)=\"actionClicked($event.name, $event.row, $event.group, $event.button)\">\n </spa-groups>\n\n\n <div class=\"tin-center\">\n <p *ngIf=\"dataSource?.length == 0 && loadingStage !== 'initial' && !loadFailed\"><em>{{config.noDataMessage ?? 'No Data'}}</em></p> <!-- Changed (Quiet Loading): the stage owns the space until it completes, then hands straight over to this message \u2014 no \"No Data\" flashing underneath the ghost rows --><!-- Changed: ...and NOT when the load failed. A failed read leaves dataSource as an empty array, so this line confidently reported \"No Data\" \u2014 an assertion about the data \u2014 when the truth was that we never got any. That state now has its own sentence below. -->\n\n <!-- Added: the load-failed state. \"There is nothing here\" and \"we could not find out\" are different facts and\n this grid stated the first for both. Gated on having no rows, so a failed REFRESH leaves the rows already\n on screen alone \u2014 the error dialog covers that case and an empty state under live rows would be a lie. -->\n <div *ngIf=\"!(dataSource?.length > 0) && !loadingStage && loadFailed\" class=\"tin-filter-empty\" role=\"status\">\n <p class=\"tin-filter-empty-title\"><em>Couldn\u2019t load {{stageEntityName}}</em></p>\n <p class=\"tin-filter-empty-hint\">Check your connection, then use the refresh button to try again.</p>\n </div>\n\n <!-- Added: the OTHER empty state. The line above means \"there is nothing here yet\"; this one means \"nothing\n matches what you typed\", and until now it rendered nothing at all \u2014 a header, a \"0 of 0\" paginator and\n no reason. filteredToNothing is false whenever dataSource is empty, so the two can never both show and\n the line above is reached on exactly the rows it always was. role=\"status\" announces the change to a\n screen reader, which otherwise gets silence when the rows disappear. -->\n <!-- Changed: ONE message became TWO, because \"no matches\" means two different things and only one of them\n was ever true. In paged mode the client filter runs over the loaded WINDOW and issues no fetch, so with\n a partial window \"No matches for X\" overstates what was searched and \"see all 240 rows\" implies the\n filter covered 240 when it covered 20 \u2014 while the banner at the top of the grid was simultaneously\n saying so. The split is driven by filterScopePartial, the same getter that banner binds to, so the two\n cannot contradict each other. Neither variant restates a COUNT the paginator or the banner already owns. -->\n\n <!-- COMPLETE scope: the filter saw every row there is (any unpaged grid, or a paged one holding the full\n set \u2014 search/POST mode and the no-total degrade both land here). The total is honest, so it is offered. -->\n <div *ngIf=\"filteredToNothing && !filterScopePartial\" class=\"tin-filter-empty\" role=\"status\">\n <p class=\"tin-filter-empty-title\"><em>{{filterEmptyTitle}}</em></p>\n <p class=\"tin-filter-empty-hint\">Clear the filter to see all {{filteredTotal}} {{filteredRowNoun}}.</p>\n </div>\n\n <!-- PARTIAL scope: the filter saw only the rows loaded so far. The headline says exactly that, and the hint\n stops at the way back \u2014 the banner above already carries both the counts and the \"use search\" remedy,\n so repeating either here would be the contradiction this split exists to remove. -->\n <div *ngIf=\"filteredToNothing && filterScopePartial\" class=\"tin-filter-empty\" role=\"status\">\n <p class=\"tin-filter-empty-title\"><em>{{filterEmptyTitlePartial}}</em></p>\n <p class=\"tin-filter-empty-hint\">Clear the filter to keep browsing.</p>\n </div>\n </div>\n\n </ng-container> <!-- Added: end section-collapse wrapper -->\n\n</ng-container>\n\n\n<!-- Changed: \"Access Restricted\" named nothing and offered no next step, and as a bare centred italic <p> it read\n as page furniture. A refusal is the one member of this file's empty-state family (:259 no rows, :264 couldn't\n load, :283/:291 filtered to nothing) that was never given a title, a hint and role=\"status\". It is also the\n only one that is NOT about data: it is decided at table.component.ts:65 from the config and the role, before\n any request exists, which is why it can name the permission with certainty \u2014 and why it can never collide\n with the \"no rows\" line, which lives inside the *ngIf=\"hasFormAccess\" container opened at :1.\n The capability DISPLAY is what a user repeats to an administrator; the raw capN key stays in the log line. -->\n<ng-container *ngIf=\"!hasFormAccess\">\n <div class=\"tin-center\">\n <div class=\"tin-filter-empty\" role=\"status\">\n <!-- Changed (Phase 4): both sentences now come from Core via the component, not from literals here. The\n route tier (the no-access page) renders the same two strings from the same source, so the grid and the\n page cannot drift apart. The empty-allow case still lands on the generic hint \u2014 Core.noAccessHint\n returns it whenever there is no capability display to name. -->\n <!-- Changed (Phase 4): tin-no-access-title added ALONGSIDE the shared empty-state class, which is kept so\n the visual styling is untouched. The extra class exists because tin-filter-empty-title is shared with\n \"Couldn't load\" (:265) and both filtered-to-nothing states (:284, :292) \u2014 so it cannot tell a REFUSAL\n from an ordinary empty grid, and a test selecting on it would pass on the wrong state. It is the same\n class the route-tier no-access page uses, so one selector reads a refusal at either tier and reads\n nothing else. -->\n <p class=\"tin-filter-empty-title tin-no-access-title\"><em>{{noAccessTitle}}</em></p>\n <p class=\"tin-filter-empty-hint tin-no-access-hint\">{{noAccessHint}}</p>\n </div>\n </div>\n</ng-container>\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0;padding-top:8px;padding-bottom:16px}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}.dlg-header-classic{padding-left:24px;padding-right:24px}.dlg-title-classic{font-size:20px;font-weight:500;margin-top:10px;margin-bottom:5px}.dialog-header-titles{display:flex;flex-direction:column;justify-content:center;min-width:0}.tin-dlg-head .dialog-header-titles label{margin:0}.tbl-section-header{display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:8px;border:1px solid rgba(0,0,0,.08);border-radius:10px;background:transparent;cursor:pointer;transition:border-color .15s}.tbl-section-header:hover{border-color:#90a4ae}.tbl-section-header.tbl-section-static{cursor:default}.tbl-section-icon{color:#546e7a}.tbl-section-title{font-size:14px;font-weight:600;color:#000000d1}.tbl-section-count{background:#e3f2fd;color:#1565c0;border-radius:12px;padding:2px 10px;font-size:12px}.tbl-section-chip{font-size:12px;font-weight:500;color:#0009}.tbl-section-spacer{margin-left:auto}.tbl-section-chevron{color:#90a4ae}.tbl-section-caption{margin:-4px 12px 10px;font-size:12.5px;line-height:1.45;color:#0000008c;max-width:82ch}.tbl-section-more{padding:0 12px 8px}.tbl-section-link{background:none;border:none;color:#1565c0;cursor:pointer;font-size:13px;padding:0}.tbl-section-link:hover{text-decoration:underline}.tbl-section-icon,.tbl-section-chevron,.tbl-section-count{flex:0 0 auto}.tbl-section-title{flex:1 1 auto;min-width:0}@media (max-width: 700px){.tbl-section-header{gap:8px;padding:10px}.tbl-section-chip,.tbl-section-btn-text.has-icon{display:none}.tbl-section-header button{min-width:0;padding:0 10px}}table.tin-no-col-headers tr.mat-mdc-header-row{display:none}.tin-filter-empty{text-align:center;padding:24px 12px}.tin-filter-empty-title{margin:0}.tin-filter-empty-hint{margin:6px 0 0;font-size:.85em;opacity:.7;overflow-wrap:anywhere}.tin-grip-cell{width:34px;padding-right:0!important;text-align:center}.tin-grip{font-size:18px;height:18px;width:18px;vertical-align:middle;cursor:grab;opacity:.28;transition:opacity .14s ease,color .14s ease}tr.mat-mdc-row:hover .tin-grip{opacity:.7}.tin-grip:active{cursor:grabbing}.tin-grip-disabled,tr.mat-mdc-row:hover .tin-grip-disabled{cursor:not-allowed;opacity:.22}::ng-deep tr.cdk-drag-preview{box-shadow:0 6px 18px #00000038,0 1px 3px #00000024;background:var(--mdc-theme-surface, #fff);opacity:.96;border-radius:4px}::ng-deep tr.cdk-drag-placeholder{background:transparent}::ng-deep .tin-drag-placeholder{height:44px;border:1px dashed rgba(0,0,0,.28);border-radius:4px;background:#00000008;box-sizing:border-box}::ng-deep tr.cdk-drag-animating{transition:transform .22s cubic-bezier(0,0,.2,1)}::ng-deep table.tin-orderable.cdk-drop-list-dragging tr.mat-mdc-row:not(.cdk-drag-placeholder){transition:transform .22s cubic-bezier(0,0,.2,1)}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i15$1.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i15$1.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i15$1.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i15$1.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i15$1.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i15$1.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i15$1.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i15$1.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i15$1.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i15$1.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i16.MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "component", type: i2$1.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: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: i15.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i15.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i15.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: i15.CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "component", type: SearchComponent, selector: "spa-search", inputs: ["config", "smallScreen", "tableDataSource"], outputs: ["searchClick"] }, { kind: "component", type: TableHeaderComponent, selector: "app-table-header", inputs: ["lastSearch", "config", "hideTitle", "tableDataSource", "tileConfig", "smallScreen", "tileReload", "showFilterButton", "data", "tileData", "isRealTime", "isConnected", "refreshing"], outputs: ["createClick", "customClick", "refreshClick", "tileClick", "tileUnClick", "chipClick", "filterChange"] }, { kind: "component", type: TableRowComponent, selector: "app-table-row", inputs: ["column", "row", "config", "smallScreen"], outputs: ["actionClick", "columnClick", "showBannerEvent"] }, { kind: "component", type: TableActionComponent, selector: "app-table-action", inputs: ["displayedButtons", "config", "row", "smallScreen"], outputs: ["actionClick"] }, { kind: "component", type: InlineCellComponent, selector: "app-inline-cell", inputs: ["field", "data"], outputs: ["valueChange"] }, { kind: "component", type: CapsulesComponent, selector: "spa-capsules", inputs: ["config", "dataSource", "displayedButtons"], outputs: ["actionClick"] }, { kind: "component", type: CardsComponent, selector: "spa-cards", inputs: ["config", "dataSource", "displayedButtons", "smallScreen"], outputs: ["actionClick", "columnClick", "showBannerEvent"] }, { kind: "component", type: GroupsComponent, selector: "spa-groups", inputs: ["config", "dataSource", "displayedButtons", "showOwnFilter", "filterText"], outputs: ["actionClick"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
|
|
23624
23848
|
}
|
|
23625
23849
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableComponent, decorators: [{
|
|
23626
23850
|
type: Component,
|
|
@@ -24892,6 +25116,24 @@ class FormComponent {
|
|
|
24892
25116
|
return;
|
|
24893
25117
|
}
|
|
24894
25118
|
this.fields = this.config.fields;
|
|
25119
|
+
// Added (addpig-label-leak): STAMP each field with the config this form is actually rendering.
|
|
25120
|
+
//
|
|
25121
|
+
// A handler that must rewrite a SIBLING field's `alias`/`hint` needs the live object, and since
|
|
25122
|
+
// 20.14.82 (42d665c) deep-clones the config per dialog open, the app service's own singleton is NOT
|
|
25123
|
+
// it — a write there repaints nothing and then leaks into the next open (measured on piglet :4203).
|
|
25124
|
+
// `onChange`/`onSelectChange` now receive the config as a trailing argument, but form.component is not
|
|
25125
|
+
// the only caller: spa-select-common and spa-text-single invoke `field.onSelectChange` themselves, and
|
|
25126
|
+
// the select markup is a CONSUMER-PROJECTABLE template (#dynamicSelect), so threading a new @Input
|
|
25127
|
+
// through it would reach outside this library. Stamping the field is what lets every call path answer
|
|
25128
|
+
// the same question without a new binding.
|
|
25129
|
+
//
|
|
25130
|
+
// NON-ENUMERABLE on purpose, and that is load-bearing twice over: `Core.deepClone2` copies via
|
|
25131
|
+
// Object.entries, so this never clones (each form stamps its own object afresh, and no clone can
|
|
25132
|
+
// inherit a stale form's config), and JSON.stringify of a field is unaffected — so no payload,
|
|
25133
|
+
// log or storage path can trip over the reference cycle it would otherwise create.
|
|
25134
|
+
this.fields?.forEach(field => {
|
|
25135
|
+
Object.defineProperty(field, 'formConfig', { value: this.config, enumerable: false, configurable: true, writable: true });
|
|
25136
|
+
});
|
|
24895
25137
|
// Initialize BehaviorSubjects for fields with loadActions
|
|
24896
25138
|
this.fields?.forEach(field => {
|
|
24897
25139
|
if (field.loadAction && !field.masterField) {
|
|
@@ -25228,7 +25470,7 @@ class FormComponent {
|
|
|
25228
25470
|
if (field.onSelectChange) {
|
|
25229
25471
|
// Changed: Find and pass the selected option as the third parameter
|
|
25230
25472
|
const selectedOption = field.options?.find(opt => opt[field.optionValue || 'value'] === this.data[field.name]);
|
|
25231
|
-
field.onSelectChange(this.data[field.name], this.data, selectedOption);
|
|
25473
|
+
field.onSelectChange(this.data[field.name], this.data, selectedOption, this.config); // Changed (addpig-label-leak): pass the LIVE config as a 4th arg, matching inputChanged below — a select that drives a sibling field's copy (a quantity that rewrites a price label) needs the object this form is actually rendering, not the app's singleton
|
|
25232
25474
|
}
|
|
25233
25475
|
}
|
|
25234
25476
|
inputChanged(field, value) {
|
|
@@ -25248,7 +25490,7 @@ class FormComponent {
|
|
|
25248
25490
|
// had none, so nothing could react to them. That is why a label/hint cannot currently follow a
|
|
25249
25491
|
// checkbox or a money value. Additive and backwards-compatible: no existing form field sets
|
|
25250
25492
|
// onChange, so nothing that works today changes behaviour.
|
|
25251
|
-
field.onChange?.(value, this.data);
|
|
25493
|
+
field.onChange?.(value, this.data, this.config); // Changed (addpig-label-leak): third arg is the LIVE FormConfig this form renders. Without it a handler could only reach its app service's SINGLETON config, and since 42d665c (20.14.82) deep-clones the config per dialog open, such a write never repainted the open dialog and then surfaced — wrong — on the next one. See Field.onChange in Classes.ts for the measurement
|
|
25252
25494
|
this.refreshVisibleFields(); // TS-11: input value may toggle field visibility / section collapse — refresh cache
|
|
25253
25495
|
}
|
|
25254
25496
|
updateChildOptions(masterFieldName) {
|
|
@@ -28044,7 +28286,7 @@ class NavMenuComponent {
|
|
|
28044
28286
|
return !this.isMiniSidebar || this.isMiniHovered;
|
|
28045
28287
|
}
|
|
28046
28288
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: NavMenuComponent, deps: [{ token: i1$1.Router }, { token: AuthService }, { token: StorageService }, { token: NotificationsService }, { token: i1$6.BreakpointObserver }, { token: DataServiceLib }, { token: i1$3.MatDialog }, { token: SubscriptionService }, { token: SetupService }, { token: LastRouteService }, { token: OfflineService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
28047
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: NavMenuComponent, isStandalone: false, selector: "spa-nav-menu", inputs: { appConfig: "appConfig", footer: "footer" }, host: { listeners: { "window:scroll": "onWindowScroll()" } }, ngImport: i0, template: "<header *ngIf=\"loggedin && dataService.appConfig.navigation == 'top'\">\n\n <!-- Changed: Removed mb-3 class to eliminate gap between toolbar and content -->\n <nav class=\"toolbar navbar navbar-expand-sm navbar-toggleable-sm navbar-light border-bottom box-shadow\" style=\"padding-right: 10px;\">\n\n\n <div class=\"container-fluid\" style=\"padding-right: 0px;\">\n\n <img *ngIf=\"appConfig.logo!=''\" [src]=\"appConfig.logo\" style=\"height: 50px; margin-right: 2em\" />\n\n <div>\n <!-- <div style=\"font-size: 20px;\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px;\">\n {{tenantName}}\n </div> -->\n\n <div *ngIf=\"!dataService.appConfig.multitenant\" style=\"font-size: 22px;\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant\" style=\"font-size: 20px; ; font-weight: 400;\" [ngStyle]=\"{'margin-top': dataService.appConfig.multitenant ? '12px' : ''}\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px; margin-bottom: 5px;\">\n {{tenantName}}\n </div>\n\n </div>\n\n\n\n <button class=\"navbar-toggler\" type=\"button\" data-toggle=\"collapse\" data-target=\".navbar-collapse\" aria-label=\"Toggle navigation\" [attr.aria-expanded]=\"isExpanded\" (click)=\"toggle()\">\n <span class=\"navbar-toggler-icon\"></span>\n </button>\n\n <div *ngIf=\"myRole\" class=\" navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse stack-top\" style=\"margin-right: 0px;\" [ngClass]=\"{ show: isExpanded, navitems: isExpanded }\" >\n\n <button mat-icon-button (click)=\"logoff()\" > <mat-icon>logout</mat-icon> </button>\n\n <div *ngIf=\"dataService.appConfig.multitenant\">\n\n <button mat-icon-button (click)=\"redirectTo('home/tenancy/settings')\" > <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon> </button>\n\n <!-- Removed: Support icon \u2014 replaced by floating assistant chat widget -->\n </div>\n\n\n <button id=\"btnUser\" mat-button [matMenuTriggerFor]=\"profileMenu\" ><mat-icon style=\"font-size: 24px;\">account_circle</mat-icon> {{loggedUserFullName}}</button>\n\n <mat-menu #profileMenu=\"matMenu\">\n <button id=\"btnProfile\" mat-menu-item (click)=\"redirectTo('home/user/profile')\" >Profile</button>\n <button id=\"btnLogOff\" mat-menu-item (click)=\"logoff()\">Log Off</button>\n </mat-menu>\n\n <div *ngFor=\"let item of reversedCapItems\">\n\n <!-- Menu Item \u2014 Added: isFeatureAllowed check for plan-based gating -->\n <button id=\"btnMenu\" *ngIf=\"myRole[item.name] && !item.capSubItems && item.showMenu && isFeatureAllowed(item)\" mat-button (click)=\"redirectTo(item.link)\">{{item.display}}</button>\n\n <!-- Menu Item with Sub items ignored \u2014 Added: isFeatureAllowed check -->\n <button id=\"btnMenu\" *ngIf=\"myRole[item.name] && item.capSubItems && item.showMenu && item.ignoreSubsDisplay && isFeatureAllowed(item)\" mat-button (click)=\"redirectTo(item.link)\">{{item.display}}</button>\n\n <!-- Menu Item with Sub items to display \u2014 Added: isFeatureAllowed check -->\n <button id=\"btnMenu\" *ngIf=\"myRole[item.name] && item.capSubItems && item.showMenu && !item.ignoreSubsDisplay && isFeatureAllowed(item)\" mat-button [matMenuTriggerFor]=\"adminMenu\">{{item.display}}</button>\n\n\n <!-- Sub Menu Items \u2014 Added: isFeatureAllowed check on sub-items -->\n <mat-menu #adminMenu=\"matMenu\">\n\n <div *ngFor=\"let subItem of item.capSubItems\">\n\n <button *ngIf=\"myRole[subItem.name] && subItem.showMenu && isFeatureAllowed(subItem)\" mat-menu-item (click)=\"redirectTo(subItem.link)\">{{subItem.display}}</button>\n\n </div>\n\n </mat-menu>\n\n </div>\n\n </div>\n\n\n </div>\n\n </nav>\n\n</header>\n\n<!-- Changed: Removed top/bottom padding to eliminate gaps, but kept left/right padding for content spacing -->\n<div class=\"container-fluid tin-bg-image\" [class.has-bottom-tabs]=\"loggedin && smallScreen\" *ngIf=\"dataService.appConfig.navigation == 'top'\" style=\"padding: 12px 12px; margin: 0;\"> <!-- Changed: bottom padding so the fixed tab bar cannot cover the last row -->\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n\n\n\n<!-- ============================================================ -->\n<!-- TOP-MODERN (navigation == 'top-modern') -->\n<!-- Changed: Modernised horizontal top navigation -->\n<!-- ============================================================ -->\n<header *ngIf=\"loggedin && dataService.appConfig.navigation == 'top-modern'\">\n\n <nav class=\"tm-navbar\" [class.tm-scrolled]=\"topbarScrolled\">\n <div class=\"tm-bar\">\n\n <!-- Brand -->\n <div class=\"tm-brand\" (click)=\"modernNavigate('')\">\n <img *ngIf=\"appConfig.logo\" [src]=\"appConfig.logo\" class=\"tm-logo\" alt=\"logo\" />\n <div class=\"tm-brand-text\">\n <div class=\"tm-app-name\">{{appConfig.appName}}</div>\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" class=\"tm-tenant\">{{tenantName}}</div>\n </div>\n </div>\n\n <!-- Mobile toggle -->\n <button class=\"tm-toggler\" type=\"button\" aria-label=\"Toggle navigation\"\n [attr.aria-expanded]=\"isExpanded\" (click)=\"toggle()\">\n <mat-icon>{{isExpanded ? 'close' : 'menu'}}</mat-icon>\n </button>\n\n <!-- Menu items -->\n <div class=\"tm-menu\" [class.tm-menu-open]=\"isExpanded\">\n\n <ng-container *ngFor=\"let item of dataService.appConfig.capItems\">\n <div class=\"tm-item-wrap\"\n *ngIf=\"myRole[item.name] && item.showMenu && isFeatureAllowed(item)\">\n\n <!-- Simple item (no sub-items, or sub-items ignored for display) -->\n <button *ngIf=\"!item.capSubItems || item.ignoreSubsDisplay\"\n class=\"tm-item\"\n [class.tm-item-active]=\"isActiveRoute(item.link)\"\n (click)=\"modernNavigate(item.link)\">\n <mat-icon *ngIf=\"item.icon && item.icon != 'navigate_next'\" class=\"tm-item-icon\">{{item.icon}}</mat-icon>\n <span class=\"tm-item-text\">{{item.display}}</span>\n </button>\n\n <!-- Parent item with displayed sub-items -->\n <ng-container *ngIf=\"item.capSubItems && !item.ignoreSubsDisplay\">\n <button class=\"tm-item tm-item-parent\"\n [class.tm-item-active]=\"isParentActive(item)\"\n [matMenuTriggerFor]=\"tmSubMenu\">\n <mat-icon *ngIf=\"item.icon && item.icon != 'navigate_next'\" class=\"tm-item-icon\">{{item.icon}}</mat-icon>\n <span class=\"tm-item-text\">{{item.display}}</span>\n <!-- Changed: Caret signals this item has more items beneath it -->\n <mat-icon class=\"tm-item-caret\">expand_more</mat-icon>\n </button>\n\n <mat-menu #tmSubMenu=\"matMenu\" class=\"tm-submenu-panel\" [overlapTrigger]=\"false\" yPosition=\"below\">\n <ng-container *ngFor=\"let sub of getSubItems(item)\">\n <button *ngIf=\"myRole[sub.name] && sub.showMenu && isFeatureAllowed(sub)\"\n mat-menu-item\n [class.tm-sub-active]=\"isActiveRoute(sub.link)\"\n (click)=\"modernNavigate(sub.link)\">\n <mat-icon *ngIf=\"sub.icon && sub.icon != 'navigate_next'\">{{sub.icon}}</mat-icon>\n <span>{{sub.display}}</span>\n </button>\n </ng-container>\n </mat-menu>\n </ng-container>\n\n </div>\n </ng-container>\n\n </div>\n\n <!-- Right-side actions -->\n <div class=\"tm-actions\" [class.tm-actions-open]=\"isExpanded\">\n\n <ng-container *ngIf=\"dataService.appConfig.multitenant\">\n <button mat-icon-button class=\"tm-action-btn\" (click)=\"modernNavigate('home/tenancy/settings')\" matTooltip=\"Organisation Settings\">\n <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon>\n </button>\n <button *ngIf=\"setupService.enabled && ((setupCount$ | async) || 0) > 0 && !smallScreen\" mat-icon-button class=\"tm-action-btn\" (click)=\"modernNavigate('home/setup')\" matTooltip=\"Getting Started\"> <!-- Added: Setup readiness badge \u2014 hidden when complete or unconfigured -->\n <mat-icon [matBadge]=\"setupCount$ | async\" matBadgeColor=\"warn\" matBadgeSize=\"small\">rocket_launch</mat-icon>\n </button>\n <button *ngIf=\"!smallScreen\" mat-icon-button class=\"tm-action-btn\" (click)=\"modernNavigate('home/workflow/notifications')\" matTooltip=\"Notifications\">\n <mat-icon [matBadge]=\"notificationCount$ | async\" [matBadgeHidden]=\"(notificationCount$ | async) === 0\" matBadgeColor=\"warn\" matBadgeSize=\"small\">notifications</mat-icon>\n </button>\n <spa-offline-indicator *ngIf=\"!smallScreen\"></spa-offline-indicator> <!-- Changed: TinSync connection + pending-sync indicator -->\n </ng-container>\n\n <span class=\"tm-divider-v\"></span>\n\n <!-- Profile -->\n <button mat-button class=\"tm-user-btn\" [matMenuTriggerFor]=\"tmProfileMenu\">\n <mat-icon class=\"tm-user-icon\">account_circle</mat-icon>\n <span class=\"tm-user-name\">{{loggedUserFullName}}</span>\n </button>\n\n <mat-menu #tmProfileMenu=\"matMenu\" [overlapTrigger]=\"false\" yPosition=\"below\">\n <button mat-menu-item (click)=\"modernNavigate('home/user/profile')\">\n <mat-icon>person</mat-icon><span>Profile</span>\n </button>\n <mat-divider></mat-divider>\n <button mat-menu-item (click)=\"logoff()\">\n <mat-icon>logout</mat-icon><span>Log Off</span>\n </button>\n </mat-menu>\n\n <!-- Sign out \u2014 Changed: aligned via flex-centred action button -->\n <button mat-icon-button class=\"tm-action-btn tm-signout\" (click)=\"logoff()\" matTooltip=\"Sign Out\">\n <mat-icon>logout</mat-icon>\n </button>\n\n </div>\n\n </div>\n </nav>\n\n</header>\n\n<!-- Top-modern page content \u2014 Changed: use original 'top' background (tin-bg-image), no footer bar -->\n<div class=\"container-fluid tin-bg-image\" [class.has-bottom-tabs]=\"loggedin && smallScreen\" *ngIf=\"loggedin && dataService.appConfig.navigation == 'top-modern'\" style=\"padding: 12px 12px; margin: 0;\"> <!-- Changed: bottom padding so the fixed tab bar cannot cover the last row -->\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n<!-- Not logged in fallback for top-modern -->\n<div class=\"tin-bg-image\" *ngIf=\"!loggedin && dataService.appConfig.navigation == 'top-modern'\">\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n\n\n\n<!-- SIDE -->\n<mat-toolbar class=\"tin-bg-image-toolbar\" *ngIf=\"loggedin && dataService.appConfig.navigation == 'side'\" style=\"padding: 0px 8px;\">\n\n <button mat-icon-button (click)=\"toggle()\" matTooltip=\"Menu\">\n <mat-icon>menu</mat-icon>\n </button>\n\n <img [src]=\"dataService.appConfig.logo\" style=\"height: 50px;\" />\n\n <div style=\"padding-left: 10px; \">\n\n <div style=\"font-size: 22px; font-weight: 400;\">\n {{appConfig.appName}}\n </div>\n\n <!-- <div style=\"font-size: 20px; height: 25px; font-weight: 400;\" [ngStyle]=\"{'margin-top': dataService.appConfig.multitenant ? '12px' : ''}\">\n {{appConfig.appName}}\n </div> -->\n\n <!-- <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px; margin-bottom: 5px;\">\n {{tenantName}}\n </div> -->\n\n </div>\n\n\n\n <span class=\"toolbar-item-spacer\"></span>\n\n <!-- buttons -->\n\n <div *ngIf=\"dataService.appConfig.multitenant\" style=\"display: flex; align-items: center;\">\n\n <!-- <label style=\"font-size: 14px;\">Hi, {{loggedUserFullName}}</label> -->\n\n <button mat-icon-button (click)=\"redirectTo('home/tenancy/settings')\" matTooltip=\"Organisation Settings\">\n <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon>\n </button>\n <label style=\"font-size: 14px;margin-right: 20px;\">{{tenantName}}</label>\n\n <!-- Changed: Support/help icon removed \u2014 replaced by floating agent chat widget -->\n\n <button *ngIf=\"setupService.enabled && ((setupCount$ | async) || 0) > 0 && !smallScreen\" mat-icon-button (click)=\"redirectTo('home/setup')\" matTooltip=\"Getting Started\"> <!-- Added: Setup readiness badge \u2014 hidden when complete or unconfigured -->\n <mat-icon [matBadge]=\"setupCount$ | async\" matBadgeColor=\"warn\" matBadgeSize=\"small\">rocket_launch</mat-icon>\n </button>\n <button *ngIf=\"!smallScreen\" mat-icon-button (click)=\"redirectTo('home/workflow/notifications')\" matTooltip=\"Notifications\">\n <mat-icon [matBadge]=\"notificationCount$ | async\" [matBadgeHidden]=\"(notificationCount$ | async) === 0\" matBadgeColor=\"warn\" matBadgeSize=\"small\">notifications</mat-icon>\n </button>\n\n <spa-offline-indicator *ngIf=\"!smallScreen\"></spa-offline-indicator> <!-- Changed: TinSync connection + pending-sync indicator -->\n\n </div>\n\n\n\n <button mat-icon-button matTooltip=\"My Account\" [matMenuTriggerFor]=\"userAccountMenu\"><mat-icon>account_circle</mat-icon></button>\n <label style=\"font-size: 14px;\">{{loggedUserFullName}}</label>\n\n <button *ngIf=\"!smallScreen\" mat-icon-button (click)=\"logoff()\" matTooltip=\"Signout\">\n <mat-icon>logout</mat-icon>\n </button>\n\n\n <!-- my account menu -->\n <mat-menu #userAccountMenu [overlapTrigger]=\"false\" yPosition=\"below\">\n\n\n <button mat-menu-item routerLink=\"home/user/profile\">\n <mat-icon>person</mat-icon><span>Profile</span>\n </button>\n\n <!-- Removed: Help menu item \u2014 replaced by floating assistant chat widget -->\n\n <mat-divider></mat-divider>\n\n <button mat-menu-item (click)=\"logoff()\">\n <mat-icon>logout</mat-icon>Logout\n </button>\n\n </mat-menu>\n\n</mat-toolbar>\n\n\n\n\n<mat-sidenav-container class=\"app-container\" [hasBackdrop]=\"smallScreen\" *ngIf=\"loggedin && dataService.appConfig.navigation == 'side'\">\n\n <mat-sidenav #sidenav [mode]=\"smallScreen ? 'over' : 'side'\" [class.mat-elevation-z4]=\"true\" [opened]=\"isExpanded\" class=\"app-sidenav side-color\" style=\"height: 100%;\"\n [ngStyle]=\"{'width': dataService.appConfig.navWidth}\">\n <mat-nav-list >\n\n <ng-container *ngFor=\"let cap of dataService.appConfig.capItems\" >\n\n <!-- Menu item \u2014 Added: isFeatureAllowed check for plan-based gating -->\n <mat-list-item [routerLink]=\"cap.link\" *ngIf=\"myRole[cap.name] && cap.showMenu && (!cap.capSubItems || cap.capSubItems && cap.ignoreSubsDisplay) && isFeatureAllowed(cap)\" style=\"height: 40px;font-size: 15px;\"\n (click)=\"smallScreen ? toggle() : null\">\n <mat-icon [ngStyle]=\"{'color': cap.color}\" style=\"margin-right: 5px;\">{{cap.icon}}</mat-icon>{{cap.display}}\n </mat-list-item>\n\n <!-- Menu With Sub items \u2014 Added: isFeatureAllowed check -->\n <mat-expansion-panel class=\"side-color\" [class.mat-elevation-z0]=\"true\" *ngIf=\"myRole[cap.name] && cap.showMenu && cap.capSubItems && !cap.ignoreSubsDisplay && isFeatureAllowed(cap)\">\n\n <mat-expansion-panel-header style=\"height: 40px;padding-left: 15px;\">\n <mat-icon [ngStyle]=\"{'color': cap.color}\" style=\"margin-right: 5px;\">{{cap.icon != 'navigate_next' ? cap.icon : 'fiber_manual_record' }}</mat-icon>{{cap.display}}\n </mat-expansion-panel-header>\n\n <!-- Sub items - Changed: Use ng-container to avoid blank spaces for hidden items -->\n <mat-nav-list>\n <ng-container *ngFor=\"let capSub of getSubItems(cap)\">\n <mat-list-item [routerLink]=\"capSub.link\" style=\"height: 30px; font-size: 15px; padding-left: 4px; padding-right: 10px; margin-bottom: 5px;\" (click)=\"smallScreen ? toggle() : null\" *ngIf=\"myRole[capSub.name] && capSub.showMenu && isFeatureAllowed(capSub)\" [matTooltip]=\"capSub.display\" matTooltipPosition=\"right\">\n <mat-icon [ngStyle]=\"{'color': capSub.color}\" style=\"margin-right: 5px;\">{{capSub.icon}}</mat-icon>{{capSub.display}}\n </mat-list-item>\n </ng-container>\n </mat-nav-list>\n\n </mat-expansion-panel>\n\n </ng-container>\n\n </mat-nav-list>\n </mat-sidenav>\n\n\n\n <mat-sidenav-content class=\"tin-bg-image\" [class.has-bottom-tabs]=\"loggedin && smallScreen\" style=\"padding: 0px 12px;\" *ngIf=\"loggedin && dataService.appConfig.navigation == 'side'\"> <!-- Changed: bottom padding so the fixed tab bar cannot cover the last row -->\n <hr style=\"margin-top: 0px;\">\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n </mat-sidenav-content>\n\n</mat-sidenav-container>\n\n\n<!-- footer -->\n<!-- Changed: hidden on mobile \u2014 the bottom tab bar owns the bottom edge there, and a footer under a fixed\n bar is either invisible or a second competing strip of chrome -->\n<div class=\"tin-center\" *ngIf=\"loggedin && !smallScreen && dataService.appConfig.navigation == 'side'\">\n <label style=\"text-align: center; font-size: 12px;\">© {{nowDate | date : 'yyyy'}} <a color=\"primary\" class=\"terms-link\" [href]=\"appConfig.siteUrl\" target=\"_blank\">{{footer}}</a> | <a color=\"primary\" class=\"terms-link\" style=\"cursor: pointer;\" (click)=\"openTerms()\">Terms</a> | <a color=\"primary\" class=\"terms-link\" style=\"cursor: pointer;\" (click)=\"openPrivacy()\">Privacy Policy</a></label>\n</div>\n\n\n<div class=\"tin-bg-image\" *ngIf=\"!loggedin && dataService.appConfig.navigation == 'side'\">\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n\n<!-- SIDE-MODERN -->\n\n<!-- Changed: Side-modern navigation layout -->\n<div class=\"sm-layout\"\n *ngIf=\"loggedin && dataService.appConfig.navigation == 'side-modern'\"\n [class.sm-mini]=\"isMiniSidebar && !isMiniHovered\"\n [class.sm-mini-hovered]=\"isMiniSidebar && isMiniHovered\"\n [class.sm-mobile-open]=\"smallScreen && isExpanded\">\n\n <!-- Sidebar -->\n <aside class=\"sm-sidebar\"\n (mouseenter)=\"onMiniMouseEnter()\"\n (mouseleave)=\"onMiniMouseLeave()\">\n\n <!-- Background layers -->\n <div class=\"sm-sidebar-bg\">\n <div class=\"sm-sidebar-bg-image\" *ngIf=\"appConfig.navImage\" [ngStyle]=\"{'background-image': 'url(' + appConfig.navImage + ')'}\"></div>\n <div class=\"sm-sidebar-bg-overlay\" [ngStyle]=\"{'background-color': appConfig.navColor}\"></div>\n </div>\n\n <!-- Sidebar content -->\n <div class=\"sm-sidebar-content\">\n\n <!-- Brand -->\n <div class=\"sm-brand\">\n <img *ngIf=\"appConfig.logo\" [src]=\"appConfig.logo\" alt=\"logo\" />\n <span class=\"sm-brand-name\">{{appConfig.appName}}</span>\n </div>\n\n <mat-divider></mat-divider>\n\n <!-- Profile -->\n <div class=\"sm-profile\">\n <mat-icon class=\"sm-profile-icon\">account_circle</mat-icon>\n <div class=\"sm-profile-info\">\n <div class=\"sm-profile-name\">{{loggedUserFullName}}</div>\n <div class=\"sm-profile-role\">{{tenantName || 'User'}}</div>\n </div>\n </div>\n\n <mat-divider></mat-divider>\n\n <!-- Scrollable menu -->\n <div class=\"sm-menu-scroll\">\n\n <ng-container *ngFor=\"let cap of dataService.appConfig.capItems\">\n\n <!-- Simple menu item (no sub-items or ignoring sub display) \u2014 Added: isFeatureAllowed check -->\n <div *ngIf=\"myRole[cap.name] && cap.showMenu && (!cap.capSubItems || cap.ignoreSubsDisplay) && isFeatureAllowed(cap)\"\n class=\"sm-menu-item\"\n [class.sm-active]=\"isActiveRoute(cap.link)\"\n (click)=\"modernNavigate(cap.link)\">\n <mat-icon class=\"sm-menu-icon\">{{cap.icon != 'navigate_next' ? cap.icon : 'dashboard'}}</mat-icon>\n <span class=\"sm-menu-text\">{{cap.display}}</span>\n </div>\n\n <!-- Parent menu item with sub-items \u2014 Added: isFeatureAllowed check -->\n <ng-container *ngIf=\"myRole[cap.name] && cap.showMenu && cap.capSubItems && !cap.ignoreSubsDisplay && isFeatureAllowed(cap)\">\n\n <!-- Parent item (toggles sub-menu) -->\n <div class=\"sm-menu-item\"\n [class.sm-active]=\"isParentActive(cap) && !isMenuOpen(cap.name)\"\n (click)=\"toggleModernMenu(cap.name)\">\n <mat-icon class=\"sm-menu-icon\">{{cap.icon != 'navigate_next' ? cap.icon : 'dashboard'}}</mat-icon>\n <span class=\"sm-menu-text\">{{cap.display}}</span>\n <mat-icon class=\"sm-caret\" [class.sm-caret-open]=\"isMenuOpen(cap.name)\">expand_more</mat-icon>\n </div>\n\n <!-- Sub-menu container (animated) -->\n <div class=\"sm-submenu\" [class.sm-submenu-open]=\"isMenuOpen(cap.name)\">\n <ng-container *ngFor=\"let sub of getSubItems(cap)\">\n <div *ngIf=\"myRole[sub.name] && sub.showMenu && isFeatureAllowed(sub)\"\n class=\"sm-submenu-item\"\n [class.sm-active]=\"isActiveRoute(sub.link)\"\n (click)=\"modernNavigate(sub.link)\">\n <mat-icon *ngIf=\"sub.icon && sub.icon != 'navigate_next'\" class=\"sm-sub-icon\">{{sub.icon}}</mat-icon>\n <span *ngIf=\"!sub.icon || sub.icon == 'navigate_next'\" class=\"sm-initials\">{{getInitials(sub.display)}}</span>\n <span class=\"sm-menu-text\">{{sub.display}}</span>\n </div>\n </ng-container>\n </div>\n\n </ng-container>\n\n </ng-container>\n\n </div>\n\n </div>\n </aside>\n\n <!-- Mobile backdrop -->\n <div class=\"sm-backdrop\" (click)=\"isExpanded = false\"></div>\n\n <!-- Main content -->\n <div class=\"sm-main\">\n\n <!-- Top bar - Changed: Added scroll class for frosted glass effect -->\n <div class=\"sm-topbar\" [class.sm-topbar-scrolled]=\"topbarScrolled\">\n <button mat-icon-button (click)=\"smallScreen ? toggle() : toggleMiniSidebar()\" matTooltip=\"Menu\">\n <mat-icon>menu</mat-icon>\n </button>\n\n <!-- Changed: Mobile branding - show logo + app name when sidebar is hidden on small screens -->\n <img *ngIf=\"smallScreen && appConfig.logo\" [src]=\"appConfig.logo\" alt=\"logo\" class=\"sm-topbar-logo\" />\n <span *ngIf=\"smallScreen\" class=\"sm-topbar-brand\">{{appConfig.appName}}</span>\n\n <span class=\"sm-topbar-spacer\"></span>\n\n <!-- Multitenant buttons -->\n <div *ngIf=\"dataService.appConfig.multitenant\" style=\"display: flex; align-items: center;\">\n <button mat-icon-button (click)=\"redirectTo('home/tenancy/settings')\" matTooltip=\"Organisation Settings\">\n <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon>\n </button>\n <span class=\"sm-topbar-label\">{{tenantName}}</span>\n\n <!-- Changed: Support/help icon removed \u2014 replaced by floating agent chat widget -->\n\n <button *ngIf=\"setupService.enabled && ((setupCount$ | async) || 0) > 0 && !smallScreen\" mat-icon-button (click)=\"redirectTo('home/setup')\" matTooltip=\"Getting Started\"> <!-- Added: Setup readiness badge \u2014 hidden when complete or unconfigured -->\n <mat-icon [matBadge]=\"setupCount$ | async\" matBadgeColor=\"warn\" matBadgeSize=\"small\">rocket_launch</mat-icon>\n </button>\n <button *ngIf=\"!smallScreen\" mat-icon-button (click)=\"redirectTo('home/workflow/notifications')\" matTooltip=\"Notifications\">\n <mat-icon [matBadge]=\"notificationCount$ | async\" [matBadgeHidden]=\"(notificationCount$ | async) === 0\" matBadgeColor=\"warn\" matBadgeSize=\"small\">notifications</mat-icon>\n </button>\n <spa-offline-indicator *ngIf=\"!smallScreen\"></spa-offline-indicator> <!-- Changed: TinSync connection + pending-sync indicator -->\n </div>\n\n <!-- Profile menu -->\n <button mat-icon-button matTooltip=\"My Account\" [matMenuTriggerFor]=\"smProfileMenu\">\n <mat-icon>account_circle</mat-icon>\n </button>\n <span class=\"sm-topbar-label\">{{loggedUserFullName}}</span>\n\n <mat-menu #smProfileMenu=\"matMenu\" [overlapTrigger]=\"false\" yPosition=\"below\">\n <button mat-menu-item routerLink=\"home/user/profile\">\n <mat-icon>person</mat-icon><span>Profile</span>\n </button>\n <!-- Changed: Help menu item removed \u2014 replaced by floating agent chat widget -->\n <mat-divider></mat-divider>\n <button mat-menu-item (click)=\"logoff()\">\n <mat-icon>logout</mat-icon>Logout\n </button>\n </mat-menu>\n\n <button *ngIf=\"!smallScreen\" mat-icon-button (click)=\"logoff()\" matTooltip=\"Signout\">\n <mat-icon>logout</mat-icon>\n </button>\n </div>\n\n <!-- Page content - Changed: Replaced tin-bg-image with sm-content modern texture -->\n <div class=\"sm-content\" [class.has-bottom-tabs]=\"loggedin && smallScreen\"> <!-- Changed: bottom padding so the fixed tab bar cannot cover the last row -->\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n </div>\n\n <!-- Footer -->\n <div class=\"sm-footer\">\n © {{nowDate | date : 'yyyy'}} <a [href]=\"appConfig.siteUrl\" target=\"_blank\">{{footer}}</a> | <a (click)=\"openTerms()\">Terms</a> | <a (click)=\"openPrivacy()\">Privacy Policy</a>\n </div>\n\n </div>\n\n</div>\n\n<!-- Not logged in fallback for side-modern -->\n<div class=\"tin-bg-image\" *ngIf=\"!loggedin && dataService.appConfig.navigation == 'side-modern'\">\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n<!-- Changed: Cascading toast notifications for real-time entity changes \u2014 visible in all layouts -->\n<spa-toast *ngIf=\"loggedin && dataService.appConfig.multitenant\"></spa-toast>\n\n<!-- Changed: Floating agent chat widget \u2014 renamed from spa-assistant -->\n<!-- Changed (Setup v3): also gated on the \"agent\" module. The widget is floating, not a nav item, so\n CapItem.moduleKey could never reach it \u2014 a tenant who switched the assistant off in Getting Started\n still had it hovering over every page. -->\n<spa-agent *ngIf=\"loggedin && dataService.appConfig.multitenant && setupService.isModuleEnabled('agent')\"></spa-agent>\n\n<!-- Added: mobile bottom tab bar. Mounted ONCE here, outside every layout block, because the bar is\n position: fixed and so serves top, top-modern, side and side-modern from this single instance.\n smallScreen is nav-menu's existing (max-width: 600px) BreakpointObserver \u2014 the one source of truth\n for this feature, so no second breakpoint is introduced. Never renders on desktop. -->\n<spa-bottom-tabs *ngIf=\"loggedin && smallScreen\"></spa-bottom-tabs>\n", styles: ["a.navbar-brand{white-space:normal;text-align:center;word-break:break-all}html{font-size:14px}.box-shadow{box-shadow:0 .25rem .75rem #0000000d}.toolbar-item-spacer{flex:1 1 auto}.toolbar{height:60px;display:flex;align-items:center;background-color:#03a;color:#fff;margin-bottom:0!important}.toolbar button,.toolbar .mat-mdc-button,.toolbar .mat-mdc-icon-button{color:#fff!important}.toolbar mat-icon{color:#fff!important}.stack-top{z-index:9;margin:20px}.navitems{background-color:#03a}.app-container{height:90%;margin:0}.app-sidenav{width:200px;border:1px solid rgb(192,190,199)}.side-color{background-color:#e6f4ff}.app-sidenav mat-list-item{display:flex!important;align-items:center!important}.app-sidenav mat-icon{display:inline-flex!important;align-items:center!important;vertical-align:middle!important}.app-sidenav mat-expansion-panel-header mat-icon{display:inline-flex!important;align-items:center!important;vertical-align:middle!important}::ng-deep .app-sidenav .mat-expansion-panel-body{padding-bottom:5px!important;padding-right:5px!important}::ng-deep .app-sidenav .mdc-list{padding-bottom:0!important}.sm-layout{display:flex;min-height:100vh;position:relative}.sm-sidebar{position:fixed;top:0;left:0;bottom:0;width:260px;z-index:1030;overflow:hidden;transition:width .3s cubic-bezier(.4,0,.2,1)}.sm-sidebar-bg{position:absolute;inset:0;z-index:0}.sm-sidebar-bg-image{position:absolute;inset:0;background-size:cover;background-position:center}.sm-sidebar-bg-overlay{position:absolute;inset:0}.sm-sidebar-content{position:relative;z-index:1;display:flex;flex-direction:column;height:100%;color:#fff}.sm-brand{display:flex;align-items:center;padding:18px 15px 10px;min-height:60px;text-decoration:none;white-space:nowrap;overflow:hidden}.sm-brand img{height:34px;width:34px;object-fit:contain;margin-right:12px;flex-shrink:0}.sm-brand-name{font-size:16px;font-weight:500;letter-spacing:.5px;color:#fff;overflow:hidden;text-overflow:ellipsis;transition:opacity .2s ease}.sm-profile{display:flex;align-items:center;padding:12px 15px;white-space:nowrap;overflow:hidden}.sm-profile-icon{font-size:34px!important;width:34px!important;height:34px!important;margin-right:12px;flex-shrink:0;color:#fffc}.sm-profile-info{overflow:hidden;transition:opacity .2s ease}.sm-profile-name{font-size:14px;font-weight:500;color:#fff;line-height:1.3;overflow:hidden;text-overflow:ellipsis}.sm-profile-role{font-size:11px;color:#fff9;line-height:1.3;overflow:hidden;text-overflow:ellipsis}.sm-sidebar mat-divider{border-color:#ffffff26!important;margin:0 15px}.sm-menu-scroll{flex:1;overflow-y:auto;overflow-x:hidden;padding:8px 0}.sm-menu-scroll::-webkit-scrollbar{width:4px}.sm-menu-scroll::-webkit-scrollbar-track{background:transparent}.sm-menu-scroll::-webkit-scrollbar-thumb{background:#fff3;border-radius:2px}.sm-menu-item{display:flex;align-items:center;padding:10px 15px;margin:2px 15px;border-radius:4px;cursor:pointer;color:#fff;font-size:13px;font-weight:400;letter-spacing:.3px;transition:all .15s ease;text-decoration:none;white-space:nowrap;overflow:hidden}.sm-menu-item:hover{background:#ffffff1f}.sm-menu-item.sm-active{background-color:#fff;color:#3c4858;box-shadow:0 4px 20px #00000024,0 7px 10px -5px #0003;font-weight:500}.sm-menu-item.sm-active .sm-menu-icon{color:#3c4858}.sm-menu-icon{font-size:20px!important;width:24px!important;height:24px!important;display:inline-flex!important;align-items:center;justify-content:center;margin-right:12px;flex-shrink:0;color:#fffc;transition:color .15s ease}.sm-menu-text{flex:1;overflow:hidden;text-overflow:ellipsis;transition:opacity .2s ease}.sm-caret{font-size:18px!important;width:18px!important;height:18px!important;transition:transform .3s cubic-bezier(.4,0,.2,1);flex-shrink:0;color:#fff9}.sm-caret.sm-caret-open{transform:rotate(180deg)}.sm-active .sm-caret{color:#3c4858}.sm-submenu{max-height:0;overflow:hidden;transition:max-height .35s cubic-bezier(.4,0,.2,1)}.sm-submenu.sm-submenu-open{max-height:1000px}.sm-submenu-item{display:flex;align-items:center;padding:8px 15px 8px 30px;margin:1px 15px;border-radius:4px;cursor:pointer;color:#fffc;font-size:12px;font-weight:400;transition:all .15s ease;white-space:nowrap;overflow:hidden}.sm-submenu-item:hover{background:#ffffff1f;color:#fff}.sm-submenu-item.sm-active{background-color:#fff;color:#3c4858;box-shadow:0 4px 20px #00000024,0 7px 10px -5px #0003;font-weight:500}.sm-submenu-item.sm-active .sm-sub-icon{color:#3c4858}.sm-sub-icon{font-size:16px!important;width:20px!important;height:20px!important;display:inline-flex!important;align-items:center;justify-content:center;margin-right:10px;flex-shrink:0;color:#fff9}.sm-initials{width:20px;height:20px;border-radius:50%;background:#ffffff26;display:inline-flex;align-items:center;justify-content:center;font-size:9px;font-weight:600;margin-right:10px;flex-shrink:0;color:#fffc}.sm-active .sm-initials{background:#3c48581f;color:#3c4858}.sm-main{flex:1;min-width:0;margin-left:260px;min-height:100vh;display:flex;flex-direction:column;transition:margin-left .3s cubic-bezier(.4,0,.2,1);background-color:#eef2f7}.sm-topbar{display:flex;align-items:center;padding:8px 16px;min-height:56px;background-color:#eef2f7;background-image:radial-gradient(circle,#d5dbe3 1px,transparent 1px);background-size:16px 16px;border-bottom:1px solid rgba(0,0,0,.08);position:sticky;top:0;z-index:1020;transition:background .3s ease,backdrop-filter .3s ease}.sm-topbar-scrolled{background-color:#eef2f78c;background-image:none;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);box-shadow:0 1px 3px #0000000f}.sm-topbar-spacer{flex:1 1 auto}.sm-topbar-logo{height:32px;width:32px;object-fit:contain;margin-right:8px}.sm-topbar-brand{font-size:18px;font-weight:500;margin-right:8px;white-space:nowrap}.sm-topbar-label{font-size:14px;margin-right:4px;display:inline-flex;align-items:center;align-self:center;height:40px;line-height:1}.sm-topbar .mat-mdc-icon-button{display:inline-flex!important;align-items:center!important;justify-content:center!important}.sm-content{flex:1;padding:12px;min-width:0;background-color:#e5eaf2;background-image:radial-gradient(ellipse at 50% 45%,#fffffff2,#fff6 35%,#fff0 60%),radial-gradient(circle,#bec7d4 1px,transparent 1px);background-size:100% 100%,16px 16px;min-height:calc(100vh - 104px)}.sm-footer{padding:12px 16px;text-align:center;font-size:12px;color:#999;border-top:1px solid #e0e0e0;background:#fff}.sm-footer a{color:inherit;cursor:pointer}.sm-footer a:hover{text-decoration:underline}.sm-backdrop{display:none;position:fixed;inset:0;background:#00000080;z-index:1025}.sm-layout.sm-mini .sm-sidebar{width:80px}.sm-layout.sm-mini .sm-main{margin-left:80px}.sm-layout.sm-mini .sm-brand-name,.sm-layout.sm-mini .sm-profile-info,.sm-layout.sm-mini .sm-menu-text,.sm-layout.sm-mini .sm-caret,.sm-layout.sm-mini .sm-submenu{display:none}.sm-layout.sm-mini .sm-sidebar mat-divider{margin:0 10px}.sm-layout.sm-mini .sm-brand{justify-content:center;padding:18px 0 10px}.sm-layout.sm-mini .sm-brand img{margin-right:0}.sm-layout.sm-mini .sm-profile{justify-content:center;padding:12px 0}.sm-layout.sm-mini .sm-profile-icon{margin-right:0}.sm-layout.sm-mini .sm-menu-item{justify-content:center;padding:12px 0;margin:2px 0}.sm-layout.sm-mini .sm-menu-icon{margin-right:0;font-size:22px!important}.sm-layout.sm-mini-hovered .sm-sidebar{width:260px;box-shadow:4px 0 20px #0000004d}.sm-layout.sm-mini-hovered .sm-main{margin-left:80px}.sm-layout.sm-mini-hovered .sm-brand-name,.sm-layout.sm-mini-hovered .sm-profile-info,.sm-layout.sm-mini-hovered .sm-menu-text,.sm-layout.sm-mini-hovered .sm-caret{display:initial}.sm-layout.sm-mini-hovered .sm-submenu{display:block}.sm-layout.sm-mini-hovered .sm-sidebar mat-divider{margin:0 15px}.sm-layout.sm-mini-hovered .sm-brand{justify-content:flex-start;padding:18px 15px 10px}.sm-layout.sm-mini-hovered .sm-brand img{margin-right:12px}.sm-layout.sm-mini-hovered .sm-profile{justify-content:flex-start;padding:12px 15px}.sm-layout.sm-mini-hovered .sm-profile-icon{margin-right:12px}.sm-layout.sm-mini-hovered .sm-menu-item{justify-content:flex-start;padding:10px 15px;margin:2px 15px}.sm-layout.sm-mini-hovered .sm-menu-icon{margin-right:12px;font-size:20px!important}@media (max-width: 600px){.sm-sidebar{transform:translate(-100%);transition:transform .3s cubic-bezier(.4,0,.2,1);width:260px!important}.sm-layout.sm-mobile-open .sm-sidebar{transform:translate(0)}.sm-layout.sm-mobile-open .sm-backdrop{display:block}.sm-main{margin-left:0!important}.sm-layout.sm-mini .sm-sidebar{width:260px!important}.sm-layout.sm-mini .sm-brand-name,.sm-layout.sm-mini .sm-profile-info,.sm-layout.sm-mini .sm-menu-text,.sm-layout.sm-mini .sm-caret{display:initial}.sm-layout.sm-mini .sm-submenu{display:block}.sm-layout.sm-mini .sm-sidebar mat-divider{margin:0 15px}.sm-layout.sm-mini .sm-menu-item{justify-content:flex-start;padding:10px 15px;margin:2px 15px}.sm-layout.sm-mini .sm-menu-icon{margin-right:12px;font-size:20px!important}.sm-layout.sm-mini .sm-brand{justify-content:flex-start;padding:18px 15px 10px}.sm-layout.sm-mini .sm-brand img{margin-right:12px}.sm-layout.sm-mini .sm-profile{justify-content:flex-start;padding:12px 15px}.sm-layout.sm-mini .sm-profile-icon{margin-right:12px}}.tm-navbar{position:sticky;top:0;z-index:1030;background-color:#03a;color:#fff;box-shadow:0 2px 12px #0000001f;transition:background-color .3s ease,backdrop-filter .3s ease,box-shadow .3s ease}.tm-navbar.tm-scrolled{background-color:#0033aad9;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);box-shadow:0 4px 18px #0000002e}.tm-bar{display:flex;align-items:center;flex-wrap:nowrap;min-height:60px;padding:6px 16px;gap:4px}.tm-brand{display:flex;align-items:center;gap:12px;flex-shrink:0;margin-right:18px;cursor:pointer;-webkit-user-select:none;user-select:none}.tm-logo{height:40px;width:auto;object-fit:contain}.tm-app-name{font-size:20px;font-weight:500;line-height:1.2;white-space:nowrap}.tm-tenant{font-size:12px;font-weight:400;color:#ffffffb3;line-height:1.2}.tm-toggler{display:none;margin-left:auto;background:transparent;border:1px solid rgba(255,255,255,.4);border-radius:8px;color:#fff;cursor:pointer;align-items:center;justify-content:center;width:40px;height:40px}.tm-toggler mat-icon{color:#fff}.tm-menu{display:flex;align-items:center;flex-wrap:wrap;flex:1 1 auto;min-width:0;row-gap:4px;justify-content:flex-end}.tm-item-wrap{display:flex;align-items:center;position:relative}.tm-item-wrap:not(:first-child):before{content:\"\";width:1px;height:18px;background:#ffffff2e;margin:0 2px;flex-shrink:0}.tm-item{position:relative;display:inline-flex;align-items:center;gap:6px;height:40px;padding:0 14px;margin:0 2px;background:transparent;border:none;border-radius:8px;color:#ffffffeb;font-size:14px;font-weight:400;letter-spacing:.2px;white-space:nowrap;cursor:pointer;transition:background .18s ease,color .18s ease}.tm-item:after{content:\"\";position:absolute;left:12px;right:12px;bottom:5px;height:1px;border-radius:1px;background:#ffffff80;transform:scaleX(0);transform-origin:center;transition:transform .25s cubic-bezier(.4,0,.2,1)}.tm-item:hover{color:#fff}.tm-item:hover:after{transform:scaleX(1)}.tm-item.tm-item-active:after{transform:scaleX(1)}.tm-item-icon{font-size:19px!important;width:19px!important;height:19px!important;display:inline-flex!important;align-items:center;justify-content:center;color:inherit!important}.tm-item-caret{font-size:18px!important;width:18px!important;height:18px!important;display:inline-flex!important;align-items:center;justify-content:center;margin-left:-2px;margin-right:-4px;color:#ffffffb3!important;transition:transform .2s ease}.tm-item:hover .tm-item-caret,.tm-item-active .tm-item-caret{color:#fff!important}.tm-actions{display:flex;align-items:center;gap:2px;flex-shrink:0;margin-left:auto}.tm-actions .mat-mdc-icon-button,.tm-action-btn{display:inline-flex!important;align-items:center!important;justify-content:center!important;color:#fff!important}.tm-actions mat-icon{color:#fff!important}.tm-divider-v{width:1px;height:24px;background:#ffffff38;margin:0 6px;flex-shrink:0}.tm-user-btn{display:inline-flex!important;align-items:center!important;gap:6px;height:40px;color:#fff!important;border-radius:8px;transition:background .18s ease}.tm-user-btn:hover{background:#ffffff1f}.tm-user-icon{font-size:24px!important;width:24px!important;height:24px!important;color:#fff!important}.tm-user-name{font-size:14px;font-weight:400;white-space:nowrap}::ng-deep .tm-submenu-panel .tm-sub-active{background:#0033aa14;font-weight:600;color:#03a}::ng-deep .tm-submenu-panel .tm-sub-active .mat-icon{color:#03a}@media (max-width: 991px){.tm-toggler{display:inline-flex}.tm-menu,.tm-actions{display:none;position:absolute;left:0;right:0;top:100%;flex-direction:column;align-items:stretch;background:#03a;padding:8px 12px;box-shadow:0 8px 18px #0003;z-index:1029}.tm-menu.tm-menu-open{display:flex}.tm-actions.tm-actions-open{display:flex;top:100%;border-top:1px solid rgba(255,255,255,.12)}.tm-item-wrap{width:100%}.tm-item-wrap:not(:first-child):before{width:100%;height:1px;margin:2px 0}.tm-item{width:100%;justify-content:flex-start;height:44px;margin:0}.tm-item:after{inset:8px auto 8px 0;width:4px;height:auto;transform:scaleY(0);transform-origin:center}.tm-item:hover:after,.tm-item.tm-item-active:after{transform:scaleY(1)}.tm-item-caret{margin-left:auto}.tm-divider-v{display:none}.tm-user-btn{justify-content:flex-start;width:100%}}@media (max-width: 600px){.has-bottom-tabs{padding-bottom:calc(64px + env(safe-area-inset-bottom,0px))!important}.sm-footer{display:none}}@media (max-width: 700px){.sm-content,.container-fluid.tin-bg-image,mat-sidenav-content.tin-bg-image{padding-left:10px!important;padding-right:10px!important}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i4$2.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i4$2.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i4$2.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "directive", type: i4$3.MatBadge, selector: "[matBadge]", inputs: ["matBadgeColor", "matBadgeOverlap", "matBadgeDisabled", "matBadgePosition", "matBadge", "matBadgeDescription", "matBadgeSize", "matBadgeHidden"] }, { kind: "component", type: i2$1.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: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i17.MatNavList, selector: "mat-nav-list", exportAs: ["matNavList"] }, { kind: "component", type: i17.MatListItem, selector: "mat-list-item, a[mat-list-item], button[mat-list-item]", inputs: ["activated"], exportAs: ["matListItem"] }, { kind: "component", type: i17.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: i7$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: i19$1.MatSidenav, selector: "mat-sidenav", inputs: ["fixedInViewport", "fixedTopGap", "fixedBottomGap"], exportAs: ["matSidenav"] }, { kind: "component", type: i19$1.MatSidenavContainer, selector: "mat-sidenav-container", exportAs: ["matSidenavContainer"] }, { kind: "component", type: i19$1.MatSidenavContent, selector: "mat-sidenav-content" }, { kind: "component", type: i20$1.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { kind: "directive", type: i1$1.RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "directive", type: i1$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: i21.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i21.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "component", type: LoaderComponent, selector: "spa-loader", inputs: ["logo"] }, { kind: "component", type: ToastComponent, selector: "spa-toast" }, { kind: "component", type: AgentComponent, selector: "spa-agent", inputs: ["pageMode"] }, { kind: "component", type: BottomTabsComponent, selector: "spa-bottom-tabs" }, { kind: "component", type: OfflineIndicatorComponent, selector: "spa-offline-indicator" }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }] }); }
|
|
28289
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: NavMenuComponent, isStandalone: false, selector: "spa-nav-menu", inputs: { appConfig: "appConfig", footer: "footer" }, host: { listeners: { "window:scroll": "onWindowScroll()" } }, ngImport: i0, template: "<header *ngIf=\"loggedin && dataService.appConfig.navigation == 'top'\">\n\n <!-- Changed: Removed mb-3 class to eliminate gap between toolbar and content -->\n <nav class=\"toolbar navbar navbar-expand-sm navbar-toggleable-sm navbar-light border-bottom box-shadow\" style=\"padding-right: 10px;\">\n\n\n <div class=\"container-fluid\" style=\"padding-right: 0px;\">\n\n <img *ngIf=\"appConfig.logo!=''\" [src]=\"appConfig.logo\" style=\"height: 50px; margin-right: 2em\" />\n\n <div>\n <!-- <div style=\"font-size: 20px;\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px;\">\n {{tenantName}}\n </div> -->\n\n <div *ngIf=\"!dataService.appConfig.multitenant\" style=\"font-size: 22px;\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant\" style=\"font-size: 20px; ; font-weight: 400;\" [ngStyle]=\"{'margin-top': dataService.appConfig.multitenant ? '12px' : ''}\">\n {{appConfig.appName}}\n </div>\n\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px; margin-bottom: 5px;\">\n {{tenantName}}\n </div>\n\n </div>\n\n\n\n <button class=\"navbar-toggler\" type=\"button\" data-toggle=\"collapse\" data-target=\".navbar-collapse\" aria-label=\"Toggle navigation\" [attr.aria-expanded]=\"isExpanded\" (click)=\"toggle()\">\n <span class=\"navbar-toggler-icon\"></span>\n </button>\n\n <div *ngIf=\"myRole\" class=\" navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse stack-top\" style=\"margin-right: 0px;\" [ngClass]=\"{ show: isExpanded, navitems: isExpanded }\" >\n\n <button mat-icon-button (click)=\"logoff()\" > <mat-icon>logout</mat-icon> </button>\n\n <div *ngIf=\"dataService.appConfig.multitenant\">\n\n <button mat-icon-button (click)=\"redirectTo('home/tenancy/settings')\" > <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon> </button>\n\n <!-- Removed: Support icon \u2014 replaced by floating assistant chat widget -->\n </div>\n\n\n <button id=\"btnUser\" mat-button [matMenuTriggerFor]=\"profileMenu\" ><mat-icon style=\"font-size: 24px;\">account_circle</mat-icon> {{loggedUserFullName}}</button>\n\n <mat-menu #profileMenu=\"matMenu\">\n <button id=\"btnProfile\" mat-menu-item (click)=\"redirectTo('home/user/profile')\" >Profile</button>\n <button id=\"btnLogOff\" mat-menu-item (click)=\"logoff()\">Log Off</button>\n </mat-menu>\n\n <div *ngFor=\"let item of reversedCapItems\">\n\n <!-- Menu Item \u2014 Added: isFeatureAllowed check for plan-based gating -->\n <button id=\"btnMenu\" *ngIf=\"myRole[item.name] && !item.capSubItems && item.showMenu && isFeatureAllowed(item)\" mat-button (click)=\"redirectTo(item.link)\">{{item.display}}</button>\n\n <!-- Menu Item with Sub items ignored \u2014 Added: isFeatureAllowed check -->\n <button id=\"btnMenu\" *ngIf=\"myRole[item.name] && item.capSubItems && item.showMenu && item.ignoreSubsDisplay && isFeatureAllowed(item)\" mat-button (click)=\"redirectTo(item.link)\">{{item.display}}</button>\n\n <!-- Menu Item with Sub items to display \u2014 Added: isFeatureAllowed check -->\n <button id=\"btnMenu\" *ngIf=\"myRole[item.name] && item.capSubItems && item.showMenu && !item.ignoreSubsDisplay && isFeatureAllowed(item)\" mat-button [matMenuTriggerFor]=\"adminMenu\">{{item.display}}</button>\n\n\n <!-- Sub Menu Items \u2014 Added: isFeatureAllowed check on sub-items -->\n <mat-menu #adminMenu=\"matMenu\">\n\n <div *ngFor=\"let subItem of item.capSubItems\">\n\n <button *ngIf=\"myRole[subItem.name] && subItem.showMenu && isFeatureAllowed(subItem)\" mat-menu-item (click)=\"redirectTo(subItem.link)\">{{subItem.display}}</button>\n\n </div>\n\n </mat-menu>\n\n </div>\n\n </div>\n\n\n </div>\n\n </nav>\n\n</header>\n\n<!-- Changed: Removed top/bottom padding to eliminate gaps, but kept left/right padding for content spacing -->\n<div class=\"container-fluid tin-bg-image\" [class.has-bottom-tabs]=\"loggedin && smallScreen\" *ngIf=\"dataService.appConfig.navigation == 'top'\" style=\"padding: 12px 12px; margin: 0;\"> <!-- Changed: bottom padding so the fixed tab bar cannot cover the last row -->\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n\n\n\n<!-- ============================================================ -->\n<!-- TOP-MODERN (navigation == 'top-modern') -->\n<!-- Changed: Modernised horizontal top navigation -->\n<!-- ============================================================ -->\n<header *ngIf=\"loggedin && dataService.appConfig.navigation == 'top-modern'\">\n\n <nav class=\"tm-navbar\" [class.tm-scrolled]=\"topbarScrolled\">\n <div class=\"tm-bar\">\n\n <!-- Brand -->\n <div class=\"tm-brand\" (click)=\"modernNavigate('')\">\n <img *ngIf=\"appConfig.logo\" [src]=\"appConfig.logo\" class=\"tm-logo\" alt=\"logo\" />\n <div class=\"tm-brand-text\">\n <div class=\"tm-app-name\">{{appConfig.appName}}</div>\n <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" class=\"tm-tenant\">{{tenantName}}</div>\n </div>\n </div>\n\n <!-- Mobile toggle -->\n <button class=\"tm-toggler\" type=\"button\" aria-label=\"Toggle navigation\"\n [attr.aria-expanded]=\"isExpanded\" (click)=\"toggle()\">\n <mat-icon>{{isExpanded ? 'close' : 'menu'}}</mat-icon>\n </button>\n\n <!-- Menu items -->\n <div class=\"tm-menu\" [class.tm-menu-open]=\"isExpanded\">\n\n <ng-container *ngFor=\"let item of dataService.appConfig.capItems\">\n <div class=\"tm-item-wrap\"\n *ngIf=\"myRole[item.name] && item.showMenu && isFeatureAllowed(item)\">\n\n <!-- Simple item (no sub-items, or sub-items ignored for display) -->\n <button *ngIf=\"!item.capSubItems || item.ignoreSubsDisplay\"\n class=\"tm-item\"\n [class.tm-item-active]=\"isActiveRoute(item.link)\"\n (click)=\"modernNavigate(item.link)\">\n <mat-icon *ngIf=\"item.icon && item.icon != 'navigate_next'\" class=\"tm-item-icon\">{{item.icon}}</mat-icon>\n <span class=\"tm-item-text\">{{item.display}}</span>\n </button>\n\n <!-- Parent item with displayed sub-items -->\n <ng-container *ngIf=\"item.capSubItems && !item.ignoreSubsDisplay\">\n <button class=\"tm-item tm-item-parent\"\n [class.tm-item-active]=\"isParentActive(item)\"\n [matMenuTriggerFor]=\"tmSubMenu\">\n <mat-icon *ngIf=\"item.icon && item.icon != 'navigate_next'\" class=\"tm-item-icon\">{{item.icon}}</mat-icon>\n <span class=\"tm-item-text\">{{item.display}}</span>\n <!-- Changed: Caret signals this item has more items beneath it -->\n <mat-icon class=\"tm-item-caret\">expand_more</mat-icon>\n </button>\n\n <mat-menu #tmSubMenu=\"matMenu\" class=\"tm-submenu-panel\" [overlapTrigger]=\"false\" yPosition=\"below\">\n <ng-container *ngFor=\"let sub of getSubItems(item)\">\n <button *ngIf=\"myRole[sub.name] && sub.showMenu && isFeatureAllowed(sub)\"\n mat-menu-item\n [class.tm-sub-active]=\"isActiveRoute(sub.link)\"\n (click)=\"modernNavigate(sub.link)\">\n <mat-icon *ngIf=\"sub.icon && sub.icon != 'navigate_next'\">{{sub.icon}}</mat-icon>\n <span>{{sub.display}}</span>\n </button>\n </ng-container>\n </mat-menu>\n </ng-container>\n\n </div>\n </ng-container>\n\n </div>\n\n <!-- Right-side actions -->\n <div class=\"tm-actions\" [class.tm-actions-open]=\"isExpanded\">\n\n <ng-container *ngIf=\"dataService.appConfig.multitenant\">\n <button mat-icon-button class=\"tm-action-btn\" (click)=\"modernNavigate('home/tenancy/settings')\" matTooltip=\"Organisation Settings\">\n <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon>\n </button>\n <button *ngIf=\"setupService.enabled && ((setupCount$ | async) || 0) > 0 && !smallScreen\" mat-icon-button class=\"tm-action-btn\" (click)=\"modernNavigate('home/setup')\" matTooltip=\"Getting Started\"> <!-- Added: Setup readiness badge \u2014 hidden when complete or unconfigured -->\n <mat-icon [matBadge]=\"setupCount$ | async\" matBadgeColor=\"warn\" matBadgeSize=\"small\">rocket_launch</mat-icon>\n </button>\n <button *ngIf=\"!smallScreen\" mat-icon-button class=\"tm-action-btn\" (click)=\"modernNavigate('home/workflow/notifications')\" matTooltip=\"Notifications\">\n <mat-icon [matBadge]=\"notificationCount$ | async\" [matBadgeHidden]=\"(notificationCount$ | async) === 0\" matBadgeColor=\"warn\" matBadgeSize=\"small\">notifications</mat-icon>\n </button>\n <spa-offline-indicator *ngIf=\"!smallScreen\"></spa-offline-indicator> <!-- Changed: TinSync connection + pending-sync indicator -->\n </ng-container>\n\n <span class=\"tm-divider-v\"></span>\n\n <!-- Profile -->\n <button mat-button class=\"tm-user-btn\" [matMenuTriggerFor]=\"tmProfileMenu\">\n <mat-icon class=\"tm-user-icon\">account_circle</mat-icon>\n <span class=\"tm-user-name\">{{loggedUserFullName}}</span>\n </button>\n\n <mat-menu #tmProfileMenu=\"matMenu\" [overlapTrigger]=\"false\" yPosition=\"below\">\n <button mat-menu-item (click)=\"modernNavigate('home/user/profile')\">\n <mat-icon>person</mat-icon><span>Profile</span>\n </button>\n <mat-divider></mat-divider>\n <button mat-menu-item (click)=\"logoff()\">\n <mat-icon>logout</mat-icon><span>Log Off</span>\n </button>\n </mat-menu>\n\n <!-- Sign out \u2014 Changed: aligned via flex-centred action button -->\n <button mat-icon-button class=\"tm-action-btn tm-signout\" (click)=\"logoff()\" matTooltip=\"Sign Out\">\n <mat-icon>logout</mat-icon>\n </button>\n\n </div>\n\n </div>\n </nav>\n\n</header>\n\n<!-- Top-modern page content \u2014 Changed: use original 'top' background (tin-bg-image), no footer bar -->\n<div class=\"container-fluid tin-bg-image\" [class.has-bottom-tabs]=\"loggedin && smallScreen\" *ngIf=\"loggedin && dataService.appConfig.navigation == 'top-modern'\" style=\"padding: 12px 12px; margin: 0;\"> <!-- Changed: bottom padding so the fixed tab bar cannot cover the last row -->\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n<!-- Not logged in fallback for top-modern -->\n<div class=\"tin-bg-image\" *ngIf=\"!loggedin && dataService.appConfig.navigation == 'top-modern'\">\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n\n\n\n<!-- SIDE -->\n<mat-toolbar class=\"tin-bg-image-toolbar\" *ngIf=\"loggedin && dataService.appConfig.navigation == 'side'\" style=\"padding: 0px 8px;\">\n\n <button mat-icon-button (click)=\"toggle()\" matTooltip=\"Menu\">\n <mat-icon>menu</mat-icon>\n </button>\n\n <img [src]=\"dataService.appConfig.logo\" style=\"height: 50px;\" />\n\n <div style=\"padding-left: 10px; \">\n\n <div style=\"font-size: 22px; font-weight: 400;\">\n {{appConfig.appName}}\n </div>\n\n <!-- <div style=\"font-size: 20px; height: 25px; font-weight: 400;\" [ngStyle]=\"{'margin-top': dataService.appConfig.multitenant ? '12px' : ''}\">\n {{appConfig.appName}}\n </div> -->\n\n <!-- <div *ngIf=\"dataService.appConfig.multitenant && tenantName\" style=\"font-size: 12px; margin-bottom: 5px;\">\n {{tenantName}}\n </div> -->\n\n </div>\n\n\n\n <span class=\"toolbar-item-spacer\"></span>\n\n <!-- buttons -->\n\n <div *ngIf=\"dataService.appConfig.multitenant\" style=\"display: flex; align-items: center;\">\n\n <!-- <label style=\"font-size: 14px;\">Hi, {{loggedUserFullName}}</label> -->\n\n <button mat-icon-button (click)=\"redirectTo('home/tenancy/settings')\" matTooltip=\"Organisation Settings\">\n <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon>\n </button>\n <label style=\"font-size: 14px;margin-right: 20px;\">{{tenantName}}</label>\n\n <!-- Changed: Support/help icon removed \u2014 replaced by floating agent chat widget -->\n\n <button *ngIf=\"setupService.enabled && ((setupCount$ | async) || 0) > 0 && !smallScreen\" mat-icon-button (click)=\"redirectTo('home/setup')\" matTooltip=\"Getting Started\"> <!-- Added: Setup readiness badge \u2014 hidden when complete or unconfigured -->\n <mat-icon [matBadge]=\"setupCount$ | async\" matBadgeColor=\"warn\" matBadgeSize=\"small\">rocket_launch</mat-icon>\n </button>\n <button *ngIf=\"!smallScreen\" mat-icon-button (click)=\"redirectTo('home/workflow/notifications')\" matTooltip=\"Notifications\">\n <mat-icon [matBadge]=\"notificationCount$ | async\" [matBadgeHidden]=\"(notificationCount$ | async) === 0\" matBadgeColor=\"warn\" matBadgeSize=\"small\">notifications</mat-icon>\n </button>\n\n <spa-offline-indicator *ngIf=\"!smallScreen\"></spa-offline-indicator> <!-- Changed: TinSync connection + pending-sync indicator -->\n\n </div>\n\n\n\n <button mat-icon-button matTooltip=\"My Account\" [matMenuTriggerFor]=\"userAccountMenu\"><mat-icon>account_circle</mat-icon></button>\n <label style=\"font-size: 14px;\">{{loggedUserFullName}}</label>\n\n <button *ngIf=\"!smallScreen\" mat-icon-button (click)=\"logoff()\" matTooltip=\"Signout\">\n <mat-icon>logout</mat-icon>\n </button>\n\n\n <!-- my account menu -->\n <mat-menu #userAccountMenu [overlapTrigger]=\"false\" yPosition=\"below\">\n\n\n <button mat-menu-item routerLink=\"home/user/profile\">\n <mat-icon>person</mat-icon><span>Profile</span>\n </button>\n\n <!-- Removed: Help menu item \u2014 replaced by floating assistant chat widget -->\n\n <mat-divider></mat-divider>\n\n <button mat-menu-item (click)=\"logoff()\">\n <mat-icon>logout</mat-icon>Logout\n </button>\n\n </mat-menu>\n\n</mat-toolbar>\n\n\n\n\n<mat-sidenav-container class=\"app-container\" [hasBackdrop]=\"smallScreen\" *ngIf=\"loggedin && dataService.appConfig.navigation == 'side'\">\n\n <mat-sidenav #sidenav [mode]=\"smallScreen ? 'over' : 'side'\" [class.mat-elevation-z4]=\"true\" [opened]=\"isExpanded\" class=\"app-sidenav side-color\" style=\"height: 100%;\"\n [ngStyle]=\"{'width': dataService.appConfig.navWidth}\">\n <mat-nav-list >\n\n <ng-container *ngFor=\"let cap of dataService.appConfig.capItems\" >\n\n <!-- Menu item \u2014 Added: isFeatureAllowed check for plan-based gating -->\n <mat-list-item [routerLink]=\"cap.link\" *ngIf=\"myRole[cap.name] && cap.showMenu && (!cap.capSubItems || cap.capSubItems && cap.ignoreSubsDisplay) && isFeatureAllowed(cap)\" style=\"height: 40px;font-size: 15px;\"\n (click)=\"smallScreen ? toggle() : null\">\n <mat-icon [ngStyle]=\"{'color': cap.color}\" style=\"margin-right: 5px;\">{{cap.icon}}</mat-icon>{{cap.display}}\n </mat-list-item>\n\n <!-- Menu With Sub items \u2014 Added: isFeatureAllowed check -->\n <mat-expansion-panel class=\"side-color\" [class.mat-elevation-z0]=\"true\" *ngIf=\"myRole[cap.name] && cap.showMenu && cap.capSubItems && !cap.ignoreSubsDisplay && isFeatureAllowed(cap)\">\n\n <mat-expansion-panel-header style=\"height: 40px;padding-left: 15px;\">\n <mat-icon [ngStyle]=\"{'color': cap.color}\" style=\"margin-right: 5px;\">{{cap.icon != 'navigate_next' ? cap.icon : 'fiber_manual_record' }}</mat-icon>{{cap.display}}\n </mat-expansion-panel-header>\n\n <!-- Sub items - Changed: Use ng-container to avoid blank spaces for hidden items -->\n <mat-nav-list>\n <ng-container *ngFor=\"let capSub of getSubItems(cap)\">\n <mat-list-item [routerLink]=\"capSub.link\" style=\"height: 30px; font-size: 15px; padding-left: 4px; padding-right: 10px; margin-bottom: 5px;\" (click)=\"smallScreen ? toggle() : null\" *ngIf=\"myRole[capSub.name] && capSub.showMenu && isFeatureAllowed(capSub)\" [matTooltip]=\"capSub.display\" matTooltipPosition=\"right\">\n <mat-icon [ngStyle]=\"{'color': capSub.color}\" style=\"margin-right: 5px;\">{{capSub.icon}}</mat-icon>{{capSub.display}}\n </mat-list-item>\n </ng-container>\n </mat-nav-list>\n\n </mat-expansion-panel>\n\n </ng-container>\n\n </mat-nav-list>\n </mat-sidenav>\n\n\n\n <mat-sidenav-content class=\"tin-bg-image\" [class.has-bottom-tabs]=\"loggedin && smallScreen\" style=\"padding: 0px 12px;\" *ngIf=\"loggedin && dataService.appConfig.navigation == 'side'\"> <!-- Changed: bottom padding so the fixed tab bar cannot cover the last row -->\n <hr style=\"margin-top: 0px;\">\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n </mat-sidenav-content>\n\n</mat-sidenav-container>\n\n\n<!-- footer -->\n<!-- Changed: hidden on mobile \u2014 the bottom tab bar owns the bottom edge there, and a footer under a fixed\n bar is either invisible or a second competing strip of chrome -->\n<div class=\"tin-center\" *ngIf=\"loggedin && !smallScreen && dataService.appConfig.navigation == 'side'\">\n <label style=\"text-align: center; font-size: 12px;\">© {{nowDate | date : 'yyyy'}} <a color=\"primary\" class=\"terms-link\" [href]=\"appConfig.siteUrl\" target=\"_blank\">{{footer}}</a> | <a color=\"primary\" class=\"terms-link\" style=\"cursor: pointer;\" (click)=\"openTerms()\">Terms</a> | <a color=\"primary\" class=\"terms-link\" style=\"cursor: pointer;\" (click)=\"openPrivacy()\">Privacy Policy</a></label>\n</div>\n\n\n<div class=\"tin-bg-image\" *ngIf=\"!loggedin && dataService.appConfig.navigation == 'side'\">\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n\n<!-- SIDE-MODERN -->\n\n<!-- Changed: Side-modern navigation layout -->\n<div class=\"sm-layout\"\n *ngIf=\"loggedin && dataService.appConfig.navigation == 'side-modern'\"\n [class.sm-mini]=\"isMiniSidebar && !isMiniHovered\"\n [class.sm-mini-hovered]=\"isMiniSidebar && isMiniHovered\"\n [class.sm-mobile-open]=\"smallScreen && isExpanded\">\n\n <!-- Sidebar -->\n <aside class=\"sm-sidebar\"\n (mouseenter)=\"onMiniMouseEnter()\"\n (mouseleave)=\"onMiniMouseLeave()\">\n\n <!-- Background layers -->\n <div class=\"sm-sidebar-bg\">\n <div class=\"sm-sidebar-bg-image\" *ngIf=\"appConfig.navImage\" [ngStyle]=\"{'background-image': 'url(' + appConfig.navImage + ')'}\"></div>\n <div class=\"sm-sidebar-bg-overlay\" [ngStyle]=\"{'background-color': appConfig.navColor}\"></div>\n </div>\n\n <!-- Sidebar content -->\n <div class=\"sm-sidebar-content\">\n\n <!-- Brand -->\n <div class=\"sm-brand\">\n <img *ngIf=\"appConfig.logo\" [src]=\"appConfig.logo\" alt=\"logo\" />\n <span class=\"sm-brand-name\">{{appConfig.appName}}</span>\n </div>\n\n <mat-divider></mat-divider>\n\n <!-- Profile -->\n <div class=\"sm-profile\">\n <mat-icon class=\"sm-profile-icon\">account_circle</mat-icon>\n <div class=\"sm-profile-info\">\n <div class=\"sm-profile-name\">{{loggedUserFullName}}</div>\n <div class=\"sm-profile-role\">{{tenantName || 'User'}}</div>\n </div>\n </div>\n\n <mat-divider></mat-divider>\n\n <!-- Scrollable menu -->\n <div class=\"sm-menu-scroll\">\n\n <ng-container *ngFor=\"let cap of dataService.appConfig.capItems\">\n\n <!-- Simple menu item (no sub-items or ignoring sub display) \u2014 Added: isFeatureAllowed check -->\n <div *ngIf=\"myRole[cap.name] && cap.showMenu && (!cap.capSubItems || cap.ignoreSubsDisplay) && isFeatureAllowed(cap)\"\n class=\"sm-menu-item\"\n [class.sm-active]=\"isActiveRoute(cap.link)\"\n (click)=\"modernNavigate(cap.link)\">\n <mat-icon class=\"sm-menu-icon\">{{cap.icon != 'navigate_next' ? cap.icon : 'dashboard'}}</mat-icon>\n <span class=\"sm-menu-text\">{{cap.display}}</span>\n </div>\n\n <!-- Parent menu item with sub-items \u2014 Added: isFeatureAllowed check -->\n <ng-container *ngIf=\"myRole[cap.name] && cap.showMenu && cap.capSubItems && !cap.ignoreSubsDisplay && isFeatureAllowed(cap)\">\n\n <!-- Parent item (toggles sub-menu) -->\n <div class=\"sm-menu-item\"\n [class.sm-active]=\"isParentActive(cap) && !isMenuOpen(cap.name)\"\n (click)=\"toggleModernMenu(cap.name)\">\n <mat-icon class=\"sm-menu-icon\">{{cap.icon != 'navigate_next' ? cap.icon : 'dashboard'}}</mat-icon>\n <span class=\"sm-menu-text\">{{cap.display}}</span>\n <mat-icon class=\"sm-caret\" [class.sm-caret-open]=\"isMenuOpen(cap.name)\">expand_more</mat-icon>\n </div>\n\n <!-- Sub-menu container (animated) -->\n <div class=\"sm-submenu\" [class.sm-submenu-open]=\"isMenuOpen(cap.name)\">\n <ng-container *ngFor=\"let sub of getSubItems(cap)\">\n <div *ngIf=\"myRole[sub.name] && sub.showMenu && isFeatureAllowed(sub)\"\n class=\"sm-submenu-item\"\n [class.sm-active]=\"isActiveRoute(sub.link)\"\n (click)=\"modernNavigate(sub.link)\">\n <mat-icon *ngIf=\"sub.icon && sub.icon != 'navigate_next'\" class=\"sm-sub-icon\">{{sub.icon}}</mat-icon>\n <span *ngIf=\"!sub.icon || sub.icon == 'navigate_next'\" class=\"sm-initials\">{{getInitials(sub.display)}}</span>\n <span class=\"sm-menu-text\">{{sub.display}}</span>\n </div>\n </ng-container>\n </div>\n\n </ng-container>\n\n </ng-container>\n\n </div>\n\n </div>\n </aside>\n\n <!-- Mobile backdrop -->\n <div class=\"sm-backdrop\" (click)=\"isExpanded = false\"></div>\n\n <!-- Main content -->\n <div class=\"sm-main\">\n\n <!-- Top bar - Changed: Added scroll class for frosted glass effect -->\n <div class=\"sm-topbar\" [class.sm-topbar-scrolled]=\"topbarScrolled\">\n <button mat-icon-button (click)=\"smallScreen ? toggle() : toggleMiniSidebar()\" matTooltip=\"Menu\">\n <mat-icon>menu</mat-icon>\n </button>\n\n <!-- Changed: Mobile branding - show logo + app name when sidebar is hidden on small screens -->\n <img *ngIf=\"smallScreen && appConfig.logo\" [src]=\"appConfig.logo\" alt=\"logo\" class=\"sm-topbar-logo\" />\n <span *ngIf=\"smallScreen\" class=\"sm-topbar-brand\">{{appConfig.appName}}</span>\n\n <span class=\"sm-topbar-spacer\"></span>\n\n <!-- Multitenant buttons -->\n <div *ngIf=\"dataService.appConfig.multitenant\" style=\"display: flex; align-items: center;\">\n <button mat-icon-button (click)=\"redirectTo('home/tenancy/settings')\" matTooltip=\"Organisation Settings\">\n <mat-icon fontSet=\"material-icons-round\">apartment</mat-icon>\n </button>\n <span class=\"sm-topbar-label\">{{tenantName}}</span>\n\n <!-- Changed: Support/help icon removed \u2014 replaced by floating agent chat widget -->\n\n <button *ngIf=\"setupService.enabled && ((setupCount$ | async) || 0) > 0 && !smallScreen\" mat-icon-button (click)=\"redirectTo('home/setup')\" matTooltip=\"Getting Started\"> <!-- Added: Setup readiness badge \u2014 hidden when complete or unconfigured -->\n <mat-icon [matBadge]=\"setupCount$ | async\" matBadgeColor=\"warn\" matBadgeSize=\"small\">rocket_launch</mat-icon>\n </button>\n <button *ngIf=\"!smallScreen\" mat-icon-button (click)=\"redirectTo('home/workflow/notifications')\" matTooltip=\"Notifications\">\n <mat-icon [matBadge]=\"notificationCount$ | async\" [matBadgeHidden]=\"(notificationCount$ | async) === 0\" matBadgeColor=\"warn\" matBadgeSize=\"small\">notifications</mat-icon>\n </button>\n <spa-offline-indicator *ngIf=\"!smallScreen\"></spa-offline-indicator> <!-- Changed: TinSync connection + pending-sync indicator -->\n </div>\n\n <!-- Profile menu -->\n <button mat-icon-button matTooltip=\"My Account\" [matMenuTriggerFor]=\"smProfileMenu\">\n <mat-icon>account_circle</mat-icon>\n </button>\n <span class=\"sm-topbar-label\">{{loggedUserFullName}}</span>\n\n <mat-menu #smProfileMenu=\"matMenu\" [overlapTrigger]=\"false\" yPosition=\"below\">\n <button mat-menu-item routerLink=\"home/user/profile\">\n <mat-icon>person</mat-icon><span>Profile</span>\n </button>\n <!-- Changed: Help menu item removed \u2014 replaced by floating agent chat widget -->\n <mat-divider></mat-divider>\n <button mat-menu-item (click)=\"logoff()\">\n <mat-icon>logout</mat-icon>Logout\n </button>\n </mat-menu>\n\n <button *ngIf=\"!smallScreen\" mat-icon-button (click)=\"logoff()\" matTooltip=\"Signout\">\n <mat-icon>logout</mat-icon>\n </button>\n </div>\n\n <!-- Page content - Changed: Replaced tin-bg-image with sm-content modern texture -->\n <div class=\"sm-content\" [class.has-bottom-tabs]=\"loggedin && smallScreen\"> <!-- Changed: bottom padding so the fixed tab bar cannot cover the last row -->\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n </div>\n\n <!-- Footer -->\n <div class=\"sm-footer\">\n © {{nowDate | date : 'yyyy'}} <a [href]=\"appConfig.siteUrl\" target=\"_blank\">{{footer}}</a> | <a (click)=\"openTerms()\">Terms</a> | <a (click)=\"openPrivacy()\">Privacy Policy</a>\n </div>\n\n </div>\n\n</div>\n\n<!-- Not logged in fallback for side-modern -->\n<div class=\"tin-bg-image\" *ngIf=\"!loggedin && dataService.appConfig.navigation == 'side-modern'\">\n <router-outlet></router-outlet>\n <spa-loader [logo]=\"this.dataService.appConfig.logo\"></spa-loader>\n</div>\n\n<!-- Changed: Cascading toast notifications for real-time entity changes \u2014 visible in all layouts -->\n<spa-toast *ngIf=\"loggedin && dataService.appConfig.multitenant\"></spa-toast>\n\n<!-- Changed: Floating agent chat widget \u2014 renamed from spa-assistant -->\n<!-- Changed (Setup v3): also gated on the \"agent\" module. The widget is floating, not a nav item, so\n CapItem.moduleKey could never reach it \u2014 a tenant who switched the assistant off in Getting Started\n still had it hovering over every page. -->\n<spa-agent *ngIf=\"loggedin && dataService.appConfig.multitenant && setupService.isModuleEnabled('agent')\"></spa-agent>\n\n<!-- Added: mobile bottom tab bar. Mounted ONCE here, outside every layout block, because the bar is\n position: fixed and so serves top, top-modern, side and side-modern from this single instance.\n smallScreen is nav-menu's existing (max-width: 600px) BreakpointObserver \u2014 the one source of truth\n for this feature, so no second breakpoint is introduced. Never renders on desktop. -->\n<spa-bottom-tabs *ngIf=\"loggedin && smallScreen\"></spa-bottom-tabs>\n", styles: ["a.navbar-brand{white-space:normal;text-align:center;word-break:break-all}html{font-size:14px}.box-shadow{box-shadow:0 .25rem .75rem #0000000d}.toolbar-item-spacer{flex:1 1 auto}.toolbar{height:60px;display:flex;align-items:center;background-color:#03a;color:#fff;margin-bottom:0!important}.toolbar button,.toolbar .mat-mdc-button,.toolbar .mat-mdc-icon-button{color:#fff!important}.toolbar mat-icon{color:#fff!important}.stack-top{z-index:9;margin:20px}.navitems{background-color:#03a}.app-container{height:90%;margin:0}.app-sidenav{width:200px;border:1px solid rgb(192,190,199)}.side-color{background-color:#e6f4ff}.app-sidenav mat-list-item{display:flex!important;align-items:center!important}.app-sidenav mat-icon{display:inline-flex!important;align-items:center!important;vertical-align:middle!important}.app-sidenav mat-expansion-panel-header mat-icon{display:inline-flex!important;align-items:center!important;vertical-align:middle!important}::ng-deep .app-sidenav .mat-expansion-panel-body{padding-bottom:5px!important;padding-right:5px!important}::ng-deep .app-sidenav .mdc-list{padding-bottom:0!important}.sm-layout{display:flex;min-height:100vh;position:relative}.sm-sidebar{position:fixed;top:0;left:0;bottom:0;width:260px;z-index:1030;overflow:hidden;transition:width .3s cubic-bezier(.4,0,.2,1)}.sm-sidebar-bg{position:absolute;inset:0;z-index:0}.sm-sidebar-bg-image{position:absolute;inset:0;background-size:cover;background-position:center}.sm-sidebar-bg-overlay{position:absolute;inset:0}.sm-sidebar-content{position:relative;z-index:1;display:flex;flex-direction:column;height:100%;color:#fff}.sm-brand{display:flex;align-items:center;padding:18px 15px 10px;min-height:60px;text-decoration:none;white-space:nowrap;overflow:hidden}.sm-brand img{height:34px;width:34px;object-fit:contain;margin-right:12px;flex-shrink:0}.sm-brand-name{font-size:16px;font-weight:500;letter-spacing:.5px;color:#fff;overflow:hidden;text-overflow:ellipsis;transition:opacity .2s ease}.sm-profile{display:flex;align-items:center;padding:12px 15px;white-space:nowrap;overflow:hidden}.sm-profile-icon{font-size:34px!important;width:34px!important;height:34px!important;margin-right:12px;flex-shrink:0;color:#fffc}.sm-profile-info{overflow:hidden;transition:opacity .2s ease}.sm-profile-name{font-size:14px;font-weight:500;color:#fff;line-height:1.3;overflow:hidden;text-overflow:ellipsis}.sm-profile-role{font-size:11px;color:#fff9;line-height:1.3;overflow:hidden;text-overflow:ellipsis}.sm-sidebar mat-divider{border-color:#ffffff26!important;margin:0 15px}.sm-menu-scroll{flex:1;overflow-y:auto;overflow-x:hidden;padding:8px 0}.sm-menu-scroll::-webkit-scrollbar{width:4px}.sm-menu-scroll::-webkit-scrollbar-track{background:transparent}.sm-menu-scroll::-webkit-scrollbar-thumb{background:#fff3;border-radius:2px}.sm-menu-item{display:flex;align-items:center;padding:10px 15px;margin:2px 15px;border-radius:4px;cursor:pointer;color:#fff;font-size:13px;font-weight:400;letter-spacing:.3px;transition:all .15s ease;text-decoration:none;white-space:nowrap;overflow:hidden}.sm-menu-item:hover{background:#ffffff1f}.sm-menu-item.sm-active{background-color:#fff;color:#3c4858;box-shadow:0 4px 20px #00000024,0 7px 10px -5px #0003;font-weight:500}.sm-menu-item.sm-active .sm-menu-icon{color:#3c4858}.sm-menu-icon{font-size:20px!important;width:24px!important;height:24px!important;display:inline-flex!important;align-items:center;justify-content:center;margin-right:12px;flex-shrink:0;color:#fffc;transition:color .15s ease}.sm-menu-text{flex:1;overflow:hidden;text-overflow:ellipsis;transition:opacity .2s ease}.sm-caret{font-size:18px!important;width:18px!important;height:18px!important;transition:transform .3s cubic-bezier(.4,0,.2,1);flex-shrink:0;color:#fff9}.sm-caret.sm-caret-open{transform:rotate(180deg)}.sm-active .sm-caret{color:#3c4858}.sm-submenu{max-height:0;overflow:hidden;transition:max-height .35s cubic-bezier(.4,0,.2,1)}.sm-submenu.sm-submenu-open{max-height:1000px}.sm-submenu-item{display:flex;align-items:center;padding:8px 15px 8px 30px;margin:1px 15px;border-radius:4px;cursor:pointer;color:#fffc;font-size:12px;font-weight:400;transition:all .15s ease;white-space:nowrap;overflow:hidden}.sm-submenu-item:hover{background:#ffffff1f;color:#fff}.sm-submenu-item.sm-active{background-color:#fff;color:#3c4858;box-shadow:0 4px 20px #00000024,0 7px 10px -5px #0003;font-weight:500}.sm-submenu-item.sm-active .sm-sub-icon{color:#3c4858}.sm-sub-icon{font-size:16px!important;width:20px!important;height:20px!important;display:inline-flex!important;align-items:center;justify-content:center;margin-right:10px;flex-shrink:0;color:#fff9}.sm-initials{width:20px;height:20px;border-radius:50%;background:#ffffff26;display:inline-flex;align-items:center;justify-content:center;font-size:9px;font-weight:600;margin-right:10px;flex-shrink:0;color:#fffc}.sm-active .sm-initials{background:#3c48581f;color:#3c4858}.sm-main{flex:1;min-width:0;margin-left:260px;min-height:100vh;display:flex;flex-direction:column;transition:margin-left .3s cubic-bezier(.4,0,.2,1);background-color:#eef2f7}.sm-topbar{display:flex;align-items:center;padding:8px 16px;min-height:56px;background-color:#eef2f7;background-image:radial-gradient(circle,#d5dbe3 1px,transparent 1px);background-size:16px 16px;border-bottom:1px solid rgba(0,0,0,.08);position:sticky;top:0;z-index:1020;transition:background .3s ease,backdrop-filter .3s ease}.sm-topbar-scrolled{background-color:#eef2f78c;background-image:none;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);box-shadow:0 1px 3px #0000000f}.sm-topbar-spacer{flex:1 1 auto}.sm-topbar-logo{height:32px;width:32px;object-fit:contain;margin-right:8px}.sm-topbar-brand{font-size:18px;font-weight:500;margin-right:8px;white-space:nowrap}.sm-topbar-label{font-size:14px;margin-right:4px;display:inline-flex;align-items:center;align-self:center;height:40px;line-height:1}.sm-topbar .mat-mdc-icon-button{display:inline-flex!important;align-items:center!important;justify-content:center!important}.sm-content{flex:1;padding:12px;min-width:0;background-color:#e5eaf2;background-image:radial-gradient(ellipse at 50% 45%,#fffffff2,#fff6 35%,#fff0 60%),radial-gradient(circle,#bec7d4 1px,transparent 1px);background-size:100% 100%,16px 16px;min-height:calc(100vh - 104px)}.sm-footer{padding:12px 16px;text-align:center;font-size:12px;color:#999;border-top:1px solid #e0e0e0;background:#fff}.sm-footer a{color:inherit;cursor:pointer}.sm-footer a:hover{text-decoration:underline}.sm-backdrop{display:none;position:fixed;inset:0;background:#00000080;z-index:1025}.sm-layout.sm-mini .sm-sidebar{width:80px}.sm-layout.sm-mini .sm-main{margin-left:80px}.sm-layout.sm-mini .sm-brand-name,.sm-layout.sm-mini .sm-profile-info,.sm-layout.sm-mini .sm-menu-text,.sm-layout.sm-mini .sm-caret,.sm-layout.sm-mini .sm-submenu{display:none}.sm-layout.sm-mini .sm-sidebar mat-divider{margin:0 10px}.sm-layout.sm-mini .sm-brand{justify-content:center;padding:18px 0 10px}.sm-layout.sm-mini .sm-brand img{margin-right:0}.sm-layout.sm-mini .sm-profile{justify-content:center;padding:12px 0}.sm-layout.sm-mini .sm-profile-icon{margin-right:0}.sm-layout.sm-mini .sm-menu-item{justify-content:center;padding:12px 0;margin:2px 0}.sm-layout.sm-mini .sm-menu-icon{margin-right:0;font-size:22px!important}.sm-layout.sm-mini-hovered .sm-sidebar{width:260px;box-shadow:4px 0 20px #0000004d}.sm-layout.sm-mini-hovered .sm-main{margin-left:80px}.sm-layout.sm-mini-hovered .sm-brand-name,.sm-layout.sm-mini-hovered .sm-profile-info,.sm-layout.sm-mini-hovered .sm-menu-text,.sm-layout.sm-mini-hovered .sm-caret{display:initial}.sm-layout.sm-mini-hovered .sm-submenu{display:block}.sm-layout.sm-mini-hovered .sm-sidebar mat-divider{margin:0 15px}.sm-layout.sm-mini-hovered .sm-brand{justify-content:flex-start;padding:18px 15px 10px}.sm-layout.sm-mini-hovered .sm-brand img{margin-right:12px}.sm-layout.sm-mini-hovered .sm-profile{justify-content:flex-start;padding:12px 15px}.sm-layout.sm-mini-hovered .sm-profile-icon{margin-right:12px}.sm-layout.sm-mini-hovered .sm-menu-item{justify-content:flex-start;padding:10px 15px;margin:2px 15px}.sm-layout.sm-mini-hovered .sm-menu-icon{margin-right:12px;font-size:20px!important}@media (max-width: 600px){.sm-sidebar{transform:translate(-100%);transition:transform .3s cubic-bezier(.4,0,.2,1);width:260px!important}.sm-layout.sm-mobile-open .sm-sidebar{transform:translate(0)}.sm-layout.sm-mobile-open .sm-backdrop{display:block}.sm-main{margin-left:0!important}.sm-layout.sm-mini .sm-sidebar{width:260px!important}.sm-layout.sm-mini .sm-brand-name,.sm-layout.sm-mini .sm-profile-info,.sm-layout.sm-mini .sm-menu-text,.sm-layout.sm-mini .sm-caret{display:initial}.sm-layout.sm-mini .sm-submenu{display:block}.sm-layout.sm-mini .sm-sidebar mat-divider{margin:0 15px}.sm-layout.sm-mini .sm-menu-item{justify-content:flex-start;padding:10px 15px;margin:2px 15px}.sm-layout.sm-mini .sm-menu-icon{margin-right:12px;font-size:20px!important}.sm-layout.sm-mini .sm-brand{justify-content:flex-start;padding:18px 15px 10px}.sm-layout.sm-mini .sm-brand img{margin-right:12px}.sm-layout.sm-mini .sm-profile{justify-content:flex-start;padding:12px 15px}.sm-layout.sm-mini .sm-profile-icon{margin-right:12px}}.tm-navbar{position:sticky;top:0;z-index:1030;background-color:#03a;color:#fff;box-shadow:0 2px 12px #0000001f;transition:background-color .3s ease,backdrop-filter .3s ease,box-shadow .3s ease}.tm-navbar.tm-scrolled{background-color:#0033aad9;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);box-shadow:0 4px 18px #0000002e}.tm-bar{display:flex;align-items:center;flex-wrap:nowrap;min-height:60px;padding:6px 16px;gap:4px}.tm-brand{display:flex;align-items:center;gap:12px;flex-shrink:0;margin-right:18px;cursor:pointer;-webkit-user-select:none;user-select:none}.tm-logo{height:40px;width:auto;object-fit:contain}.tm-app-name{font-size:20px;font-weight:500;line-height:1.2;white-space:nowrap}.tm-tenant{font-size:12px;font-weight:400;color:#ffffffb3;line-height:1.2}.tm-toggler{display:none;margin-left:auto;background:transparent;border:1px solid rgba(255,255,255,.4);border-radius:8px;color:#fff;cursor:pointer;align-items:center;justify-content:center;width:40px;height:40px}.tm-toggler mat-icon{color:#fff}.tm-menu{display:flex;align-items:center;flex-wrap:wrap;flex:1 1 auto;min-width:0;row-gap:4px;justify-content:flex-end}.tm-item-wrap{display:flex;align-items:center;position:relative}.tm-item-wrap:not(:first-child):before{content:\"\";width:1px;height:18px;background:#ffffff2e;margin:0 2px;flex-shrink:0}.tm-item{position:relative;display:inline-flex;align-items:center;gap:6px;height:40px;padding:0 14px;margin:0 2px;background:transparent;border:none;border-radius:8px;color:#ffffffeb;font-size:14px;font-weight:400;letter-spacing:.2px;white-space:nowrap;cursor:pointer;transition:background .18s ease,color .18s ease}.tm-item:after{content:\"\";position:absolute;left:12px;right:12px;bottom:5px;height:1px;border-radius:1px;background:#ffffff80;transform:scaleX(0);transform-origin:center;transition:transform .25s cubic-bezier(.4,0,.2,1)}.tm-item:hover{color:#fff}.tm-item:hover:after{transform:scaleX(1)}.tm-item.tm-item-active:after{transform:scaleX(1)}.tm-item-icon{font-size:19px!important;width:19px!important;height:19px!important;display:inline-flex!important;align-items:center;justify-content:center;color:inherit!important}.tm-item-caret{font-size:18px!important;width:18px!important;height:18px!important;display:inline-flex!important;align-items:center;justify-content:center;margin-left:-2px;margin-right:-4px;color:#ffffffb3!important;transition:transform .2s ease}.tm-item:hover .tm-item-caret,.tm-item-active .tm-item-caret{color:#fff!important}.tm-actions{display:flex;align-items:center;gap:2px;flex-shrink:0;margin-left:auto}.tm-actions .mat-mdc-icon-button,.tm-action-btn{display:inline-flex!important;align-items:center!important;justify-content:center!important;color:#fff!important}.tm-actions mat-icon{color:#fff!important}.tm-divider-v{width:1px;height:24px;background:#ffffff38;margin:0 6px;flex-shrink:0}.tm-user-btn{display:inline-flex!important;align-items:center!important;gap:6px;height:40px;color:#fff!important;border-radius:8px;transition:background .18s ease}.tm-user-btn:hover{background:#ffffff1f}.tm-user-icon{font-size:24px!important;width:24px!important;height:24px!important;color:#fff!important}.tm-user-name{font-size:14px;font-weight:400;white-space:nowrap}::ng-deep .tm-submenu-panel .tm-sub-active{background:#0033aa14;font-weight:600;color:#03a}::ng-deep .tm-submenu-panel .tm-sub-active .mat-icon{color:#03a}@media (max-width: 991px){.tm-toggler{display:inline-flex}.tm-menu,.tm-actions{display:none;position:absolute;left:0;right:0;top:100%;flex-direction:column;align-items:stretch;background:#03a;padding:8px 12px;box-shadow:0 8px 18px #0003;z-index:1029}.tm-menu.tm-menu-open{display:flex}.tm-actions.tm-actions-open{display:flex;top:100%;border-top:1px solid rgba(255,255,255,.12)}.tm-item-wrap{width:100%}.tm-item-wrap:not(:first-child):before{width:100%;height:1px;margin:2px 0}.tm-item{width:100%;justify-content:flex-start;height:44px;margin:0}.tm-item:after{inset:8px auto 8px 0;width:4px;height:auto;transform:scaleY(0);transform-origin:center}.tm-item:hover:after,.tm-item.tm-item-active:after{transform:scaleY(1)}.tm-item-caret{margin-left:auto}.tm-divider-v{display:none}.tm-user-btn{justify-content:flex-start;width:100%}}@media (max-width: 600px){.has-bottom-tabs{padding-bottom:calc(64px + env(safe-area-inset-bottom,0px))!important}.sm-footer{display:none}}@media (max-width: 700px){.sm-content,.container-fluid.tin-bg-image,mat-sidenav-content.tin-bg-image{padding-left:10px!important;padding-right:10px!important}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i4$2.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i4$2.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i4$2.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "directive", type: i4$3.MatBadge, selector: "[matBadge]", inputs: ["matBadgeColor", "matBadgeOverlap", "matBadgeDisabled", "matBadgePosition", "matBadge", "matBadgeDescription", "matBadgeSize", "matBadgeHidden"] }, { kind: "component", type: i2$1.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: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i17.MatNavList, selector: "mat-nav-list", exportAs: ["matNavList"] }, { kind: "component", type: i17.MatListItem, selector: "mat-list-item, a[mat-list-item], button[mat-list-item]", inputs: ["activated"], exportAs: ["matListItem"] }, { kind: "component", type: i17.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: i7$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: i19$1.MatSidenav, selector: "mat-sidenav", inputs: ["fixedInViewport", "fixedTopGap", "fixedBottomGap"], exportAs: ["matSidenav"] }, { kind: "component", type: i19$1.MatSidenavContainer, selector: "mat-sidenav-container", exportAs: ["matSidenavContainer"] }, { kind: "component", type: i19$1.MatSidenavContent, selector: "mat-sidenav-content" }, { kind: "component", type: i20.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { kind: "directive", type: i1$1.RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "directive", type: i1$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: i21.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i21.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "component", type: LoaderComponent, selector: "spa-loader", inputs: ["logo"] }, { kind: "component", type: ToastComponent, selector: "spa-toast" }, { kind: "component", type: AgentComponent, selector: "spa-agent", inputs: ["pageMode"] }, { kind: "component", type: BottomTabsComponent, selector: "spa-bottom-tabs" }, { kind: "component", type: OfflineIndicatorComponent, selector: "spa-offline-indicator" }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }] }); }
|
|
28048
28290
|
}
|
|
28049
28291
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: NavMenuComponent, decorators: [{
|
|
28050
28292
|
type: Component,
|
|
@@ -28687,11 +28929,11 @@ class TabsFloatComponent {
|
|
|
28687
28929
|
return null;
|
|
28688
28930
|
}
|
|
28689
28931
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TabsFloatComponent, deps: [{ token: i0.ElementRef }, { token: i0.NgZone }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
28690
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TabsFloatComponent, isStandalone: false, selector: "spa-tabs-float", inputs: { tabs: "tabs" }, viewQueries: [{ propertyName: "strip", first: true, predicate: ["strip"], descendants: true }, { propertyName: "anchor", first: true, predicate: ["anchor"], descendants: true }], ngImport: i0, template: "<!-- Zero-height sticky anchor. It is deliberately ALWAYS in flow and always height 0, so no dialog layout\n changes in any state; the strip itself is absolutely positioned off its bottom edge. -->\n<div #anchor class=\"tin-float-anchor\" *ngIf=\"tabs\">\n\n <!-- Kept in the DOM when hidden (opacity/transform, not display:none) for two reasons: its height must be\n measurable to compute the release line, and the show/hide reads as a slide rather than a blink. -->\n <div #strip class=\"tin-float-strip\" [class.tin-float-on]=\"visible\" [attr.aria-hidden]=\"!visible\" role=\"
|
|
28932
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TabsFloatComponent, isStandalone: false, selector: "spa-tabs-float", inputs: { tabs: "tabs" }, viewQueries: [{ propertyName: "strip", first: true, predicate: ["strip"], descendants: true }, { propertyName: "anchor", first: true, predicate: ["anchor"], descendants: true }], ngImport: i0, template: "<!-- Zero-height sticky anchor. It is deliberately ALWAYS in flow and always height 0, so no dialog layout\n changes in any state; the strip itself is absolutely positioned off its bottom edge. -->\n<div #anchor class=\"tin-float-anchor\" *ngIf=\"tabs\">\n\n <!-- Kept in the DOM when hidden (opacity/transform, not display:none) for two reasons: its height must be\n measurable to compute the release line, and the show/hide reads as a slide rather than a blink. -->\n <!-- Changed: NOT a second tablist. This strip is a scroll shortcut to the real Material tab strip, which is the\n one authoritative tablist on the page. Declaring role=\"tablist\"/role=\"tab\" here put TWO nodes carrying each\n tab's accessible name into the a11y tree, which (a) is invalid ARIA anyway \u2014 these buttons control no\n tabpanel and have no aria-controls \u2014 and (b) made every `getByRole('tab', { name })` in the estate a\n strict-mode violation that reads as a MISSING tab. They stay plain <button>s: same click, same focus order,\n same appearance, still reachable as getByRole('button', { name }). aria-current marks the active one. -->\n <div #strip class=\"tin-float-strip\" [class.tin-float-on]=\"visible\" [attr.aria-hidden]=\"!visible\" role=\"group\" aria-label=\"Jump to tab\">\n\n <button type=\"button\" class=\"tin-float-tab\" *ngFor=\"let tab of tabs.visibleTabs; let i = index\"\n [class.tin-float-tab-active]=\"tabs.selectedTabIndex === i\" [attr.aria-current]=\"tabs.selectedTabIndex === i ? 'true' : null\" [attr.tabindex]=\"visible ? 0 : -1\" (click)=\"go(i)\">\n <span class=\"tin-float-label\">\n {{ tabs.getTabTitle(tab.config) }}\n <!-- Same pill, same rules as the real strip \u2014 read straight off spa-tabs, never recomputed here -->\n <span *ngIf=\"tabs.shouldShowBadge(tab.originalIndex)\" class=\"tin-float-count\" [class.tin-float-count-zero]=\"tabs.getTabCount(tab.originalIndex) === 0\">{{ tabs.getTabCount(tab.originalIndex) }}</span>\n </span>\n </button>\n\n </div>\n\n</div>\n", styles: [":host{display:contents}.tin-float-anchor{position:sticky;bottom:0;height:0;z-index:5;pointer-events:none}.tin-float-strip{position:absolute;left:0;right:0;bottom:calc(-1 * var(--tin-float-gap, 0px));display:flex;align-items:stretch;gap:4px;padding:0 4px;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;background:var(--mat-sys-surface, #ffffff);border-top:1px solid rgba(0,0,0,.08);box-shadow:0 -6px 16px -8px #00000047,0 -1px #00000005;opacity:0;transform:translateY(100%);pointer-events:none;transition:opacity .14s ease,transform .18s cubic-bezier(.2,0,0,1)}.tin-float-strip::-webkit-scrollbar{display:none}.tin-float-on{opacity:1;transform:translateY(0);pointer-events:auto}@media (prefers-reduced-motion: reduce){.tin-float-strip{transition:none}}.tin-float-tab{position:relative;flex:0 0 auto;appearance:none;background:none;border:0;cursor:pointer;padding:12px 16px;min-height:46px;font:inherit;font-size:14px;font-weight:500;line-height:1;white-space:nowrap;color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .6));transition:color .12s ease,background-color .12s ease}.tin-float-tab:hover{background-color:#1976d20f}.tin-float-tab:focus-visible{outline:2px solid #1976d2;outline-offset:-3px}.tin-float-tab-active{color:#1976d2}.tin-float-tab-active:before{content:\"\";position:absolute;top:0;left:8px;right:8px;height:2px;border-radius:0 0 2px 2px;background:#1976d2}.tin-float-label{display:inline-flex;align-items:center;gap:8px}.tin-float-count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:18px;padding:0 6px;border-radius:9px;background-color:#1976d2;color:#fff;font-size:11px;font-weight:600;line-height:1}.tin-float-count-zero{background-color:#d5d9de;color:#6b7280}@media (max-width: 599px){.tin-float-tab{padding:10px 12px;min-height:42px;font-size:13px}}:host-context([data-theme=\"dark\"]) .tin-float-strip,:host-context(.tin-dark) .tin-float-strip{background:var(--mat-sys-surface, #1e242b);border-top-color:#ffffff1f;box-shadow:0 -6px 16px -8px #0009}:host-context([data-theme=\"dark\"]) .tin-float-tab,:host-context(.tin-dark) .tin-float-tab{color:#ffffffa8}:host-context([data-theme=\"dark\"]) .tin-float-tab-active,:host-context(.tin-dark) .tin-float-tab-active{color:#7fb3ec}\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"] }] }); }
|
|
28691
28933
|
}
|
|
28692
28934
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TabsFloatComponent, decorators: [{
|
|
28693
28935
|
type: Component,
|
|
28694
|
-
args: [{ selector: 'spa-tabs-float', standalone: false, template: "<!-- Zero-height sticky anchor. It is deliberately ALWAYS in flow and always height 0, so no dialog layout\n changes in any state; the strip itself is absolutely positioned off its bottom edge. -->\n<div #anchor class=\"tin-float-anchor\" *ngIf=\"tabs\">\n\n <!-- Kept in the DOM when hidden (opacity/transform, not display:none) for two reasons: its height must be\n measurable to compute the release line, and the show/hide reads as a slide rather than a blink. -->\n <div #strip class=\"tin-float-strip\" [class.tin-float-on]=\"visible\" [attr.aria-hidden]=\"!visible\" role=\"
|
|
28936
|
+
args: [{ selector: 'spa-tabs-float', standalone: false, template: "<!-- Zero-height sticky anchor. It is deliberately ALWAYS in flow and always height 0, so no dialog layout\n changes in any state; the strip itself is absolutely positioned off its bottom edge. -->\n<div #anchor class=\"tin-float-anchor\" *ngIf=\"tabs\">\n\n <!-- Kept in the DOM when hidden (opacity/transform, not display:none) for two reasons: its height must be\n measurable to compute the release line, and the show/hide reads as a slide rather than a blink. -->\n <!-- Changed: NOT a second tablist. This strip is a scroll shortcut to the real Material tab strip, which is the\n one authoritative tablist on the page. Declaring role=\"tablist\"/role=\"tab\" here put TWO nodes carrying each\n tab's accessible name into the a11y tree, which (a) is invalid ARIA anyway \u2014 these buttons control no\n tabpanel and have no aria-controls \u2014 and (b) made every `getByRole('tab', { name })` in the estate a\n strict-mode violation that reads as a MISSING tab. They stay plain <button>s: same click, same focus order,\n same appearance, still reachable as getByRole('button', { name }). aria-current marks the active one. -->\n <div #strip class=\"tin-float-strip\" [class.tin-float-on]=\"visible\" [attr.aria-hidden]=\"!visible\" role=\"group\" aria-label=\"Jump to tab\">\n\n <button type=\"button\" class=\"tin-float-tab\" *ngFor=\"let tab of tabs.visibleTabs; let i = index\"\n [class.tin-float-tab-active]=\"tabs.selectedTabIndex === i\" [attr.aria-current]=\"tabs.selectedTabIndex === i ? 'true' : null\" [attr.tabindex]=\"visible ? 0 : -1\" (click)=\"go(i)\">\n <span class=\"tin-float-label\">\n {{ tabs.getTabTitle(tab.config) }}\n <!-- Same pill, same rules as the real strip \u2014 read straight off spa-tabs, never recomputed here -->\n <span *ngIf=\"tabs.shouldShowBadge(tab.originalIndex)\" class=\"tin-float-count\" [class.tin-float-count-zero]=\"tabs.getTabCount(tab.originalIndex) === 0\">{{ tabs.getTabCount(tab.originalIndex) }}</span>\n </span>\n </button>\n\n </div>\n\n</div>\n", styles: [":host{display:contents}.tin-float-anchor{position:sticky;bottom:0;height:0;z-index:5;pointer-events:none}.tin-float-strip{position:absolute;left:0;right:0;bottom:calc(-1 * var(--tin-float-gap, 0px));display:flex;align-items:stretch;gap:4px;padding:0 4px;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;background:var(--mat-sys-surface, #ffffff);border-top:1px solid rgba(0,0,0,.08);box-shadow:0 -6px 16px -8px #00000047,0 -1px #00000005;opacity:0;transform:translateY(100%);pointer-events:none;transition:opacity .14s ease,transform .18s cubic-bezier(.2,0,0,1)}.tin-float-strip::-webkit-scrollbar{display:none}.tin-float-on{opacity:1;transform:translateY(0);pointer-events:auto}@media (prefers-reduced-motion: reduce){.tin-float-strip{transition:none}}.tin-float-tab{position:relative;flex:0 0 auto;appearance:none;background:none;border:0;cursor:pointer;padding:12px 16px;min-height:46px;font:inherit;font-size:14px;font-weight:500;line-height:1;white-space:nowrap;color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .6));transition:color .12s ease,background-color .12s ease}.tin-float-tab:hover{background-color:#1976d20f}.tin-float-tab:focus-visible{outline:2px solid #1976d2;outline-offset:-3px}.tin-float-tab-active{color:#1976d2}.tin-float-tab-active:before{content:\"\";position:absolute;top:0;left:8px;right:8px;height:2px;border-radius:0 0 2px 2px;background:#1976d2}.tin-float-label{display:inline-flex;align-items:center;gap:8px}.tin-float-count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:18px;padding:0 6px;border-radius:9px;background-color:#1976d2;color:#fff;font-size:11px;font-weight:600;line-height:1}.tin-float-count-zero{background-color:#d5d9de;color:#6b7280}@media (max-width: 599px){.tin-float-tab{padding:10px 12px;min-height:42px;font-size:13px}}:host-context([data-theme=\"dark\"]) .tin-float-strip,:host-context(.tin-dark) .tin-float-strip{background:var(--mat-sys-surface, #1e242b);border-top-color:#ffffff1f;box-shadow:0 -6px 16px -8px #0009}:host-context([data-theme=\"dark\"]) .tin-float-tab,:host-context(.tin-dark) .tin-float-tab{color:#ffffffa8}:host-context([data-theme=\"dark\"]) .tin-float-tab-active,:host-context(.tin-dark) .tin-float-tab-active{color:#7fb3ec}\n"] }]
|
|
28695
28937
|
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.NgZone }, { type: i0.ChangeDetectorRef }], propDecorators: { tabs: [{
|
|
28696
28938
|
type: Input
|
|
28697
28939
|
}], strip: [{
|
|
@@ -32200,7 +32442,7 @@ class LoginComponent {
|
|
|
32200
32442
|
}
|
|
32201
32443
|
}
|
|
32202
32444
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LoginComponent, deps: [{ token: HttpService }, { token: StorageService }, { token: i1$1.Router }, { token: MessageService }, { token: DataServiceLib }, { token: AuthService }, { token: LogService }, { token: i1$1.ActivatedRoute }, { token: NotificationsService }, { token: SignalRService }, { token: i10$1.MsalService }, { token: i1$3.MatDialog }, { token: SetupService }, { token: LastRouteService }, { token: TIN_SPA_RUNTIME_CONFIG }, { token: ApiErrorService }, { token: 'SocialAuthServiceConfig', optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
32203
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: LoginComponent, isStandalone: false, selector: "spa-login", ngImport: i0, template: "\n <div *ngIf=\"style=='default'\" class=\"login-page background tin-bg-login\">\n\n <div class=\"container\" >\n\n <div class=\"logo\">\n <img *ngIf=\"appConfig.logoSize=='normal'\" [src]=\"appConfig.logo\" style=\"width: 100px\" />\n <img *ngIf=\"appConfig.logoSize=='medium'\" [src]=\"appConfig.logo\" style=\"width: 150px\" />\n <img *ngIf=\"appConfig.logoSize=='large'\" [src]=\"appConfig.logo\" style=\"width: 250px\" />\n </div>\n\n <mat-card class=\"mat-elevation-z3 \" style=\"width:400px; \">\n\n <mat-card-header style=\"margin-bottom: 30px;margin-top: 30px;\">\n <mat-card-title style=\"font-size: 40px;margin-bottom: 10px; margin-top: 20px; font-weight: 300\">{{appConfig.appName}}</mat-card-title>\n </mat-card-header>\n\n <!-- Added: silent-restore state \u2014 replaces the credential form while the session is being restored -->\n <mat-card-content *ngIf=\"restoring\">\n <div class=\"restore-panel\" role=\"status\" aria-live=\"polite\">\n <mat-spinner diameter=\"40\"></mat-spinner>\n <p class=\"restore-title\">Signing you back in\u2026</p>\n <p class=\"restore-hint\">Restoring your session \u2014 this can take a few seconds.</p>\n <button id=\"btnManualLogin\" mat-button color=\"primary\" class=\"restore-manual\" (click)=\"signInManually()\">Use a different account</button>\n </div>\n </mat-card-content>\n\n <!-- Added: automatic sign-in failed because the server was unreachable \u2014 explain before asking for credentials -->\n <mat-card-content *ngIf=\"restoreOffline && !restoring\">\n <div class=\"restore-note\" role=\"alert\">We couldn't sign you back in automatically \u2014 please check your connection and log in below.</div>\n </mat-card-content>\n\n <mat-card-content *ngIf=\"!restoring && (appConfig.localAuth || appConfig.ADAuth)\"> <!-- Changed: hidden while the silent restore runs -->\n\n <div class=\"tin-input mt-2\">\n\n <spa-text id=\"txtuserName\" display=\"Username\" [(value)]=\"user.userName\" style=\"margin-bottom: 20px;\"></spa-text>\n\n <spa-text-mask id=\"txtPassword\" display=\"Password\" [(value)]=\"user.password\" (enterPress)=\"login()\"></spa-text-mask>\n\n <!-- Changed: \"Keep me signed in\" now opt-in via appConfig.keepSignedIn -->\n <mat-checkbox *ngIf=\"appConfig.keepSignedIn\" [(ngModel)]=\"rememberMe\" color=\"primary\" style=\"margin-top: 10px;\">Keep me signed in</mat-checkbox>\n\n </div>\n\n </mat-card-content>\n\n\n <mat-card-actions style=\"margin-bottom: 10px;\" *ngIf=\"!restoring\"> <!-- Changed: no clickable sign-in options while the silent restore runs -->\n\n <!-- Changed: Widths removed \u2014 all elements aligned to one column via .container CSS -->\n <div class=\"button\" *ngIf=\"appConfig.localAuth || appConfig.ADAuth\">\n <button id=\"btnLogin\" mat-flat-button [disabled]=\"isProcessing\" (click)=\"login()\" color=\"primary\">Login</button>\n </div>\n\n <!-- Changed: Show signup button whenever selfService is enabled, not just for local/AD auth -->\n <div class=\"button\" *ngIf=\"appConfig.selfService\" >\n <button id=\"btnSignup\" mat-stroked-button color=\"primary\" (click)=\"signup()\">Create an account</button>\n </div>\n\n <div class=\"divider\" *ngIf=\"appConfig.googleAuth || appConfig.microsoftAuth\">\n <span>OR</span>\n </div>\n\n <!-- Changed: Match modern Google button \u2014 pill shape + width aligned to the column -->\n <div class=\"button\" *ngIf=\"appConfig.googleAuth\">\n <asl-google-signin-button type='standard' width=\"350\" size='large' shape=\"pill\" logo_alignment=\"center\" style=\"text-align: center;\"></asl-google-signin-button> <!-- Changed: size 'medium' renders a 32px GSI button, 8px shorter than the 40px Material buttons; 'large' renders 40px so all three align -->\n </div>\n\n <div class=\"button\" *ngIf=\"appConfig.microsoftAuth\">\n <button mat-stroked-button color=\"primary\" (click)=\"loginWithMS()\">{{appConfig.microsoftAuthMessage}}</button>\n </div>\n\n </mat-card-actions>\n\n </mat-card>\n\n <a *ngIf=\"appConfig.selfService && !restoring\" mat-button id=\"lnkRecover\" style=\"margin-top: 1em\" (click)=\"recoverAccount()\">Forgot your password ?</a> <!-- Changed: hidden while the silent restore runs -->\n\n\n\n\n </div>\n </div>\n\n\n <div *ngIf=\"style=='modern'\" class=\"modern-login\">\n <mat-card class=\"login-card\">\n <!-- Logo -->\n <div class=\"logo-container\">\n <img *ngIf=\"appConfig.logoSize=='normal'\" [src]=\"appConfig.logo\" style=\"width: 100px\" />\n <img *ngIf=\"appConfig.logoSize=='medium'\" [src]=\"appConfig.logo\" style=\"width: 150px\" />\n <img *ngIf=\"appConfig.logoSize=='large'\" [src]=\"appConfig.logo\" style=\"width: 250px\" />\n </div>\n\n <!-- Welcome text -->\n <div class=\"header-section\"> <!-- Changed: Use custom div instead of mat-card-header -->\n <h2 class=\"login-title\">{{appConfig.loginTitle ?? appConfig.appName}}</h2> <!-- Changed: Use h2 for title -->\n <p class=\"login-subtitle\" *ngIf=\"appConfig.loginMessage\">{{appConfig.loginMessage}}</p> <!-- Changed: Use p for subtitle -->\n </div>\n\n <mat-card-content>\n <!-- Added: silent-restore state \u2014 replaces the credential form while the session is being restored -->\n <div class=\"restore-panel\" *ngIf=\"restoring\" role=\"status\" aria-live=\"polite\">\n <mat-spinner diameter=\"40\"></mat-spinner>\n <p class=\"restore-title\">Signing you back in\u2026</p>\n <p class=\"restore-hint\">Restoring your session \u2014 this can take a few seconds.</p>\n <button id=\"btnManualLogin\" mat-button color=\"primary\" class=\"restore-manual\" (click)=\"signInManually()\">Use a different account</button>\n </div>\n\n <div class=\"login-form\" *ngIf=\"!restoring\"> <!-- Changed: hidden while the silent restore runs -->\n <!-- Added: automatic sign-in failed because the server was unreachable \u2014 explain before asking for credentials -->\n <div class=\"restore-note\" *ngIf=\"restoreOffline\" role=\"alert\">We couldn't sign you back in automatically \u2014 please check your connection and log in below.</div>\n <!-- Username -->\n <spa-text id=\"txtuserName\" display=\"Username\" [(value)]=\"user.userName\" [appearance]=\"'outline'\" style=\"margin-bottom: 20px;\"></spa-text>\n\n <!-- Password -->\n <spa-text-mask id=\"txtPassword\" display=\"Password\" [(value)]=\"user.password\" [appearance]=\"'outline'\" (enterPress)=\"login()\"></spa-text-mask>\n\n <!-- Changed: \"Keep me signed in\" now opt-in via appConfig.keepSignedIn -->\n <mat-checkbox *ngIf=\"appConfig.keepSignedIn\" [(ngModel)]=\"rememberMe\" color=\"primary\" style=\"margin-top: 10px; margin-bottom: 10px;\">Keep me signed in</mat-checkbox>\n\n <!-- Login Button -->\n <div class=\"button-container\">\n <button id=\"btnLogin\" mat-flat-button color=\"primary\" [disabled]=\"isProcessing\" (click)=\"login()\">\n Login\n </button>\n </div>\n\n <!-- Divider -->\n <div class=\"divider\">\n <span>OR</span>\n </div>\n\n <!-- Social Login -->\n <div *ngIf=\"appConfig.googleAuth\" class=\"social-login\">\n <asl-google-signin-button type='standard' size='large' width=\"320\" logo_alignment=\"center\" shape=\"pill\"></asl-google-signin-button> <!-- Changed: size 'medium' renders a 32px GSI button, 8px shorter than the 40px Material buttons; 'large' renders 40px so all three align -->\n </div>\n\n <!-- Microsoft Login -->\n <div class=\"button\" *ngIf=\"appConfig.microsoftAuth\">\n <button mat-stroked-button color=\"primary\" style=\"width: 350px;\" (click)=\"loginWithMS()\">{{appConfig.microsoftAuthMessage}}</button>\n </div>\n\n <!-- Links -->\n <div class=\"links-container mb-5\">\n <a *ngIf=\"appConfig.selfService\" mat-button id=\"lnkRecover\" color=\"primary\" (click)=\"recoverAccount()\">\n Forgot password?\n </a>\n\n <div *ngIf=\"appConfig.selfService\" class=\"signup-container\">\n <span>Don't have an account?</span>\n <a mat-button id=\"btnSignup\" color=\"primary\" (click)=\"signup()\">Sign up</a>\n </div>\n </div>\n </div>\n </mat-card-content>\n\n <mat-card-footer>\n <div class=\"terms-container\">\n <mat-divider></mat-divider>\n <p class=\"terms-text\">\n By continuing, you acknowledge that you accept our\n <a mat-button color=\"primary\" class=\"terms-link\" (click)=\"openTerms()\">Terms and Conditions</a>\n and\n <a mat-button color=\"primary\" class=\"terms-link\" (click)=\"openPrivacy()\">Privacy Policy</a>.\n </p>\n </div>\n </mat-card-footer>\n </mat-card>\n</div>\n\n\n\n", styles: [".login-page{position:absolute;inset:0;overflow:auto}.background{min-height:100%}.container{display:flex;flex-direction:column;align-items:center;height:100vh}.logo{margin-top:3em;margin-bottom:1em}.container mat-card-header{text-align:center;justify-content:center}.container mat-card-title{text-align:center}.container mat-card-actions{display:flex;flex-direction:column;align-items:center;justify-content:center}.buttons{display:flex;flex-direction:row;justify-content:space-evenly}.button{display:flex;flex-direction:row;justify-content:center;margin-top:10px}.container mat-card-content,.container mat-card-actions{width:350px;max-width:100%;box-sizing:border-box;padding-left:0;padding-right:0;margin-left:auto;margin-right:auto}.container mat-card-actions{align-items:stretch}.container .button{width:100%}.container .button button{width:100%!important}.container .tin-input ::ng-deep mat-form-field{width:100%!important;margin-right:0!important}.container .button asl-google-signin-button{display:flex;justify-content:center;width:100%}.container mat-card{padding-top:16px;padding-bottom:44px;border-radius:16px}.container mat-card-content{padding-top:0;padding-bottom:0}.container mat-card-actions .button:first-of-type,.container mat-card-actions .divider:first-of-type{margin-top:20px}.restore-panel{display:flex;flex-direction:column;align-items:center;text-align:center;padding:20px 0 8px;animation:restore-fade .35s ease}.restore-title{margin:20px 0 4px;font-size:16px;font-weight:400;letter-spacing:.25px;color:#000000de}.restore-hint{margin:0;font-size:13px;font-weight:300;color:#0000008a}.restore-manual{margin-top:18px}.restore-note{width:100%;box-sizing:border-box;margin:0 0 16px;padding:10px 14px;border-radius:8px;background:#e692141f;color:#9a6200;font-size:13px;line-height:1.45;text-align:center;animation:restore-fade .35s ease}@keyframes restore-fade{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}.modern-login{min-height:100vh;display:flex;align-items:center;justify-content:center;background-color:#000}.login-card{width:100%;max-width:400px;padding:24px}.logo-container{text-align:center;margin-bottom:24px}.header-section{text-align:center;margin-bottom:32px}.login-title{margin:0 0 8px;font-size:32px;font-weight:300;letter-spacing:.5px;color:#000000de;text-align:center}.login-subtitle{margin:0;font-size:14px;font-weight:300;color:#0000008a;letter-spacing:.25px;text-align:center}.mat-card-header{text-align:center;justify-content:center;margin-bottom:24px}.mat-card-title{margin:0;font-size:24px;font-weight:400}.mat-card-subtitle{margin:8px 0 0}.login-form{display:flex;flex-direction:column}.button-container button{width:100%;margin-top:10px;border-radius:24px;height:40px}.divider{position:relative;text-align:center;margin:10px 0}.divider span{background:transparent;padding:0 16px;color:#666;font-size:14px;position:relative}.social-login{display:flex;justify-content:center}.modern-login .button button{width:100%;border-radius:24px;height:40px;margin-top:10px}.links-container{text-align:center;margin-top:16px}.signup-container{margin-top:16px;color:#0009}.signup-container span{margin-right:8px}.terms-container{margin-top:24px;padding:16px}.terms-text{color:#0009;font-size:12px;text-align:center;margin:16px 0 0;line-height:1.5}.terms-link{padding:0 4px;min-width:auto;line-height:inherit;height:auto}\n"], dependencies: [{ kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i4$1.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: i2$1.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: 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: i19.MatCardContent, selector: "mat-card-content" }, { kind: "directive", type: i19.MatCardFooter, selector: "mat-card-footer" }, { kind: "component", type: i19.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i19.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "component", type: i20$2.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "component", type: i17.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "component", type: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextMaskComponent, selector: "spa-text-mask", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "regex", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "directive", type: i2$2.GoogleSigninButtonDirective, selector: "asl-google-signin-button", inputs: ["type", "size", "text", "shape", "theme", "logo_alignment", "width", "locale"] }] }); }
|
|
32445
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: LoginComponent, isStandalone: false, selector: "spa-login", ngImport: i0, template: "\n <div *ngIf=\"style=='default'\" class=\"login-page background tin-bg-login\">\n\n <div class=\"container\" >\n\n <div class=\"logo\">\n <img *ngIf=\"appConfig.logoSize=='normal'\" [src]=\"appConfig.logo\" style=\"width: 100px\" />\n <img *ngIf=\"appConfig.logoSize=='medium'\" [src]=\"appConfig.logo\" style=\"width: 150px\" />\n <img *ngIf=\"appConfig.logoSize=='large'\" [src]=\"appConfig.logo\" style=\"width: 250px\" />\n </div>\n\n <mat-card class=\"mat-elevation-z3 \" style=\"width:400px; \">\n\n <mat-card-header style=\"margin-bottom: 30px;margin-top: 30px;\">\n <mat-card-title style=\"font-size: 40px;margin-bottom: 10px; margin-top: 20px; font-weight: 300\">{{appConfig.appName}}</mat-card-title>\n </mat-card-header>\n\n <!-- Added: silent-restore state \u2014 replaces the credential form while the session is being restored -->\n <mat-card-content *ngIf=\"restoring\">\n <div class=\"restore-panel\" role=\"status\" aria-live=\"polite\">\n <mat-spinner diameter=\"40\"></mat-spinner>\n <p class=\"restore-title\">Signing you back in\u2026</p>\n <p class=\"restore-hint\">Restoring your session \u2014 this can take a few seconds.</p>\n <button id=\"btnManualLogin\" mat-button color=\"primary\" class=\"restore-manual\" (click)=\"signInManually()\">Use a different account</button>\n </div>\n </mat-card-content>\n\n <!-- Added: automatic sign-in failed because the server was unreachable \u2014 explain before asking for credentials -->\n <mat-card-content *ngIf=\"restoreOffline && !restoring\">\n <div class=\"restore-note\" role=\"alert\">We couldn't sign you back in automatically \u2014 please check your connection and log in below.</div>\n </mat-card-content>\n\n <mat-card-content *ngIf=\"!restoring && (appConfig.localAuth || appConfig.ADAuth)\"> <!-- Changed: hidden while the silent restore runs -->\n\n <div class=\"tin-input mt-2\">\n\n <spa-text id=\"txtuserName\" display=\"Username\" [(value)]=\"user.userName\" style=\"margin-bottom: 20px;\"></spa-text>\n\n <spa-text-mask id=\"txtPassword\" display=\"Password\" [(value)]=\"user.password\" (enterPress)=\"login()\"></spa-text-mask>\n\n <!-- Changed: \"Keep me signed in\" now opt-in via appConfig.keepSignedIn -->\n <mat-checkbox *ngIf=\"appConfig.keepSignedIn\" [(ngModel)]=\"rememberMe\" color=\"primary\" style=\"margin-top: 10px;\">Keep me signed in</mat-checkbox>\n\n </div>\n\n </mat-card-content>\n\n\n <mat-card-actions style=\"margin-bottom: 10px;\" *ngIf=\"!restoring\"> <!-- Changed: no clickable sign-in options while the silent restore runs -->\n\n <!-- Changed: Widths removed \u2014 all elements aligned to one column via .container CSS -->\n <div class=\"button\" *ngIf=\"appConfig.localAuth || appConfig.ADAuth\">\n <button id=\"btnLogin\" mat-flat-button [disabled]=\"isProcessing\" (click)=\"login()\" color=\"primary\">Login</button>\n </div>\n\n <!-- Changed: Show signup button whenever selfService is enabled, not just for local/AD auth -->\n <div class=\"button\" *ngIf=\"appConfig.selfService\" >\n <button id=\"btnSignup\" mat-stroked-button color=\"primary\" (click)=\"signup()\">Create an account</button>\n </div>\n\n <div class=\"divider\" *ngIf=\"appConfig.googleAuth || appConfig.microsoftAuth\">\n <span>OR</span>\n </div>\n\n <!-- Changed: Match modern Google button \u2014 pill shape + width aligned to the column -->\n <div class=\"button\" *ngIf=\"appConfig.googleAuth\">\n <asl-google-signin-button type='standard' width=\"350\" size='large' shape=\"pill\" logo_alignment=\"center\" style=\"text-align: center;\"></asl-google-signin-button> <!-- Changed: size 'medium' renders a 32px GSI button, 8px shorter than the 40px Material buttons; 'large' renders 40px so all three align -->\n </div>\n\n <div class=\"button\" *ngIf=\"appConfig.microsoftAuth\">\n <button mat-stroked-button color=\"primary\" (click)=\"loginWithMS()\">{{appConfig.microsoftAuthMessage}}</button>\n </div>\n\n </mat-card-actions>\n\n </mat-card>\n\n <a *ngIf=\"appConfig.selfService && !restoring\" mat-button id=\"lnkRecover\" style=\"margin-top: 1em\" (click)=\"recoverAccount()\">Forgot your password ?</a> <!-- Changed: hidden while the silent restore runs -->\n\n\n\n\n </div>\n </div>\n\n\n <div *ngIf=\"style=='modern'\" class=\"modern-login\">\n <mat-card class=\"login-card\">\n <!-- Logo -->\n <div class=\"logo-container\">\n <img *ngIf=\"appConfig.logoSize=='normal'\" [src]=\"appConfig.logo\" style=\"width: 100px\" />\n <img *ngIf=\"appConfig.logoSize=='medium'\" [src]=\"appConfig.logo\" style=\"width: 150px\" />\n <img *ngIf=\"appConfig.logoSize=='large'\" [src]=\"appConfig.logo\" style=\"width: 250px\" />\n </div>\n\n <!-- Welcome text -->\n <div class=\"header-section\"> <!-- Changed: Use custom div instead of mat-card-header -->\n <h2 class=\"login-title\">{{appConfig.loginTitle ?? appConfig.appName}}</h2> <!-- Changed: Use h2 for title -->\n <p class=\"login-subtitle\" *ngIf=\"appConfig.loginMessage\">{{appConfig.loginMessage}}</p> <!-- Changed: Use p for subtitle -->\n </div>\n\n <mat-card-content>\n <!-- Added: silent-restore state \u2014 replaces the credential form while the session is being restored -->\n <div class=\"restore-panel\" *ngIf=\"restoring\" role=\"status\" aria-live=\"polite\">\n <mat-spinner diameter=\"40\"></mat-spinner>\n <p class=\"restore-title\">Signing you back in\u2026</p>\n <p class=\"restore-hint\">Restoring your session \u2014 this can take a few seconds.</p>\n <button id=\"btnManualLogin\" mat-button color=\"primary\" class=\"restore-manual\" (click)=\"signInManually()\">Use a different account</button>\n </div>\n\n <div class=\"login-form\" *ngIf=\"!restoring\"> <!-- Changed: hidden while the silent restore runs -->\n <!-- Added: automatic sign-in failed because the server was unreachable \u2014 explain before asking for credentials -->\n <div class=\"restore-note\" *ngIf=\"restoreOffline\" role=\"alert\">We couldn't sign you back in automatically \u2014 please check your connection and log in below.</div>\n <!-- Username -->\n <spa-text id=\"txtuserName\" display=\"Username\" [(value)]=\"user.userName\" [appearance]=\"'outline'\" style=\"margin-bottom: 20px;\"></spa-text>\n\n <!-- Password -->\n <spa-text-mask id=\"txtPassword\" display=\"Password\" [(value)]=\"user.password\" [appearance]=\"'outline'\" (enterPress)=\"login()\"></spa-text-mask>\n\n <!-- Changed: \"Keep me signed in\" now opt-in via appConfig.keepSignedIn -->\n <mat-checkbox *ngIf=\"appConfig.keepSignedIn\" [(ngModel)]=\"rememberMe\" color=\"primary\" style=\"margin-top: 10px; margin-bottom: 10px;\">Keep me signed in</mat-checkbox>\n\n <!-- Login Button -->\n <div class=\"button-container\">\n <button id=\"btnLogin\" mat-flat-button color=\"primary\" [disabled]=\"isProcessing\" (click)=\"login()\">\n Login\n </button>\n </div>\n\n <!-- Divider -->\n <div class=\"divider\">\n <span>OR</span>\n </div>\n\n <!-- Social Login -->\n <div *ngIf=\"appConfig.googleAuth\" class=\"social-login\">\n <asl-google-signin-button type='standard' size='large' width=\"320\" logo_alignment=\"center\" shape=\"pill\"></asl-google-signin-button> <!-- Changed: size 'medium' renders a 32px GSI button, 8px shorter than the 40px Material buttons; 'large' renders 40px so all three align -->\n </div>\n\n <!-- Microsoft Login -->\n <div class=\"button\" *ngIf=\"appConfig.microsoftAuth\">\n <button mat-stroked-button color=\"primary\" style=\"width: 350px;\" (click)=\"loginWithMS()\">{{appConfig.microsoftAuthMessage}}</button>\n </div>\n\n <!-- Links -->\n <div class=\"links-container mb-5\">\n <a *ngIf=\"appConfig.selfService\" mat-button id=\"lnkRecover\" color=\"primary\" (click)=\"recoverAccount()\">\n Forgot password?\n </a>\n\n <div *ngIf=\"appConfig.selfService\" class=\"signup-container\">\n <span>Don't have an account?</span>\n <a mat-button id=\"btnSignup\" color=\"primary\" (click)=\"signup()\">Sign up</a>\n </div>\n </div>\n </div>\n </mat-card-content>\n\n <mat-card-footer>\n <div class=\"terms-container\">\n <mat-divider></mat-divider>\n <p class=\"terms-text\">\n By continuing, you acknowledge that you accept our\n <a mat-button color=\"primary\" class=\"terms-link\" (click)=\"openTerms()\">Terms and Conditions</a>\n and\n <a mat-button color=\"primary\" class=\"terms-link\" (click)=\"openPrivacy()\">Privacy Policy</a>.\n </p>\n </div>\n </mat-card-footer>\n </mat-card>\n</div>\n\n\n\n", styles: [".login-page{position:absolute;inset:0;overflow:auto}.background{min-height:100%}.container{display:flex;flex-direction:column;align-items:center;height:100vh}.logo{margin-top:3em;margin-bottom:1em}.container mat-card-header{text-align:center;justify-content:center}.container mat-card-title{text-align:center}.container mat-card-actions{display:flex;flex-direction:column;align-items:center;justify-content:center}.buttons{display:flex;flex-direction:row;justify-content:space-evenly}.button{display:flex;flex-direction:row;justify-content:center;margin-top:10px}.container mat-card-content,.container mat-card-actions{width:350px;max-width:100%;box-sizing:border-box;padding-left:0;padding-right:0;margin-left:auto;margin-right:auto}.container mat-card-actions{align-items:stretch}.container .button{width:100%}.container .button button{width:100%!important}.container .tin-input ::ng-deep mat-form-field{width:100%!important;margin-right:0!important}.container .button asl-google-signin-button{display:flex;justify-content:center;width:100%}.container mat-card{padding-top:16px;padding-bottom:44px;border-radius:16px}.container mat-card-content{padding-top:0;padding-bottom:0}.container mat-card-actions .button:first-of-type,.container mat-card-actions .divider:first-of-type{margin-top:20px}.restore-panel{display:flex;flex-direction:column;align-items:center;text-align:center;padding:20px 0 8px;animation:restore-fade .35s ease}.restore-title{margin:20px 0 4px;font-size:16px;font-weight:400;letter-spacing:.25px;color:#000000de}.restore-hint{margin:0;font-size:13px;font-weight:300;color:#0000008a}.restore-manual{margin-top:18px}.restore-note{width:100%;box-sizing:border-box;margin:0 0 16px;padding:10px 14px;border-radius:8px;background:#e692141f;color:#9a6200;font-size:13px;line-height:1.45;text-align:center;animation:restore-fade .35s ease}@keyframes restore-fade{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}.modern-login{min-height:100vh;display:flex;align-items:center;justify-content:center;background-color:#000}.login-card{width:100%;max-width:400px;padding:24px}.logo-container{text-align:center;margin-bottom:24px}.header-section{text-align:center;margin-bottom:32px}.login-title{margin:0 0 8px;font-size:32px;font-weight:300;letter-spacing:.5px;color:#000000de;text-align:center}.login-subtitle{margin:0;font-size:14px;font-weight:300;color:#0000008a;letter-spacing:.25px;text-align:center}.mat-card-header{text-align:center;justify-content:center;margin-bottom:24px}.mat-card-title{margin:0;font-size:24px;font-weight:400}.mat-card-subtitle{margin:8px 0 0}.login-form{display:flex;flex-direction:column}.button-container button{width:100%;margin-top:10px;border-radius:24px;height:40px}.divider{position:relative;text-align:center;margin:10px 0}.divider span{background:transparent;padding:0 16px;color:#666;font-size:14px;position:relative}.social-login{display:flex;justify-content:center}.modern-login .button button{width:100%;border-radius:24px;height:40px;margin-top:10px}.links-container{text-align:center;margin-top:16px}.signup-container{margin-top:16px;color:#0009}.signup-container span{margin-right:8px}.terms-container{margin-top:24px;padding:16px}.terms-text{color:#0009;font-size:12px;text-align:center;margin:16px 0 0;line-height:1.5}.terms-link{padding:0 4px;min-width:auto;line-height:inherit;height:auto}\n"], dependencies: [{ kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i4$1.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: i2$1.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: 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: i19.MatCardContent, selector: "mat-card-content" }, { kind: "directive", type: i19.MatCardFooter, selector: "mat-card-footer" }, { kind: "component", type: i19.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i19.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "component", type: i20$1.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "component", type: i17.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "component", type: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextMaskComponent, selector: "spa-text-mask", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "regex", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "directive", type: i2$2.GoogleSigninButtonDirective, selector: "asl-google-signin-button", inputs: ["type", "size", "text", "shape", "theme", "logo_alignment", "width", "locale"] }] }); }
|
|
32204
32446
|
}
|
|
32205
32447
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LoginComponent, decorators: [{
|
|
32206
32448
|
type: Component,
|
|
@@ -32400,7 +32642,7 @@ class SignupComponent {
|
|
|
32400
32642
|
}
|
|
32401
32643
|
}
|
|
32402
32644
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SignupComponent, deps: [{ token: HttpService }, { token: MessageService }, { token: DataServiceLib }, { token: AuthService }, { token: StorageService }, { token: i1$1.Router }, { token: i10$1.MsalService }, { token: NotificationsService }, { token: SetupService }, { token: i1$3.MatDialog }, { token: i0.ChangeDetectorRef }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
32403
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SignupComponent, isStandalone: false, selector: "spa-signup", ngImport: i0, template: "<!-- Modern Style -->\n<div *ngIf=\"style=='modern'\" class=\"modern-signup\">\n <mat-card class=\"signup-card\">\n\n <!-- Logo -->\n <div class=\"logo-container\">\n <img *ngIf=\"appConfig.logoSize=='normal'\" [src]=\"appConfig.logo\" style=\"width: 100px\" />\n <img *ngIf=\"appConfig.logoSize=='medium'\" [src]=\"appConfig.logo\" style=\"width: 150px\" />\n <img *ngIf=\"appConfig.logoSize=='large'\" [src]=\"appConfig.logo\" style=\"width: 250px\" />\n </div>\n\n <!-- Provider Step -->\n <div *ngIf=\"step === 'provider'\">\n <div class=\"header-section\">\n <h2 class=\"signup-title\">Create your account</h2>\n <p class=\"signup-subtitle\">Get started with {{appConfig.appName}}</p>\n </div>\n\n <div class=\"provider-buttons\">\n <div *ngIf=\"appConfig.googleAuth\" class=\"social-login\">\n <asl-google-signin-button type='standard' size='medium' width=\"320\" logo_alignment=\"center\" shape=\"pill\"></asl-google-signin-button>\n </div>\n\n <div class=\"button\" *ngIf=\"appConfig.microsoftAuth\">\n <button mat-stroked-button color=\"primary\" (click)=\"loginWithMS()\">{{appConfig.microsoftAuthMessage ?? 'Continue with Microsoft'}}</button>\n </div>\n </div>\n\n <div class=\"links-container\">\n <div class=\"login-link-container\">\n <span>Already have an account?</span>\n <a mat-button color=\"primary\" (click)=\"goToLogin()\">Log in</a>\n </div>\n </div>\n </div>\n\n <!-- Form Step (New Org) -->\n <div *ngIf=\"step === 'form'\">\n <div class=\"header-section\">\n <h2 class=\"signup-title\">Almost there</h2>\n <p class=\"signup-subtitle\">Confirm your details to get started</p>\n </div>\n\n <div class=\"signup-form\">\n <div class=\"name-row\">\n <mat-form-field appearance=\"outline\" class=\"name-field\">\n <mat-label>First Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.firstName\">\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" class=\"name-field\">\n <mat-label>Last Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.lastName\">\n </mat-form-field>\n </div>\n\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Email</mat-label>\n <input matInput [value]=\"signupData.email\" readonly>\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Organisation Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.orgName\">\n </mat-form-field>\n\n <mat-checkbox [(ngModel)]=\"signupData.termsAccepted\" color=\"primary\" class=\"terms-checkbox\">\n I agree to the\n <a (click)=\"openTerms(); $event.preventDefault()\" class=\"terms-link\">Terms and Conditions</a>\n and\n <a (click)=\"openPrivacy(); $event.preventDefault()\" class=\"terms-link\">Privacy Policy</a>\n </mat-checkbox>\n\n <div class=\"button-container\">\n <button mat-flat-button color=\"primary\" [disabled]=\"isProcessing\" (click)=\"submit()\">\n <mat-spinner *ngIf=\"isProcessing\" diameter=\"20\" class=\"inline-spinner\"></mat-spinner>\n <span *ngIf=\"!isProcessing\">Create Account</span>\n </button>\n </div>\n\n <div class=\"links-container\">\n <a mat-button color=\"primary\" (click)=\"goToLogin()\">Back to Login</a>\n </div>\n </div>\n </div>\n\n <!-- Invitations Step (Join Existing Org) -->\n <div *ngIf=\"step === 'invitations'\">\n <div class=\"header-section\">\n <mat-icon class=\"invite-icon\" color=\"primary\">mail</mat-icon>\n <h2 class=\"signup-title\">You've been invited!</h2>\n <p class=\"signup-subtitle\">Accept an invitation to get started</p>\n </div>\n\n <div class=\"signup-form\">\n <div class=\"invitation-list\">\n <mat-card *ngFor=\"let inv of signupData.invitations\" class=\"invitation-card\" [class.accepted]=\"inv.accepted\" (click)=\"toggleInvitation(inv)\">\n <div class=\"invitation-content\">\n <mat-icon [color]=\"inv.accepted ? 'primary' : ''\">{{inv.accepted ? 'check_circle' : 'circle'}}</mat-icon>\n <span class=\"invitation-org-name\">{{inv.tenantName}}</span>\n </div>\n </mat-card>\n </div>\n\n <div class=\"name-row\">\n <mat-form-field appearance=\"outline\" class=\"name-field\">\n <mat-label>First Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.firstName\">\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" class=\"name-field\">\n <mat-label>Last Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.lastName\">\n </mat-form-field>\n </div>\n\n <mat-checkbox [(ngModel)]=\"signupData.termsAccepted\" color=\"primary\" class=\"terms-checkbox\">\n I agree to the\n <a (click)=\"openTerms(); $event.preventDefault()\" class=\"terms-link\">Terms and Conditions</a>\n and\n <a (click)=\"openPrivacy(); $event.preventDefault()\" class=\"terms-link\">Privacy Policy</a>\n </mat-checkbox>\n\n <div class=\"button-container\">\n <button mat-flat-button color=\"primary\" [disabled]=\"isProcessing\" (click)=\"submit()\">\n <mat-spinner *ngIf=\"isProcessing\" diameter=\"20\" class=\"inline-spinner\"></mat-spinner>\n <span *ngIf=\"!isProcessing\">Complete Signup</span>\n </button>\n </div>\n\n <div class=\"links-container\">\n <a mat-button color=\"primary\" (click)=\"goToLogin()\">Back to Login</a>\n </div>\n </div>\n </div>\n\n <!-- Complete Step -->\n <div *ngIf=\"step === 'complete'\" class=\"complete-section\">\n <mat-icon class=\"complete-icon\">check_circle</mat-icon>\n <h2 class=\"signup-title\">You're all set!</h2>\n <p class=\"signup-subtitle\">Welcome to {{appConfig.appName}}</p>\n <div class=\"button-container\">\n <button mat-flat-button color=\"primary\" (click)=\"goToDashboard()\">Go to Dashboard</button>\n </div>\n </div>\n\n </mat-card>\n</div>\n\n\n<!-- Default Style -->\n<div *ngIf=\"style=='default'\" class=\"default-signup background tin-bg-login\">\n <div class=\"container\">\n\n <div class=\"logo\">\n <img *ngIf=\"appConfig.logoSize=='normal'\" [src]=\"appConfig.logo\" style=\"width: 100px\" />\n <img *ngIf=\"appConfig.logoSize=='medium'\" [src]=\"appConfig.logo\" style=\"width: 150px\" />\n <img *ngIf=\"appConfig.logoSize=='large'\" [src]=\"appConfig.logo\" style=\"width: 250px\" />\n </div>\n\n <mat-card class=\"mat-elevation-z3\" style=\"width:400px;\">\n\n <!-- Provider Step -->\n <div *ngIf=\"step === 'provider'\">\n <mat-card-header style=\"margin-bottom: 30px; margin-top: 30px;\">\n <mat-card-title style=\"font-size: 28px; margin-bottom: 10px; margin-top: 20px; font-weight: 300\">Create Account</mat-card-title>\n <mat-card-subtitle>Get started with {{appConfig.appName}}</mat-card-subtitle>\n </mat-card-header>\n\n <mat-card-actions style=\"margin-bottom: 10px;\">\n <div *ngIf=\"appConfig.googleAuth\" class=\"button\">\n <asl-google-signin-button type='standard' width=\"320px\" size='medium' logo_alignment=\"center\" style=\"text-align: center;\"></asl-google-signin-button>\n </div>\n\n <div class=\"button\" *ngIf=\"appConfig.microsoftAuth\">\n <button mat-stroked-button color=\"primary\" style=\"width: 350px;\" (click)=\"loginWithMS()\">{{appConfig.microsoftAuthMessage ?? 'Continue with Microsoft'}}</button>\n </div>\n\n <div class=\"button\" style=\"margin-top: 20px;\">\n <a mat-button color=\"primary\" (click)=\"goToLogin()\">Already have an account? Log in</a>\n </div>\n </mat-card-actions>\n </div>\n\n <!-- Form Step (New Org) -->\n <div *ngIf=\"step === 'form'\">\n <mat-card-header style=\"margin-bottom: 20px; margin-top: 20px;\">\n <mat-card-title style=\"font-size: 28px; margin-bottom: 10px; font-weight: 300\">Almost there</mat-card-title>\n <mat-card-subtitle>Confirm your details to get started</mat-card-subtitle>\n </mat-card-header>\n\n <mat-card-content>\n <div class=\"signup-form-default\">\n <mat-form-field class=\"full-width\">\n <mat-label>First Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.firstName\">\n </mat-form-field>\n\n <mat-form-field class=\"full-width\">\n <mat-label>Last Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.lastName\">\n </mat-form-field>\n\n <mat-form-field class=\"full-width\">\n <mat-label>Email</mat-label>\n <input matInput [value]=\"signupData.email\" readonly>\n </mat-form-field>\n\n <mat-form-field class=\"full-width\">\n <mat-label>Organisation Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.orgName\">\n </mat-form-field>\n\n <mat-checkbox [(ngModel)]=\"signupData.termsAccepted\" color=\"primary\" class=\"terms-checkbox\">\n I agree to the\n <a (click)=\"openTerms(); $event.preventDefault()\" class=\"terms-link\">Terms</a>\n and\n <a (click)=\"openPrivacy(); $event.preventDefault()\" class=\"terms-link\">Privacy Policy</a>\n </mat-checkbox>\n </div>\n </mat-card-content>\n\n <mat-card-actions style=\"margin-bottom: 10px;\">\n <div class=\"button\">\n <button mat-flat-button color=\"primary\" style=\"width: 350px;\" [disabled]=\"isProcessing\" (click)=\"submit()\">\n <mat-spinner *ngIf=\"isProcessing\" diameter=\"20\" class=\"inline-spinner\"></mat-spinner>\n <span *ngIf=\"!isProcessing\">Create Account</span>\n </button>\n </div>\n <div class=\"button\">\n <a mat-button color=\"primary\" (click)=\"goToLogin()\">Back to Login</a>\n </div>\n </mat-card-actions>\n </div>\n\n <!-- Invitations Step -->\n <div *ngIf=\"step === 'invitations'\">\n <mat-card-header style=\"margin-bottom: 20px; margin-top: 20px;\">\n <mat-card-title style=\"font-size: 28px; margin-bottom: 10px; font-weight: 300\">You've been invited!</mat-card-title>\n <mat-card-subtitle>Accept an invitation to get started</mat-card-subtitle>\n </mat-card-header>\n\n <mat-card-content>\n <div class=\"signup-form-default\">\n <div class=\"invitation-list\">\n <mat-card *ngFor=\"let inv of signupData.invitations\" class=\"invitation-card\" [class.accepted]=\"inv.accepted\" (click)=\"toggleInvitation(inv)\">\n <div class=\"invitation-content\">\n <mat-icon [color]=\"inv.accepted ? 'primary' : ''\">{{inv.accepted ? 'check_circle' : 'circle'}}</mat-icon>\n <span class=\"invitation-org-name\">{{inv.tenantName}}</span>\n </div>\n </mat-card>\n </div>\n\n <mat-form-field class=\"full-width\">\n <mat-label>First Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.firstName\">\n </mat-form-field>\n\n <mat-form-field class=\"full-width\">\n <mat-label>Last Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.lastName\">\n </mat-form-field>\n\n <mat-checkbox [(ngModel)]=\"signupData.termsAccepted\" color=\"primary\" class=\"terms-checkbox\">\n I agree to the\n <a (click)=\"openTerms(); $event.preventDefault()\" class=\"terms-link\">Terms</a>\n and\n <a (click)=\"openPrivacy(); $event.preventDefault()\" class=\"terms-link\">Privacy Policy</a>\n </mat-checkbox>\n </div>\n </mat-card-content>\n\n <mat-card-actions style=\"margin-bottom: 10px;\">\n <div class=\"button\">\n <button mat-flat-button color=\"primary\" style=\"width: 350px;\" [disabled]=\"isProcessing\" (click)=\"submit()\">\n <mat-spinner *ngIf=\"isProcessing\" diameter=\"20\" class=\"inline-spinner\"></mat-spinner>\n <span *ngIf=\"!isProcessing\">Complete Signup</span>\n </button>\n </div>\n <div class=\"button\">\n <a mat-button color=\"primary\" (click)=\"goToLogin()\">Back to Login</a>\n </div>\n </mat-card-actions>\n </div>\n\n <!-- Complete Step -->\n <div *ngIf=\"step === 'complete'\" class=\"complete-section-default\">\n <mat-icon class=\"complete-icon-default\">check_circle</mat-icon>\n <h3 style=\"font-weight: 300; font-size: 28px;\">You're all set!</h3>\n <p style=\"color: rgba(0,0,0,0.6);\">Welcome to {{appConfig.appName}}</p>\n <div class=\"button\" style=\"margin-bottom: 20px;\">\n <button mat-flat-button color=\"primary\" style=\"width: 350px;\" (click)=\"goToDashboard()\">Go to Dashboard</button>\n </div>\n </div>\n\n </mat-card>\n\n </div>\n</div>\n", styles: [".modern-signup{min-height:100vh;display:flex;align-items:center;justify-content:center;background-color:#000}.signup-card{width:100%;max-width:440px;padding:24px}.logo-container{text-align:center;margin-bottom:24px}.header-section{text-align:center;margin-bottom:32px}.signup-title{margin:0 0 8px;font-size:32px;font-weight:300;letter-spacing:.5px;color:#000000de;text-align:center}.signup-subtitle{margin:0;font-size:14px;font-weight:300;color:#0000008a;letter-spacing:.25px;text-align:center}.signup-form{display:flex;flex-direction:column}.name-row{display:flex;gap:12px}.name-field{flex:1}.full-width{width:100%}.button-container{margin:16px 0}.button-container button{width:100%;border-radius:24px;height:40px}.provider-buttons{display:flex;flex-direction:column;align-items:center;gap:10px}.provider-buttons .button button{width:320px;border-radius:24px;height:40px}.social-login{display:flex;justify-content:center}.links-container{text-align:center;margin-top:16px}.login-link-container{margin-top:16px;color:#0009}.login-link-container span{margin-right:8px}.terms-checkbox{margin:12px 0;font-size:13px}.terms-link{color:inherit;text-decoration:underline;cursor:pointer}.inline-spinner{display:inline-block}.invitation-list{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}.invitation-card{cursor:pointer;padding:12px 16px;transition:all .2s ease;border:1px solid rgba(0,0,0,.12)}.invitation-card:hover{border-color:#0000003d}.invitation-card.accepted{border-color:var(--mdc-theme-primary, #3f51b5);background-color:#3f51b50a}.invitation-content{display:flex;align-items:center;gap:12px}.invitation-org-name{font-size:15px;font-weight:400}.invite-icon{font-size:40px;width:40px;height:40px;margin-bottom:8px}.complete-section{text-align:center;padding:40px 0}.complete-icon{font-size:64px;width:64px;height:64px;color:#4caf50;margin-bottom:16px}.default-signup{position:absolute;inset:0;overflow:auto;min-height:100%}.container{display:flex;flex-direction:column;align-items:center;height:100vh}.logo{margin-top:3em;margin-bottom:1em}.container mat-card-header{text-align:center;justify-content:center}.container mat-card-title{text-align:center}.container mat-card-actions{display:flex;flex-direction:column;align-items:center;justify-content:center}.button{display:flex;flex-direction:row;justify-content:center;margin-top:10px}.signup-form-default{padding:0 20px;display:flex;flex-direction:column}.complete-section-default{text-align:center;padding:40px 20px}.complete-icon-default{font-size:64px;width:64px;height:64px;color:#4caf50;margin-bottom:16px}\n"], dependencies: [{ kind: "directive", type: i2.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.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.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i4$1.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: i2$1.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$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { 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: i19.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i19.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i19.MatCardSubtitle, selector: "mat-card-subtitle, [mat-card-subtitle], [matCardSubtitle]" }, { kind: "directive", type: i19.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "component", type: i20$2.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "directive", type: i2$2.GoogleSigninButtonDirective, selector: "asl-google-signin-button", inputs: ["type", "size", "text", "shape", "theme", "logo_alignment", "width", "locale"] }] }); }
|
|
32645
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SignupComponent, isStandalone: false, selector: "spa-signup", ngImport: i0, template: "<!-- Modern Style -->\n<div *ngIf=\"style=='modern'\" class=\"modern-signup\">\n <mat-card class=\"signup-card\">\n\n <!-- Logo -->\n <div class=\"logo-container\">\n <img *ngIf=\"appConfig.logoSize=='normal'\" [src]=\"appConfig.logo\" style=\"width: 100px\" />\n <img *ngIf=\"appConfig.logoSize=='medium'\" [src]=\"appConfig.logo\" style=\"width: 150px\" />\n <img *ngIf=\"appConfig.logoSize=='large'\" [src]=\"appConfig.logo\" style=\"width: 250px\" />\n </div>\n\n <!-- Provider Step -->\n <div *ngIf=\"step === 'provider'\">\n <div class=\"header-section\">\n <h2 class=\"signup-title\">Create your account</h2>\n <p class=\"signup-subtitle\">Get started with {{appConfig.appName}}</p>\n </div>\n\n <div class=\"provider-buttons\">\n <div *ngIf=\"appConfig.googleAuth\" class=\"social-login\">\n <asl-google-signin-button type='standard' size='medium' width=\"320\" logo_alignment=\"center\" shape=\"pill\"></asl-google-signin-button>\n </div>\n\n <div class=\"button\" *ngIf=\"appConfig.microsoftAuth\">\n <button mat-stroked-button color=\"primary\" (click)=\"loginWithMS()\">{{appConfig.microsoftAuthMessage ?? 'Continue with Microsoft'}}</button>\n </div>\n </div>\n\n <div class=\"links-container\">\n <div class=\"login-link-container\">\n <span>Already have an account?</span>\n <a mat-button color=\"primary\" (click)=\"goToLogin()\">Log in</a>\n </div>\n </div>\n </div>\n\n <!-- Form Step (New Org) -->\n <div *ngIf=\"step === 'form'\">\n <div class=\"header-section\">\n <h2 class=\"signup-title\">Almost there</h2>\n <p class=\"signup-subtitle\">Confirm your details to get started</p>\n </div>\n\n <div class=\"signup-form\">\n <div class=\"name-row\">\n <mat-form-field appearance=\"outline\" class=\"name-field\">\n <mat-label>First Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.firstName\">\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" class=\"name-field\">\n <mat-label>Last Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.lastName\">\n </mat-form-field>\n </div>\n\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Email</mat-label>\n <input matInput [value]=\"signupData.email\" readonly>\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Organisation Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.orgName\">\n </mat-form-field>\n\n <mat-checkbox [(ngModel)]=\"signupData.termsAccepted\" color=\"primary\" class=\"terms-checkbox\">\n I agree to the\n <a (click)=\"openTerms(); $event.preventDefault()\" class=\"terms-link\">Terms and Conditions</a>\n and\n <a (click)=\"openPrivacy(); $event.preventDefault()\" class=\"terms-link\">Privacy Policy</a>\n </mat-checkbox>\n\n <div class=\"button-container\">\n <button mat-flat-button color=\"primary\" [disabled]=\"isProcessing\" (click)=\"submit()\">\n <mat-spinner *ngIf=\"isProcessing\" diameter=\"20\" class=\"inline-spinner\"></mat-spinner>\n <span *ngIf=\"!isProcessing\">Create Account</span>\n </button>\n </div>\n\n <div class=\"links-container\">\n <a mat-button color=\"primary\" (click)=\"goToLogin()\">Back to Login</a>\n </div>\n </div>\n </div>\n\n <!-- Invitations Step (Join Existing Org) -->\n <div *ngIf=\"step === 'invitations'\">\n <div class=\"header-section\">\n <mat-icon class=\"invite-icon\" color=\"primary\">mail</mat-icon>\n <h2 class=\"signup-title\">You've been invited!</h2>\n <p class=\"signup-subtitle\">Accept an invitation to get started</p>\n </div>\n\n <div class=\"signup-form\">\n <div class=\"invitation-list\">\n <mat-card *ngFor=\"let inv of signupData.invitations\" class=\"invitation-card\" [class.accepted]=\"inv.accepted\" (click)=\"toggleInvitation(inv)\">\n <div class=\"invitation-content\">\n <mat-icon [color]=\"inv.accepted ? 'primary' : ''\">{{inv.accepted ? 'check_circle' : 'circle'}}</mat-icon>\n <span class=\"invitation-org-name\">{{inv.tenantName}}</span>\n </div>\n </mat-card>\n </div>\n\n <div class=\"name-row\">\n <mat-form-field appearance=\"outline\" class=\"name-field\">\n <mat-label>First Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.firstName\">\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" class=\"name-field\">\n <mat-label>Last Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.lastName\">\n </mat-form-field>\n </div>\n\n <mat-checkbox [(ngModel)]=\"signupData.termsAccepted\" color=\"primary\" class=\"terms-checkbox\">\n I agree to the\n <a (click)=\"openTerms(); $event.preventDefault()\" class=\"terms-link\">Terms and Conditions</a>\n and\n <a (click)=\"openPrivacy(); $event.preventDefault()\" class=\"terms-link\">Privacy Policy</a>\n </mat-checkbox>\n\n <div class=\"button-container\">\n <button mat-flat-button color=\"primary\" [disabled]=\"isProcessing\" (click)=\"submit()\">\n <mat-spinner *ngIf=\"isProcessing\" diameter=\"20\" class=\"inline-spinner\"></mat-spinner>\n <span *ngIf=\"!isProcessing\">Complete Signup</span>\n </button>\n </div>\n\n <div class=\"links-container\">\n <a mat-button color=\"primary\" (click)=\"goToLogin()\">Back to Login</a>\n </div>\n </div>\n </div>\n\n <!-- Complete Step -->\n <div *ngIf=\"step === 'complete'\" class=\"complete-section\">\n <mat-icon class=\"complete-icon\">check_circle</mat-icon>\n <h2 class=\"signup-title\">You're all set!</h2>\n <p class=\"signup-subtitle\">Welcome to {{appConfig.appName}}</p>\n <div class=\"button-container\">\n <button mat-flat-button color=\"primary\" (click)=\"goToDashboard()\">Go to Dashboard</button>\n </div>\n </div>\n\n </mat-card>\n</div>\n\n\n<!-- Default Style -->\n<div *ngIf=\"style=='default'\" class=\"default-signup background tin-bg-login\">\n <div class=\"container\">\n\n <div class=\"logo\">\n <img *ngIf=\"appConfig.logoSize=='normal'\" [src]=\"appConfig.logo\" style=\"width: 100px\" />\n <img *ngIf=\"appConfig.logoSize=='medium'\" [src]=\"appConfig.logo\" style=\"width: 150px\" />\n <img *ngIf=\"appConfig.logoSize=='large'\" [src]=\"appConfig.logo\" style=\"width: 250px\" />\n </div>\n\n <mat-card class=\"mat-elevation-z3\" style=\"width:400px;\">\n\n <!-- Provider Step -->\n <div *ngIf=\"step === 'provider'\">\n <mat-card-header style=\"margin-bottom: 30px; margin-top: 30px;\">\n <mat-card-title style=\"font-size: 28px; margin-bottom: 10px; margin-top: 20px; font-weight: 300\">Create Account</mat-card-title>\n <mat-card-subtitle>Get started with {{appConfig.appName}}</mat-card-subtitle>\n </mat-card-header>\n\n <mat-card-actions style=\"margin-bottom: 10px;\">\n <div *ngIf=\"appConfig.googleAuth\" class=\"button\">\n <asl-google-signin-button type='standard' width=\"320px\" size='medium' logo_alignment=\"center\" style=\"text-align: center;\"></asl-google-signin-button>\n </div>\n\n <div class=\"button\" *ngIf=\"appConfig.microsoftAuth\">\n <button mat-stroked-button color=\"primary\" style=\"width: 350px;\" (click)=\"loginWithMS()\">{{appConfig.microsoftAuthMessage ?? 'Continue with Microsoft'}}</button>\n </div>\n\n <div class=\"button\" style=\"margin-top: 20px;\">\n <a mat-button color=\"primary\" (click)=\"goToLogin()\">Already have an account? Log in</a>\n </div>\n </mat-card-actions>\n </div>\n\n <!-- Form Step (New Org) -->\n <div *ngIf=\"step === 'form'\">\n <mat-card-header style=\"margin-bottom: 20px; margin-top: 20px;\">\n <mat-card-title style=\"font-size: 28px; margin-bottom: 10px; font-weight: 300\">Almost there</mat-card-title>\n <mat-card-subtitle>Confirm your details to get started</mat-card-subtitle>\n </mat-card-header>\n\n <mat-card-content>\n <div class=\"signup-form-default\">\n <mat-form-field class=\"full-width\">\n <mat-label>First Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.firstName\">\n </mat-form-field>\n\n <mat-form-field class=\"full-width\">\n <mat-label>Last Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.lastName\">\n </mat-form-field>\n\n <mat-form-field class=\"full-width\">\n <mat-label>Email</mat-label>\n <input matInput [value]=\"signupData.email\" readonly>\n </mat-form-field>\n\n <mat-form-field class=\"full-width\">\n <mat-label>Organisation Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.orgName\">\n </mat-form-field>\n\n <mat-checkbox [(ngModel)]=\"signupData.termsAccepted\" color=\"primary\" class=\"terms-checkbox\">\n I agree to the\n <a (click)=\"openTerms(); $event.preventDefault()\" class=\"terms-link\">Terms</a>\n and\n <a (click)=\"openPrivacy(); $event.preventDefault()\" class=\"terms-link\">Privacy Policy</a>\n </mat-checkbox>\n </div>\n </mat-card-content>\n\n <mat-card-actions style=\"margin-bottom: 10px;\">\n <div class=\"button\">\n <button mat-flat-button color=\"primary\" style=\"width: 350px;\" [disabled]=\"isProcessing\" (click)=\"submit()\">\n <mat-spinner *ngIf=\"isProcessing\" diameter=\"20\" class=\"inline-spinner\"></mat-spinner>\n <span *ngIf=\"!isProcessing\">Create Account</span>\n </button>\n </div>\n <div class=\"button\">\n <a mat-button color=\"primary\" (click)=\"goToLogin()\">Back to Login</a>\n </div>\n </mat-card-actions>\n </div>\n\n <!-- Invitations Step -->\n <div *ngIf=\"step === 'invitations'\">\n <mat-card-header style=\"margin-bottom: 20px; margin-top: 20px;\">\n <mat-card-title style=\"font-size: 28px; margin-bottom: 10px; font-weight: 300\">You've been invited!</mat-card-title>\n <mat-card-subtitle>Accept an invitation to get started</mat-card-subtitle>\n </mat-card-header>\n\n <mat-card-content>\n <div class=\"signup-form-default\">\n <div class=\"invitation-list\">\n <mat-card *ngFor=\"let inv of signupData.invitations\" class=\"invitation-card\" [class.accepted]=\"inv.accepted\" (click)=\"toggleInvitation(inv)\">\n <div class=\"invitation-content\">\n <mat-icon [color]=\"inv.accepted ? 'primary' : ''\">{{inv.accepted ? 'check_circle' : 'circle'}}</mat-icon>\n <span class=\"invitation-org-name\">{{inv.tenantName}}</span>\n </div>\n </mat-card>\n </div>\n\n <mat-form-field class=\"full-width\">\n <mat-label>First Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.firstName\">\n </mat-form-field>\n\n <mat-form-field class=\"full-width\">\n <mat-label>Last Name</mat-label>\n <input matInput [(ngModel)]=\"signupData.lastName\">\n </mat-form-field>\n\n <mat-checkbox [(ngModel)]=\"signupData.termsAccepted\" color=\"primary\" class=\"terms-checkbox\">\n I agree to the\n <a (click)=\"openTerms(); $event.preventDefault()\" class=\"terms-link\">Terms</a>\n and\n <a (click)=\"openPrivacy(); $event.preventDefault()\" class=\"terms-link\">Privacy Policy</a>\n </mat-checkbox>\n </div>\n </mat-card-content>\n\n <mat-card-actions style=\"margin-bottom: 10px;\">\n <div class=\"button\">\n <button mat-flat-button color=\"primary\" style=\"width: 350px;\" [disabled]=\"isProcessing\" (click)=\"submit()\">\n <mat-spinner *ngIf=\"isProcessing\" diameter=\"20\" class=\"inline-spinner\"></mat-spinner>\n <span *ngIf=\"!isProcessing\">Complete Signup</span>\n </button>\n </div>\n <div class=\"button\">\n <a mat-button color=\"primary\" (click)=\"goToLogin()\">Back to Login</a>\n </div>\n </mat-card-actions>\n </div>\n\n <!-- Complete Step -->\n <div *ngIf=\"step === 'complete'\" class=\"complete-section-default\">\n <mat-icon class=\"complete-icon-default\">check_circle</mat-icon>\n <h3 style=\"font-weight: 300; font-size: 28px;\">You're all set!</h3>\n <p style=\"color: rgba(0,0,0,0.6);\">Welcome to {{appConfig.appName}}</p>\n <div class=\"button\" style=\"margin-bottom: 20px;\">\n <button mat-flat-button color=\"primary\" style=\"width: 350px;\" (click)=\"goToDashboard()\">Go to Dashboard</button>\n </div>\n </div>\n\n </mat-card>\n\n </div>\n</div>\n", styles: [".modern-signup{min-height:100vh;display:flex;align-items:center;justify-content:center;background-color:#000}.signup-card{width:100%;max-width:440px;padding:24px}.logo-container{text-align:center;margin-bottom:24px}.header-section{text-align:center;margin-bottom:32px}.signup-title{margin:0 0 8px;font-size:32px;font-weight:300;letter-spacing:.5px;color:#000000de;text-align:center}.signup-subtitle{margin:0;font-size:14px;font-weight:300;color:#0000008a;letter-spacing:.25px;text-align:center}.signup-form{display:flex;flex-direction:column}.name-row{display:flex;gap:12px}.name-field{flex:1}.full-width{width:100%}.button-container{margin:16px 0}.button-container button{width:100%;border-radius:24px;height:40px}.provider-buttons{display:flex;flex-direction:column;align-items:center;gap:10px}.provider-buttons .button button{width:320px;border-radius:24px;height:40px}.social-login{display:flex;justify-content:center}.links-container{text-align:center;margin-top:16px}.login-link-container{margin-top:16px;color:#0009}.login-link-container span{margin-right:8px}.terms-checkbox{margin:12px 0;font-size:13px}.terms-link{color:inherit;text-decoration:underline;cursor:pointer}.inline-spinner{display:inline-block}.invitation-list{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}.invitation-card{cursor:pointer;padding:12px 16px;transition:all .2s ease;border:1px solid rgba(0,0,0,.12)}.invitation-card:hover{border-color:#0000003d}.invitation-card.accepted{border-color:var(--mdc-theme-primary, #3f51b5);background-color:#3f51b50a}.invitation-content{display:flex;align-items:center;gap:12px}.invitation-org-name{font-size:15px;font-weight:400}.invite-icon{font-size:40px;width:40px;height:40px;margin-bottom:8px}.complete-section{text-align:center;padding:40px 0}.complete-icon{font-size:64px;width:64px;height:64px;color:#4caf50;margin-bottom:16px}.default-signup{position:absolute;inset:0;overflow:auto;min-height:100%}.container{display:flex;flex-direction:column;align-items:center;height:100vh}.logo{margin-top:3em;margin-bottom:1em}.container mat-card-header{text-align:center;justify-content:center}.container mat-card-title{text-align:center}.container mat-card-actions{display:flex;flex-direction:column;align-items:center;justify-content:center}.button{display:flex;flex-direction:row;justify-content:center;margin-top:10px}.signup-form-default{padding:0 20px;display:flex;flex-direction:column}.complete-section-default{text-align:center;padding:40px 20px}.complete-icon-default{font-size:64px;width:64px;height:64px;color:#4caf50;margin-bottom:16px}\n"], dependencies: [{ kind: "directive", type: i2.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.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.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i4$1.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: i2$1.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$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { 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: i19.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i19.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i19.MatCardSubtitle, selector: "mat-card-subtitle, [mat-card-subtitle], [matCardSubtitle]" }, { kind: "directive", type: i19.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "component", type: i20$1.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "directive", type: i2$2.GoogleSigninButtonDirective, selector: "asl-google-signin-button", inputs: ["type", "size", "text", "shape", "theme", "logo_alignment", "width", "locale"] }] }); }
|
|
32404
32646
|
}
|
|
32405
32647
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SignupComponent, decorators: [{
|
|
32406
32648
|
type: Component,
|
|
@@ -38882,7 +39124,7 @@ class BillingPageComponent {
|
|
|
38882
39124
|
</mat-card>
|
|
38883
39125
|
</div>
|
|
38884
39126
|
</div>
|
|
38885
|
-
`, isInline: true, styles: [".billing-container{padding:16px;max-width:1200px}.page-title{display:flex;align-items:center;gap:8px;margin-bottom:16px;color:#333;font-weight:500}.summary-card{margin-bottom:24px}.summary-row{display:flex;gap:32px;flex-wrap:wrap}.summary-item{display:flex;flex-direction:column}.summary-label{font-size:12px;color:#888;text-transform:uppercase}.summary-value{font-size:16px;font-weight:500}.status-badge{padding:2px 10px;border-radius:12px;font-size:12px;font-weight:500}.status-0{background:#e3f2fd;color:#1565c0}.status-1{background:#e8f5e9;color:#2e7d32}.status-2{background:#fff3e0;color:#e65100}.status-3{background:#fce4ec;color:#c62828}.status-4{background:#f5f5f5;color:#616161}.invoices-card{margin-bottom:24px}.table-container{overflow-x:auto}.invoice-table,.transaction-table{width:100%;border-collapse:collapse}.invoice-table th,.invoice-table td,.transaction-table th,.transaction-table td{padding:10px 12px;text-align:left;border-bottom:1px solid #e0e0e0;font-size:14px}.invoice-table th,.transaction-table th{font-weight:500;color:#666;font-size:12px;text-transform:uppercase}.invoice-table tbody tr:hover{background:#f5f5f5}.invoice-status{padding:2px 8px;border-radius:10px;font-size:12px;font-weight:500}.inv-status-0{background:#fff3e0;color:#e65100}.inv-status-1{background:#e8f5e9;color:#2e7d32}.inv-status-2{background:#fce4ec;color:#c62828}.inv-status-3{background:#f3e5f5;color:#6a1b9a}.txn-status{padding:2px 8px;border-radius:10px;font-size:12px;font-weight:500}.txn-status-0{background:#e3f2fd;color:#1565c0}.txn-status-1{background:#fff3e0;color:#e65100}.txn-status-2{background:#e8f5e9;color:#2e7d32}.txn-status-3{background:#fce4ec;color:#c62828}.txn-status-4{background:#f5f5f5;color:#616161}.empty-state,.loading-state{display:flex;flex-direction:column;align-items:center;padding:32px;color:#999}.empty-state mat-icon{font-size:48px;width:48px;height:48px;margin-bottom:8px}.overlay{position:fixed;inset:0;background:#0006;display:flex;align-items:center;justify-content:center;z-index:1000}.detail-dialog{max-width:640px;width:90%;max-height:80vh;overflow-y:auto}.payment-dialog{max-width:440px;width:90%}.close-btn{position:absolute;top:8px;right:8px}.detail-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px}.detail-field{display:flex;flex-direction:column}.field-label{font-size:12px;color:#888;text-transform:uppercase}.field-value{font-size:14px;font-weight:500}.transactions-section{margin-top:16px}.transactions-section h5{margin:0 0 8px;color:#555}.no-transactions{color:#999;font-size:13px}.payment-summary{margin-bottom:16px}.payment-summary p{margin:4px 0;font-size:15px}.payment-state{display:flex;flex-direction:column;align-items:center;padding:24px;text-align:center}.result-icon{font-size:48px;width:48px;height:48px;margin-bottom:8px}.success-icon{color:#4caf50}.redirect-icon{color:#1976d2}.error-icon{color:#f44336}.payment-ref{font-size:12px;color:#888}\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: i2$1.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: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$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: i19.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i19.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i19.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "component", type: i20$
|
|
39127
|
+
`, isInline: true, styles: [".billing-container{padding:16px;max-width:1200px}.page-title{display:flex;align-items:center;gap:8px;margin-bottom:16px;color:#333;font-weight:500}.summary-card{margin-bottom:24px}.summary-row{display:flex;gap:32px;flex-wrap:wrap}.summary-item{display:flex;flex-direction:column}.summary-label{font-size:12px;color:#888;text-transform:uppercase}.summary-value{font-size:16px;font-weight:500}.status-badge{padding:2px 10px;border-radius:12px;font-size:12px;font-weight:500}.status-0{background:#e3f2fd;color:#1565c0}.status-1{background:#e8f5e9;color:#2e7d32}.status-2{background:#fff3e0;color:#e65100}.status-3{background:#fce4ec;color:#c62828}.status-4{background:#f5f5f5;color:#616161}.invoices-card{margin-bottom:24px}.table-container{overflow-x:auto}.invoice-table,.transaction-table{width:100%;border-collapse:collapse}.invoice-table th,.invoice-table td,.transaction-table th,.transaction-table td{padding:10px 12px;text-align:left;border-bottom:1px solid #e0e0e0;font-size:14px}.invoice-table th,.transaction-table th{font-weight:500;color:#666;font-size:12px;text-transform:uppercase}.invoice-table tbody tr:hover{background:#f5f5f5}.invoice-status{padding:2px 8px;border-radius:10px;font-size:12px;font-weight:500}.inv-status-0{background:#fff3e0;color:#e65100}.inv-status-1{background:#e8f5e9;color:#2e7d32}.inv-status-2{background:#fce4ec;color:#c62828}.inv-status-3{background:#f3e5f5;color:#6a1b9a}.txn-status{padding:2px 8px;border-radius:10px;font-size:12px;font-weight:500}.txn-status-0{background:#e3f2fd;color:#1565c0}.txn-status-1{background:#fff3e0;color:#e65100}.txn-status-2{background:#e8f5e9;color:#2e7d32}.txn-status-3{background:#fce4ec;color:#c62828}.txn-status-4{background:#f5f5f5;color:#616161}.empty-state,.loading-state{display:flex;flex-direction:column;align-items:center;padding:32px;color:#999}.empty-state mat-icon{font-size:48px;width:48px;height:48px;margin-bottom:8px}.overlay{position:fixed;inset:0;background:#0006;display:flex;align-items:center;justify-content:center;z-index:1000}.detail-dialog{max-width:640px;width:90%;max-height:80vh;overflow-y:auto}.payment-dialog{max-width:440px;width:90%}.close-btn{position:absolute;top:8px;right:8px}.detail-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px}.detail-field{display:flex;flex-direction:column}.field-label{font-size:12px;color:#888;text-transform:uppercase}.field-value{font-size:14px;font-weight:500}.transactions-section{margin-top:16px}.transactions-section h5{margin:0 0 8px;color:#555}.no-transactions{color:#999;font-size:13px}.payment-summary{margin-bottom:16px}.payment-summary p{margin:4px 0;font-size:15px}.payment-state{display:flex;flex-direction:column;align-items:center;padding:24px;text-align:center}.result-icon{font-size:48px;width:48px;height:48px;margin-bottom:8px}.success-icon{color:#4caf50}.redirect-icon{color:#1976d2}.error-icon{color:#f44336}.payment-ref{font-size:12px;color:#888}\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: i2$1.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: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$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: i19.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i19.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i19.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "component", type: i20$1.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "directive", type: i7$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "pipe", type: i1$2.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$2.DatePipe, name: "date" }] }); }
|
|
38886
39128
|
}
|
|
38887
39129
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BillingPageComponent, decorators: [{
|
|
38888
39130
|
type: Component,
|