tin-spa 20.14.29 → 20.14.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -375,7 +375,22 @@ class Core {
375
375
  return false;
376
376
  }
377
377
  static getInitialValue(field) {
378
- if (field.defaultValue) {
378
+ // Changed (E21): was `if (field.defaultValue)` — a TRUTHINESS test, so a deliberately falsy default
379
+ // (0, false, '') was silently ignored and the field fell through to the type default below. For most
380
+ // types that is harmless, because the fallthrough happens to land on the same value anyway (number -> 0,
381
+ // checkbox -> false, text -> ''). For a SELECT it is not: the fallthrough is `return null`, and a null
382
+ // posted into a non-nullable backend enum is rejected at model binding with an HTTP 400 that the user
383
+ // sees as the generic "Something went wrong ... a technical problem in the app" panel
384
+ // (api-error.service.ts:138-144). Nothing is logged server-side, because a binding failure is not an
385
+ // exception — which is exactly why this stayed hidden through a 124k-line API log.
386
+ //
387
+ // Measured before changing it: across ng-space and all four consumer apps exactly 11 fields declare a
388
+ // falsy defaultValue. Seven are number/money/checkbox and are unaffected, their fallthrough already
389
+ // producing the same value. The other four are selects that asked for 0 and were silently getting null:
390
+ // accounting.service.ts:152 itemType, :574 kind, assets.service.ts:25 defaultDepreciationMethod and
391
+ // :115 disposalType — three of them required, so users were being made to re-pick a value the config had
392
+ // already chosen for them. This is therefore behaviour-neutral everywhere except where it repairs a bug.
393
+ if (field.defaultValue !== undefined && field.defaultValue !== null) {
379
394
  if ((field.type == 'date' || field.type == 'datetime') && field.defaultValue == 'now')
380
395
  return this.nowDate(true);
381
396
  return field.defaultValue;
@@ -6766,7 +6781,13 @@ class AccountingService {
6766
6781
  { name: 'code', type: 'text', alias: 'Account Code', section: 'classification', infoMessage: 'Chart of accounts code (1xxx assets, 2xxx liabilities, 3xxx equity, 4xxx revenue, 5xxx expenses)' }, // Changed: COA code (B1); moved into the collapsed section
6767
6782
  { name: 'includeInCashTotal', type: 'checkbox', alias: 'Include in Cash Total', section: 'classification', hidden: (data) => data.type !== 0 }, // Changed: Only visible for Asset accounts (type 0)
6768
6783
  { name: 'includeInBankTotal', type: 'checkbox', alias: 'Include in Bank Total', section: 'classification', hidden: (data) => data.type !== 0 }, // Changed: Only visible for Asset accounts (type 0)
6769
- { name: 'cashFlow', type: 'select', alias: 'Cash Flow Section', section: 'classification', infoMessage: 'IAS 7 cash flow statement classification', // Changed: C5 cash flow category
6784
+ // Changed (E21): defaultValue added. An untouched select initialises to null (TinCore.getInitialValue
6785
+ // `case 'select': return null`), and Account.CashFlow is a NON-NULLABLE CashFlowCategory, so creating an
6786
+ // account without opening this collapsed section posted cashFlow:null and was rejected at model binding
6787
+ // with a 400 — surfaced to the user as "Something went wrong ... a technical problem in the app" and
6788
+ // logged nowhere. 0 is Operating, which is the model's own C# default (Account.cs:69), so this makes the
6789
+ // form agree with the entity rather than inventing a value.
6790
+ { name: 'cashFlow', type: 'select', alias: 'Cash Flow Section', section: 'classification', defaultValue: 0, infoMessage: 'IAS 7 cash flow statement classification', // Changed: C5 cash flow category
6770
6791
  options: [
6771
6792
  { name: 'Operating', value: 0 },
6772
6793
  { name: 'Investing', value: 1 },
@@ -17362,6 +17383,7 @@ class TableComponent {
17362
17383
  this.apiErrorService = apiErrorService;
17363
17384
  this.runtimeConfig = runtimeConfig;
17364
17385
  this.subs = []; // TS-8: breakpoint + parent-owned reload subscriptions leaked on table teardown; stacked reload subs also caused duplicate loadData fetches
17386
+ this.initialized = false; // Added: guards ensureInitialized — initialization must happen exactly once, whichever hook reaches it first
17365
17387
  this.elevation = "mat-elevation-z5";
17366
17388
  this.actionsWidth = "50px";
17367
17389
  // Added: collapsible flat section header (sectionConfig) — Day Book sections
@@ -17465,6 +17487,17 @@ class TableComponent {
17465
17487
  }));
17466
17488
  }
17467
17489
  ngOnInit() {
17490
+ this.ensureInitialized(); // Changed: the body moved to ensureInitialized so ngOnChanges can pull it forward when it has to start a load first
17491
+ }
17492
+ // Added: Angular runs ngOnChanges BEFORE ngOnInit on the first pass. A lazy tab that is already active on the
17493
+ // first binding (a details dialog's tableConfigs[0]) therefore started its load from ngOnChanges while
17494
+ // setupPagination() had not yet run, so pagedMode was still false and the rows landed on the legacy path;
17495
+ // ngOnInit then flipped pagedMode on and the next updateSlice() drew from the empty loadedRows window and
17496
+ // replaced the good rows with nothing. Initialization is now idempotent so the load site can order it first.
17497
+ ensureInitialized() {
17498
+ if (this.initialized)
17499
+ return;
17500
+ this.initialized = true;
17468
17501
  this.sectionCollapsed = !!this.config?.sectionConfig?.collapsed; // Added: seed section collapse from config — state lives on the component, never written back to the shared config
17469
17502
  if (this.config?.formConfig) {
17470
17503
  this.hasFormAccess = Core.hasFormAccess(this.config.formConfig, this.authService.currentRoleSource.value);
@@ -17487,6 +17520,7 @@ class TableComponent {
17487
17520
  }
17488
17521
  if (this.inTab && changes['activeTab']) {
17489
17522
  if (!this.hasBeenActivated && this.activeTab && this.config?.lazyLoad && this.config.loadAction) {
17523
+ this.ensureInitialized(); // Changed: this is the ONLY load ngOnChanges can start, and it must not run ahead of setupPagination() — see ensureInitialized
17490
17524
  this.loadData(this.config.loadAction, "");
17491
17525
  this.hasBeenActivated = true;
17492
17526
  }
@@ -18548,13 +18582,17 @@ class TableComponent {
18548
18582
  return "mat-elevation-z5";
18549
18583
  }
18550
18584
  }
18551
- // Changed: Real-time tables fall back to a refresh when SignalR is disconnected.
18585
+ // Changed: this now ALWAYS refreshes. The old body refreshed only when realTime was OFF or SignalR was DOWN,
18586
+ // so a healthy real-time table did nothing after a write and simply waited for a broadcast. But the SignalR
18587
+ // streams below are filtered on this table's own entityName, and an action that writes a DIFFERENT entity
18588
+ // (invoicing a rental writes an Invoice) never emits a matching one — grip's rentals grid kept rendering
18589
+ // "Returned" for a row already Invoiced in the database. Nothing correlates a broadcast back to the action
18590
+ // just performed, so "refresh unless a broadcast arrives" is not implementable without new machinery.
18591
+ // A same-URL re-read after a user-initiated write is idempotent; a broadcast that does arrive patches the same rows.
18552
18592
  // In paged mode this is NOT a full reload — refreshClicked routes through loadDataPaged's same-URL branch,
18553
18593
  // which re-fetches only the loaded window (skip=0, take=loadedCount), never the whole dataset.
18554
18594
  realTimeRefreshOrFallback() {
18555
- if (!this.effRealTime || !this.isSignalRConnected) { // Changed: resolves via the app-wide default
18556
- this.refreshClicked();
18557
- }
18595
+ this.refreshClicked(); // Changed: unconditional — was `if (!this.effRealTime || !this.isSignalRConnected)`, which made the healthy real-time case a silent no-op
18558
18596
  }
18559
18597
  //---------------- TinSync offline support ----------------
18560
18598
  // Registers the table's URLs with the offline service, starts the sync engine, and wires live overlay updates