tin-spa 20.14.57 → 20.14.58

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.
@@ -3564,11 +3564,15 @@ class AuthService {
3564
3564
  }
3565
3565
  sessionExpired() {
3566
3566
  this.clearSession(false); // Changed: Don't revoke on server, token just expired
3567
- let url = new URL(window.location.href);
3568
- let path = url.hash.replace("#/", "");
3567
+ // Changed: the path came from url.hash, which is EMPTY under HTML5 routing — every current app runs
3568
+ // HTML5 routing, so the redirect target was always blank and the sign-back-in landed on the default page
3569
+ // instead of the page the user was on. router.url is the in-app path under either routing mode.
3570
+ let path = this.router.url.startsWith('/') ? this.router.url.substring(1) : this.router.url;
3571
+ if (path.startsWith('login'))
3572
+ path = ''; // never bounce back into the login page itself
3569
3573
  this.router.navigate(["login"], {
3570
3574
  relativeTo: this._route,
3571
- queryParams: { redirectTo: path },
3575
+ queryParams: path ? { redirectTo: path } : {}, // Changed: an empty redirectTo must be ABSENT, not present-and-blank — a blank param overrides the saved last-route with nothing
3572
3576
  queryParamsHandling: 'merge',
3573
3577
  skipLocationChange: false
3574
3578
  });
@@ -3971,6 +3975,7 @@ class DataServiceLib {
3971
3975
  this.capAging = new CapItem; // Added: Capability for aging report
3972
3976
  this.capTaxRates = new CapItem; // Changed: Capability for tax rates management
3973
3977
  this.capStandingOrders = new CapItem; // Changed: Capability for standing orders management
3978
+ this.capTenantTransfers = new CapItem; // Added (N11): Organisation Transfers — cross-organisation money, accounting menu
3974
3979
  this.capFixedAssetsModule = new CapItem; // Changed: Top-level Fixed Assets module
3975
3980
  this.capFixedAssetsDashboard = new CapItem; // Changed: Fixed Assets dashboard
3976
3981
  this.capFixedAssets = new CapItem; // Changed: Asset Register (renamed from Fixed Assets)
@@ -5095,7 +5100,7 @@ class DataServiceLib {
5095
5100
  this.capAccounting.display = "Accounting";
5096
5101
  this.capAccounting.moduleKey = "accounting"; // Added (Setup v3)
5097
5102
  this.capAccounting.icon = "account_balance";
5098
- this.capAccounting.capSubItems = [this.capAccountingDashboard, this.capAccounts, this.capTransactions, this.capAggregates, this.capReports, this.capVatReturn, this.capFiscalPeriods, this.capBankReconciliation, this.capBudgets, this.capBudgetVsActual, this.capTransactionTypes, this.capTaxRates, this.capCurrencies, this.capStandingOrders]; // Changed: Added Bank Reconciliation (C4) after Fiscal Periods
5103
+ this.capAccounting.capSubItems = [this.capAccountingDashboard, this.capAccounts, this.capTransactions, this.capAggregates, this.capReports, this.capVatReturn, this.capFiscalPeriods, this.capBankReconciliation, this.capTenantTransfers, this.capBudgets, this.capBudgetVsActual, this.capTransactionTypes, this.capTaxRates, this.capCurrencies, this.capStandingOrders]; // Changed: Added Bank Reconciliation (C4) after Fiscal Periods
5099
5104
  this.capAccountingDashboard.name = "cap27"; // Changed: Reuses module cap number for dashboard visibility
5100
5105
  this.capAccountingDashboard.display = "Dashboard";
5101
5106
  this.capAccountingDashboard.link = "home/accounting/dashboard";
@@ -5150,6 +5155,21 @@ class DataServiceLib {
5150
5155
  this.capStandingOrders.display = "Standing Orders";
5151
5156
  this.capStandingOrders.link = "home/accounting/standing-orders";
5152
5157
  this.capStandingOrders.icon = "schedule";
5158
+ // Added (N11): Organisation Transfers — cross-organisation money arrangements and transfers, shared page,
5159
+ // sits in the ACCOUNTING menu by owner directive (2026-08-17). Replaces grip-spa's app-local page and its
5160
+ // unrecorded cap111 (cap111 is in the consumer-app range and is Piglet's Treatments — the collision was
5161
+ // only survivable because roles are per-app).
5162
+ //
5163
+ // ── HOW cap91 WAS CHOSEN — same rule as cap87/cap89/cap90 (TinWeb Contracts\Capabilities.cs census) ──
5164
+ // The census on 2026-08-17 (cap90's) found "cap90" and "cap91" used by NOTHING anywhere — backend,
5165
+ // tin-spa, consumer apps, Kotlin. cap90 took Shared Reference Data, so 91 is the next number that is both
5166
+ // free and NEVER-ASSIGNED (88 stays skipped — vacated numbers can hold stale grants). Like cap89 this
5167
+ // gates the MENU ENTRY and the dialogs' security allow-lists; the server authority remains
5168
+ // CrossTenantTransfersController's IsAdmin() test on every verb.
5169
+ this.capTenantTransfers.name = "cap91";
5170
+ this.capTenantTransfers.display = "Organisation Transfers";
5171
+ this.capTenantTransfers.link = "home/accounting/tenant-transfers";
5172
+ this.capTenantTransfers.icon = "swap_horiz";
5153
5173
  // Changed: Fixed Assets is now its own top-level module
5154
5174
  this.capFixedAssetsModule.name = "cap70";
5155
5175
  this.capFixedAssetsModule.display = "Fixed Assets";
@@ -12647,6 +12667,231 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
12647
12667
  args: [{ providedIn: 'root' }]
12648
12668
  }], ctorParameters: () => [{ type: i1$1.Router }, { type: StorageService }, { type: DataServiceLib }] });
12649
12669
 
12670
+ // Tenant transfer service — page/table/form configs for cross-organisation money arrangements.
12671
+ //
12672
+ // Moved (N11) from grip-spa into tin-spa: the backend is shared library code (TinWeb
12673
+ // CrossTenantTransfersController), so the page belongs to the library too — all four apps now get it, gated
12674
+ // by cap91 and routed under Accounting (owner directive 2026-08-17: "defined in the accounting module and menu").
12675
+ // The design commentary from the Grip original carries over verbatim where it still applies.
12676
+ //
12677
+ // The backend for this is DELIBERATELY NOT a BaseController: TenantLink and TenantTransfer sit outside
12678
+ // ITenant so BOTH parties can read the same row, so there is no `?action=list` and no generic CRUD. Every
12679
+ // action here is an explicit verb, and the whole authority model lives server-side in
12680
+ // CrossTenantTransferService. Consequences:
12681
+ // 1. Every loadAction is a plain GET returning RepoResponse.Data.
12682
+ // 2. `revoke` is POST crosstenanttransfers/links/{id}/revoke — a PATH parameter, so it is an onClick
12683
+ // handler that builds the url itself. Same for decline, which needs only a confirmation.
12684
+ // 3. Accept and Send Money DO need input, so those are real dialogs with their own formConfig.
12685
+ //
12686
+ // N11 additions on top of the Grip original:
12687
+ // • CROSS-APPLICATION — the propose dialog asks WHICH application first; the organisation list then loads
12688
+ // from that application via the {applicationCode} placeholder (select-common re-fetches on change).
12689
+ // • The transfers caption now states that a cross-application receiver's journal lives in the other
12690
+ // application, correlated by the shared Reference.
12691
+ class TenantTransferService {
12692
+ constructor() {
12693
+ this.dataService = inject(DataServiceLib);
12694
+ this.messageService = inject(MessageService); // decline/revoke confirm from an onClick handler — a pure-onClick row button is terminal in table.component.actionClicked, so the framework confirm never runs for it
12695
+ // Added: the two tables are one story — Send Money is a button on the ARRANGEMENTS table, so a successful
12696
+ // action there must also refresh the Transfers list underneath. The page component wires these.
12697
+ this.transfersReload = new Subject();
12698
+ // Changed (N11): which side is "me" is now computed SERVER-SIDE (GetLinks projects iAmSender/iAmReceiver)
12699
+ // — a client-side tenant-id comparison can collide across applications, because a cross-app link carries a
12700
+ // foreign id from a different database. The client just reads the flags.
12701
+ this.isPending = (x) => x?.status === 'Pending';
12702
+ this.iAmReceiver = (x) => !!x?.iAmReceiver; // the ONLY side that may accept
12703
+ this.iAmSender = (x) => !!x?.iAmSender; // the ONLY side that may send
12704
+ // --------------------------Arrangements (the handshake)-------------------------
12705
+ this.proposeFormConfig = {
12706
+ security: { allow: [this.dataService.capTenantTransfers] }, // capability gate — the server refuses these verbs to a non-admin anyway, so the dialog should not offer them either
12707
+ title: 'Propose an arrangement',
12708
+ alertConfig: {
12709
+ compact: true,
12710
+ messages: [
12711
+ { type: 'info', message: 'The other organisation must accept before any money can move. You cannot send to an organisation that has not agreed to receive from you.' },
12712
+ { type: 'warn', message: 'An arrangement is ONE-WAY. It lets YOU send to them. For money to come back the other way, they must propose a separate arrangement to you.' },
12713
+ ]
12714
+ },
12715
+ fields: [
12716
+ // Added (N11): application first, then that application's organisations. The {applicationCode}
12717
+ // placeholder makes the second select re-fetch whenever the first changes.
12718
+ { name: 'applicationCode', type: 'select', alias: 'Application', required: true, span: true, loadAction: { url: 'crosstenanttransfers/applications' }, optionDisplay: 'name', optionValue: 'code', hint: 'Which system the other organisation uses' },
12719
+ { name: 'toTenantID', type: 'select', alias: 'Organisation', required: true, span: true, loadAction: { url: 'crosstenanttransfers/counterparties?application={applicationCode}' }, optionDisplay: 'name', optionValue: 'tenantID', hint: 'Who you want to be able to send money to' },
12720
+ { name: 'accountID', type: 'select', alias: 'Your funding account', span: true, loadAction: { url: 'accounts/list/x' }, nullable: true, hint: 'The account the money leaves. Leave blank to use Bank Account.' },
12721
+ { name: 'note', type: 'text-area', alias: 'Note', rows: 2, span: true, hint: 'Shown to the other organisation when they decide whether to accept' },
12722
+ ]
12723
+ };
12724
+ // 🔴 The dialog's SUBMIT button is resolved by NAME ('create') — see the two-step-create ordering rule.
12725
+ this.proposeSubmitButton = { name: 'create', display: 'Propose', action: { url: 'crosstenanttransfers/links/propose', method: 'post', successMessage: 'Proposed. It is now waiting for the other organisation to accept.' } };
12726
+ this.proposeDetailsConfig = {
12727
+ formConfig: this.proposeFormConfig,
12728
+ mode: 'create',
12729
+ buttons: [this.proposeSubmitButton]
12730
+ };
12731
+ this.proposeButton = { name: 'propose', display: 'Propose Arrangement', icon: { name: 'handshake' }, inHeader: true, dialog: true, detailsConfig: this.proposeDetailsConfig };
12732
+ this.acceptFormConfig = {
12733
+ security: { allow: [this.dataService.capTenantTransfers] },
12734
+ title: 'Accept this arrangement',
12735
+ alertConfig: {
12736
+ compact: true,
12737
+ messages: [
12738
+ { type: 'info', message: 'Accepting lets the other organisation send money INTO your books. It does not let you send money to them — that would be a separate arrangement, proposed by you.' },
12739
+ ]
12740
+ },
12741
+ fields: [
12742
+ { name: 'accept', type: 'checkbox', hidden: true, defaultValue: true },
12743
+ { name: 'accountID', type: 'select', alias: 'Your receiving account', span: true, loadAction: { url: 'accounts/list/x' }, nullable: true, hint: 'Where incoming money lands. Leave blank to use Bank Account.' },
12744
+ ]
12745
+ };
12746
+ this.acceptSubmitButton = { name: 'create', display: 'Accept Arrangement', action: { url: 'crosstenanttransfers/links/respond', method: 'post', successMessage: 'Accepted. Money can now flow to you over this arrangement.' } };
12747
+ this.acceptDetailsConfig = {
12748
+ formConfig: this.acceptFormConfig,
12749
+ mode: 'create', // create merges the clicked row over the generated model, which is how tenantLinkID reaches the DTO
12750
+ buttons: [this.acceptSubmitButton]
12751
+ };
12752
+ this.acceptButton = { name: 'accept', display: 'Accept', icon: { name: 'check_circle' }, color: 'primary', dialog: true, detailsConfig: this.acceptDetailsConfig, visible: (x) => this.isPending(x) && this.iAmReceiver(x) };
12753
+ this.declineButton = { name: 'decline', display: 'Decline', icon: { name: 'block' }, visible: (x) => this.isPending(x) && this.iAmReceiver(x),
12754
+ onClick: (row, reload) => {
12755
+ this.messageService.confirm(`Decline the arrangement proposed by ${row?.tenantAName}? They will not be able to send you money.`, 'Decline', 'Keep it open', true).subscribe(result => {
12756
+ if (result !== 'yes')
12757
+ return;
12758
+ this.dataService.CallApi({ url: 'crosstenanttransfers/links/respond', method: 'post' }, { tenantLinkID: row.tenantLinkID, accept: false }).subscribe((response) => {
12759
+ this.messageService.toast(response?.success ? 'Declined' : (response?.message || 'Could not decline this arrangement'));
12760
+ if (response?.success && reload)
12761
+ reload();
12762
+ });
12763
+ });
12764
+ }
12765
+ };
12766
+ this.revokeButton = { name: 'revoke', display: 'Revoke', icon: { name: 'link_off' }, visible: (x) => (x?.status === 'Pending' || x?.status === 'Accepted'),
12767
+ onClick: (row, reload) => {
12768
+ this.messageService.confirm(`Revoke the arrangement between ${row?.tenantAName} and ${row?.tenantBName}? No further transfers can be sent over it. Transfers already made are not reversed.`, 'Revoke', 'Keep it', true).subscribe(result => {
12769
+ if (result !== 'yes')
12770
+ return;
12771
+ // The id is a PATH segment here, and Action.url is used literally on a POST — built at click time
12772
+ this.dataService.CallApi({ url: `crosstenanttransfers/links/${row.tenantLinkID}/revoke`, method: 'post' }, {}).subscribe((response) => {
12773
+ this.messageService.toast(response?.success ? 'Revoked' : (response?.message || 'Could not revoke this arrangement'));
12774
+ if (response?.success && reload)
12775
+ reload();
12776
+ });
12777
+ });
12778
+ }
12779
+ };
12780
+ // --------------------------Sending money-------------------------
12781
+ this.transferFormConfig = {
12782
+ security: { allow: [this.dataService.capTenantTransfers] },
12783
+ title: 'Send money',
12784
+ alertConfig: {
12785
+ messages: [
12786
+ { type: 'warn', message: 'There is NO balance check. This will post even if it takes the funding account negative.' },
12787
+ { type: 'info', message: 'The money leaves immediately and sits in your "Cross-Tenant Transfers in Transit" account until a background worker posts the receiving side. Nothing clears that balance afterwards — there is no settlement step, so the asset stays on your books and a matching "Due to" liability stays on theirs.' },
12788
+ ]
12789
+ },
12790
+ fields: [
12791
+ { name: 'amount', type: 'money', alias: 'Amount', required: true, span: true, min: 0.01, hint: 'Must be greater than zero, and both organisations must share a base currency' },
12792
+ { name: 'description', type: 'text-area', alias: 'Description', rows: 2, span: true, hint: 'Appears on the journal on BOTH sides' },
12793
+ // Date deliberately NOT offered — the server defaults it to today; see the Grip original's note.
12794
+ ]
12795
+ };
12796
+ this.transferSubmitButton = { name: 'create', display: 'Send', action: { url: 'crosstenanttransfers', method: 'post', successMessage: 'Sent. It is in transit until the receiving side posts.' } };
12797
+ this.transferDetailsConfig = {
12798
+ formConfig: this.transferFormConfig,
12799
+ mode: 'create',
12800
+ buttons: [this.transferSubmitButton]
12801
+ };
12802
+ this.transferButton = { name: 'send-money', display: 'Send Money', icon: { name: 'payments' }, color: 'primary', dialog: true, detailsConfig: this.transferDetailsConfig, visible: (x) => x?.status === 'Accepted' && this.iAmSender(x) };
12803
+ // --------------------------Tables-------------------------
12804
+ this.linksTableConfig = {
12805
+ sectionConfig: {
12806
+ title: 'Arrangements',
12807
+ icon: 'handshake',
12808
+ collapsible: false,
12809
+ caption: 'An arrangement is a one-way agreement. Only the Sender may move money over it, and only after the Receiver has accepted. For money to travel the other way, the other organisation must propose its own arrangement back to you.',
12810
+ chips: [
12811
+ { text: 'waiting for you to accept', color: '#FFA726', visible: (rows) => (rows || []).some((x) => this.isPending(x) && this.iAmReceiver(x)) },
12812
+ { text: 'waiting on them', color: '#90A4AE', visible: (rows) => (rows || []).some((x) => this.isPending(x) && this.iAmSender(x)) },
12813
+ ]
12814
+ },
12815
+ showFilter: true,
12816
+ flatButtons: true,
12817
+ minColumns: ['tenantAName', 'tenantBName', 'status'],
12818
+ identityColumn: 'tenantAName',
12819
+ noDataMessage: 'No arrangements yet. Propose one to another organisation to open a money channel to it.',
12820
+ columns: [
12821
+ { name: 'tenantAName', type: 'chip', alias: 'Sender', icons: [{ name: 'north_east', color: '#42A5F5', condition: (x) => this.iAmSender(x), tip: 'This is you — you are the side that may send over this arrangement' }] },
12822
+ { name: 'tenantBName', type: 'chip', alias: 'Receiver', icons: [{ name: 'south_west', color: '#7E57C2', condition: (x) => this.iAmReceiver(x), tip: 'This is you — you may only receive over this arrangement, never send' }] },
12823
+ // Added (N11): which application the other side lives in — same-app rows show a quiet dash via the server value
12824
+ { name: 'tenantBApplicationCode', type: 'text', alias: 'Application' },
12825
+ { name: 'status', type: 'chip', alias: 'Status',
12826
+ colors: [
12827
+ { name: '#FFA726', condition: (x) => x.status === 'Pending' },
12828
+ { name: '#81C784', condition: (x) => x.status === 'Accepted' },
12829
+ { name: '#90A4AE', condition: (x) => x.status === 'Declined' },
12830
+ { name: '#E57373', condition: (x) => x.status === 'Revoked' }
12831
+ ],
12832
+ icons: [
12833
+ { name: 'hourglass_top', color: '#FFA726', condition: (x) => this.isPending(x) && this.iAmReceiver(x), tip: 'Waiting for YOU — only the receiving organisation can accept this' },
12834
+ ]
12835
+ },
12836
+ { name: 'tenantABaseCurrency', type: 'text', alias: 'Currency',
12837
+ icons: [{ name: 'error_outline', color: '#E57373', condition: (x) => !!x.tenantBBaseCurrency && x.tenantABaseCurrency !== x.tenantBBaseCurrency, tip: 'The two organisations use different base currencies, so transfers over this arrangement will be refused' }]
12838
+ },
12839
+ { name: 'proposedBy', type: 'text', alias: 'Proposed by' },
12840
+ { name: 'note', type: 'text', alias: 'Note', maxLength: 40 },
12841
+ ],
12842
+ buttons: [this.proposeButton, this.acceptButton, this.declineButton, this.transferButton, this.revokeButton],
12843
+ loadAction: { url: 'crosstenanttransfers/links' }
12844
+ };
12845
+ this.transfersTableConfig = {
12846
+ sectionConfig: {
12847
+ title: 'Transfers',
12848
+ icon: 'swap_horiz',
12849
+ collapsible: false,
12850
+ caption: 'Each transfer is a real double-entry posting on both sides. There is no settlement step: the sender keeps an "in transit" asset and the receiver keeps a "Due to" liability, and neither ever clears. For a transfer to another application, the receiving journal lives in that application — the shared Reference is the correlation.',
12851
+ chips: [
12852
+ { text: 'money in transit', color: '#FFA726', visible: (rows) => (rows || []).some((x) => x.status === 'Queued') },
12853
+ { text: 'a transfer failed', color: '#E57373', visible: (rows) => (rows || []).some((x) => x.status === 'Failed') },
12854
+ ]
12855
+ },
12856
+ showFilter: true,
12857
+ flatButtons: true,
12858
+ minColumns: ['reference', 'amount', 'status'],
12859
+ identityColumn: 'reference',
12860
+ noDataMessage: 'No transfers yet. Send money over an accepted arrangement and it will appear here.',
12861
+ columns: [
12862
+ { name: 'reference', type: 'chip', alias: 'Reference' },
12863
+ { name: 'fromTenantName', type: 'text', alias: 'From' },
12864
+ { name: 'toTenantName', type: 'text', alias: 'To' },
12865
+ { name: 'toApplicationCode', type: 'text', alias: 'Application' }, // Added (N11)
12866
+ { name: 'date', type: 'date-short', alias: 'Date' },
12867
+ { name: 'amount', type: 'money', alias: 'Amount' },
12868
+ { name: 'status', type: 'chip', alias: 'Status',
12869
+ colors: [
12870
+ { name: '#FFA726', condition: (x) => x.status === 'Queued' },
12871
+ { name: '#81C784', condition: (x) => x.status === 'Completed' },
12872
+ { name: '#E57373', condition: (x) => x.status === 'Failed' }
12873
+ ],
12874
+ icons: [
12875
+ { name: 'flight_takeoff', color: '#FFA726', condition: (x) => x.status === 'Queued', tip: 'In transit — it has LEFT the sender and sits in their in-transit account. A background worker posts the receiving side. Refresh to watch it land.' },
12876
+ { name: 'error_outline', color: '#E57373', condition: (x) => x.status === 'Failed', tip: 'The receiving side could not be posted. The money is stranded in the sender\'s in-transit account — see the Error column.' },
12877
+ ]
12878
+ },
12879
+ { name: 'description', type: 'text', alias: 'Description', maxLength: 40 },
12880
+ { name: 'error', type: 'text', alias: 'Error', maxLength: 60 },
12881
+ ],
12882
+ loadAction: { url: 'crosstenanttransfers' }
12883
+ };
12884
+ }
12885
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TenantTransferService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
12886
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TenantTransferService, providedIn: 'root' }); }
12887
+ }
12888
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TenantTransferService, decorators: [{
12889
+ type: Injectable,
12890
+ args: [{
12891
+ providedIn: 'root'
12892
+ }]
12893
+ }] });
12894
+
12650
12895
  class PasswordPolicyService {
12651
12896
  constructor() {
12652
12897
  this.dataService = inject(DataServiceLib);
@@ -27435,14 +27680,14 @@ class LoginComponent {
27435
27680
  // Added: where a successful login/resume should land. An explicit ?redirectTo= always wins (it means the
27436
27681
  // user was bounced out of a specific page); otherwise resumeLastRoute lands them on their last page.
27437
27682
  async resumeTarget() {
27438
- if (this.route.snapshot.queryParams["redirectTo"] != undefined)
27439
- return this.redirectPath;
27683
+ if (this.route.snapshot.queryParams["redirectTo"])
27684
+ return this.redirectPath; // Changed: was `!= undefined` — a present-but-EMPTY redirectTo (the old hash-derived expiry redirect produced exactly that) beat the saved last-route with a blank target
27440
27685
  const last = await this.lastRouteService.resolve();
27441
27686
  return last ?? this.redirectPath;
27442
27687
  }
27443
27688
  ngOnInit() {
27444
27689
  this.dataService.appConfigObserv.subscribe(x => { this.appConfig = x; this.style = x.loginStyle ?? 'default'; }); // Changed: resolve the login style immediately — the restore panel must render in the app's configured style, not flip from default to modern when the form appears
27445
- if (this.route.snapshot.queryParams["redirectTo"] != undefined) {
27690
+ if (this.route.snapshot.queryParams["redirectTo"]) { // Changed: was `!= undefined` — an empty param must fall back to home, not adopt ""
27446
27691
  this.redirectPath = this.route.snapshot.queryParams["redirectTo"];
27447
27692
  }
27448
27693
  else {
@@ -31438,6 +31683,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
31438
31683
  args: [PageComponent]
31439
31684
  }] } });
31440
31685
 
31686
+ // Moved (N11) from grip-spa into tin-spa, under Accounting (owner directive 2026-08-17). The design notes
31687
+ // from the Grip original hold: the two tables are one story, so an action on the arrangements table
31688
+ // refreshes the transfers list beneath it via the service's shared Subject.
31689
+ class TenantTransfersComponent {
31690
+ constructor(tenantTransferService) {
31691
+ this.tenantTransferService = tenantTransferService;
31692
+ }
31693
+ // Any successful action on an arrangement can change the transfers list — sending money adds a row, and
31694
+ // revoking is worth re-reading the list after too. Refreshing unconditionally is correct and cheap.
31695
+ arrangementActioned() {
31696
+ this.tenantTransferService.transfersReload.next(true);
31697
+ }
31698
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TenantTransfersComponent, deps: [{ token: TenantTransferService }], target: i0.ɵɵFactoryTarget.Component }); }
31699
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TenantTransfersComponent, isStandalone: false, selector: "spa-tenant-transfers", ngImport: i0, template: "<!-- Organisation Transfers \u2014 the handshake and the money that flows over it. Two sections in one page\n deliberately: an arrangement is meaningless without the transfers it permits, and a transfer is\n unreadable without the arrangement that authorised it. -->\n<h4>Organisation Transfers</h4>\n<hr>\n\n<div class=\"mt-3 tenant-transfers\" style=\"font-size: 14px;\">\n\n <p class=\"tt-lead\">\n Money can only move between two organisations that have <strong>agreed an arrangement</strong> first.\n One side proposes it, the other accepts it, and only then can the proposing side send.\n </p>\n\n <!-- (actionSuccess) \u2014 Send Money is a button on THIS table, so without this wire the Transfers list below\n still shows the previous rows after money has already moved. -->\n <spa-table [config]=\"tenantTransferService.linksTableConfig\" (actionSuccess)=\"arrangementActioned()\"></spa-table>\n\n <div class=\"tt-gap\"></div>\n\n <spa-table [config]=\"tenantTransferService.transfersTableConfig\" [reload]=\"tenantTransferService.transfersReload\"></spa-table>\n\n</div>\n", styles: [".tenant-transfers .tt-lead{max-width:74ch;margin:0 0 20px;line-height:1.55;opacity:.85}.tenant-transfers .tt-gap{height:28px}\n"], dependencies: [{ kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }] }); }
31700
+ }
31701
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TenantTransfersComponent, decorators: [{
31702
+ type: Component,
31703
+ args: [{ selector: 'spa-tenant-transfers', standalone: false, template: "<!-- Organisation Transfers \u2014 the handshake and the money that flows over it. Two sections in one page\n deliberately: an arrangement is meaningless without the transfers it permits, and a transfer is\n unreadable without the arrangement that authorised it. -->\n<h4>Organisation Transfers</h4>\n<hr>\n\n<div class=\"mt-3 tenant-transfers\" style=\"font-size: 14px;\">\n\n <p class=\"tt-lead\">\n Money can only move between two organisations that have <strong>agreed an arrangement</strong> first.\n One side proposes it, the other accepts it, and only then can the proposing side send.\n </p>\n\n <!-- (actionSuccess) \u2014 Send Money is a button on THIS table, so without this wire the Transfers list below\n still shows the previous rows after money has already moved. -->\n <spa-table [config]=\"tenantTransferService.linksTableConfig\" (actionSuccess)=\"arrangementActioned()\"></spa-table>\n\n <div class=\"tt-gap\"></div>\n\n <spa-table [config]=\"tenantTransferService.transfersTableConfig\" [reload]=\"tenantTransferService.transfersReload\"></spa-table>\n\n</div>\n", styles: [".tenant-transfers .tt-lead{max-width:74ch;margin:0 0 20px;line-height:1.55;opacity:.85}.tenant-transfers .tt-gap{height:28px}\n"] }]
31704
+ }], ctorParameters: () => [{ type: TenantTransferService }] });
31705
+
31441
31706
  // New revenue schedule dialog (C7) — defer a submitted invoice's net revenue over a date range
31442
31707
  class RevenueScheduleDialogComponent {
31443
31708
  constructor() {
@@ -31647,6 +31912,7 @@ const ACCOUNTING_ROUTES = [
31647
31912
  { path: "fiscal-periods", component: FiscalPeriodsComponent }, // Changed: Fiscal periods route (C2)
31648
31913
  { path: "receipts", component: ReceiptsComponent, data: { moduleKey: 'invoicing' } }, // Changed: Customer receipts route (C3) // Added (F3)
31649
31914
  { path: "bank-reconciliation", component: BankReconciliationComponent }, // Changed: Bank reconciliation route (C4)
31915
+ { path: "tenant-transfers", component: TenantTransfersComponent }, // Added (N11): Organisation Transfers
31650
31916
  { path: "revenue-schedules", component: RevenueSchedulesComponent, data: { moduleKey: 'invoicing' } }, // Changed: Deferred revenue route (C7) // Added (F3)
31651
31917
  { path: "quotes", component: QuotesComponent, data: { moduleKey: 'invoicing' } } // Changed: Quotations route (C10) // Added (F3)
31652
31918
  ];
@@ -35648,6 +35914,7 @@ class AccountingModule {
35648
35914
  ReceiptsComponent, // Changed: Customer receipts page (C3)
35649
35915
  ReceiptDialogComponent, // Changed: Receipt allocation dialog (C3)
35650
35916
  BankReconciliationComponent, // Changed: Bank reconciliation page (C4)
35917
+ TenantTransfersComponent, // Added (N11): Organisation Transfers page
35651
35918
  StatementImportDialogComponent, // Changed: Statement import dialog (C4)
35652
35919
  ReconcileDialogComponent, // Changed: Reconcile match screen (C4)
35653
35920
  RevenueSchedulesComponent, // Changed: Deferred revenue page (C7)
@@ -35692,6 +35959,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
35692
35959
  ReceiptsComponent, // Changed: Customer receipts page (C3)
35693
35960
  ReceiptDialogComponent, // Changed: Receipt allocation dialog (C3)
35694
35961
  BankReconciliationComponent, // Changed: Bank reconciliation page (C4)
35962
+ TenantTransfersComponent, // Added (N11): Organisation Transfers page
35695
35963
  StatementImportDialogComponent, // Changed: Statement import dialog (C4)
35696
35964
  ReconcileDialogComponent, // Changed: Reconcile match screen (C4)
35697
35965
  RevenueSchedulesComponent, // Changed: Deferred revenue page (C7)
@@ -36211,5 +36479,5 @@ const ALSQUARE_SVG_WHITE = `<svg viewBox="0 0 80 80" fill="none" xmlns="http://w
36211
36479
  * Generated bundle index. Do not edit.
36212
36480
  */
36213
36481
 
36214
- export { ALSQUARE_SVG_DARK, ALSQUARE_SVG_WHITE, Account, AccountsComponent as AccountingAccountsComponent, AggregatesComponent as AccountingAggregatesComponent, AgingComponent as AccountingAgingComponent, CreditNotesComponent as AccountingCreditNotesComponent, CurrenciesComponent as AccountingCurrenciesComponent, AccountingDashboardComponent, InvoicesComponent as AccountingInvoicesComponent, AccountingModule, ReportsComponent as AccountingReportsComponent, AccountingService, StatementComponent as AccountingStatementComponent, TransactionTypesComponent as AccountingTransactionTypesComponent, TransactionsComponent as AccountingTransactionsComponent, VatReturnComponent as AccountingVatReturnComponent, Action, ActivityComponent, AdminModule, AgentComponent, AgentPageComponent, AgentService, AlertComponent, AlertConfig, AlertMessage, AnalyticsService, ApiErrorService, ApiResponse, AppConfig, AppConfigurationComponent, AppModelsComponent, AssetStatus, AssetsService, AttachComponent, AuthService, BankReconciliationComponent, BillingPageComponent, BottomTab, BrandsComponent, CacheConfig, CapItem, CapsulesComponent, CategoriesComponent, ChangePasswordComponent, ChangeUserPassword, ChartConfig, ChartsComponent, CheckComponent, ChecklistComponent, ChipsComponent, ConfigService, Constants, Core, CreateAccountComponent, CreditNoteStatus, CustomersComponent, DataServiceLib, DateComponent, DatetimeComponent, DayBookComponent, DepartmentsComponent, DetailsDialog, DetailsDialogConfig, DetailsDialogProcessor, DetailsSource, DialogService, EditorComponent, EllipsisDirective, EmailComponent, EmployeesComponent, ExportService, FeatureDirective, FieldLoadIndicator, FilterComponent, FiscalPeriodStatus, FiscalPeriodsComponent, FormComponent, FormConfig, FormSkeletonComponent, GeneralModule, GeneralService, GradesComponent, GroupsComponent, HRModule, HtmlComponent, HttpService, ImportDialogComponent, IndexModule, InventoryDashboardComponent, InventoryModule, InventoryService, InvitationsTableComponent, InvoiceDashboardComponent, InvoiceItemType, InvoiceStatus, JournalEntryDialogComponent, LabelComponent, LastRouteService, ListDialogComponent, ListDialogConfig, LoaderComponent, LoaderService, LoanPaymentsComponent, LoanProductsComponent, LoansComponent, LoansModule, LoansService, LogLevel, LogService, LoginComponent, LogsComponent, MONOGRAM_TONES, ManufacturingModule, MembershipComponent, MessageService, MoneyComponent, MonogramComponent, MovementType, NavMenuComponent, NotesComponent, NotesConfig, NotificationsService, NumberComponent, OfflineIndicatorComponent, OfflineService, OnboardingComponent, OptionComponent, OverviewDashboardComponent, OverviewModule, PageComponent, PageConfig, PaginationConfig, PasswordPolicyComponent, PasswordPolicyService, PayrollDashboardComponent, PayrollModule, PayrollService, PerceivedProgress, PlansComponent, PositionsComponent, PrivacyDialogComponent, ProductionService, Profile, ProfileComponent, PurchaseStatus, PurchasingDashboardComponent, PurchasingModule, PurchasingService, PushNotificationService, QUIET_DELAY_MS, QUIET_MIN_MS, QuoteStatus, QuotesComponent, ReceiptDialogComponent, ReceiptStatus, ReceiptsComponent, ReconcileDialogComponent, RecoverAccountComponent, Register, RevenueScheduleDialogComponent, RevenueSchedulesComponent, Role, RoleAccess, RolesComponent, SalesDashboardComponent, SalesModule, SalesService, SearchComponent, SearchConfig, SecurityConfig, SelectBitwiseComponent, SelectComponent, SelectLiteComponent, SelectMultiComponent, SettingsComponent, SetupGuideComponent, SetupService, SignupComponent, SignupData, SpaAdminModule, SpaDayBookModule, SpaHomeModule, SpaIndexModule, SpaLandingComponent, SpaMatModule, SpaUserModule, StatementImportDialogComponent, StatusesComponent, Step, StepConfig, StepsComponent, StorageService, SubCategoriesComponent, SubscriptionPageComponent, SubscriptionService, SuppliersComponent, SyncComponent, TIN_SILENT_REQUEST, TIN_SPA_RUNTIME_CONFIG, TabService, TableComponent, TableConfig, TableDialogComponent, TabsComponent, TasksComponent, TenancyModule, TenantsComponent, TermsDialogComponent, TextAreaComponent, TextComponent, TextMaskComponent, TextMultiComponent, TextSingleComponent, TileConfig, TilesComponent, TinSpaComponent, TinSpaModule, TinSpaService, TitleActionsComponent, TransactionTiming, UnitOfMeasure, UpdateService, User, UserModule, UsersComponent, ViewerComponent, WelcomeComponent, WorkflowModule, authGuard, dialogOptions, featureGuard, getTinSpaAppSettings, isSilentRequest, loadTinSpaAppSettings, loginConfig, messageDialog, moduleGuard, monogramInitials, monogramKind, monogramPaletteIndex, provideTinSpaRuntime, readOnlyTableConfig, resolveQuietLoading, silentContext, tinSpaLocationStrategyFactory, tinSpaMsalInstanceFactory, tinSpaRuntimeConfigFactory, viewerDialog };
36482
+ export { ALSQUARE_SVG_DARK, ALSQUARE_SVG_WHITE, Account, AccountsComponent as AccountingAccountsComponent, AggregatesComponent as AccountingAggregatesComponent, AgingComponent as AccountingAgingComponent, CreditNotesComponent as AccountingCreditNotesComponent, CurrenciesComponent as AccountingCurrenciesComponent, AccountingDashboardComponent, InvoicesComponent as AccountingInvoicesComponent, AccountingModule, ReportsComponent as AccountingReportsComponent, AccountingService, StatementComponent as AccountingStatementComponent, TransactionTypesComponent as AccountingTransactionTypesComponent, TransactionsComponent as AccountingTransactionsComponent, VatReturnComponent as AccountingVatReturnComponent, Action, ActivityComponent, AdminModule, AgentComponent, AgentPageComponent, AgentService, AlertComponent, AlertConfig, AlertMessage, AnalyticsService, ApiErrorService, ApiResponse, AppConfig, AppConfigurationComponent, AppModelsComponent, AssetStatus, AssetsService, AttachComponent, AuthService, BankReconciliationComponent, BillingPageComponent, BottomTab, BrandsComponent, CacheConfig, CapItem, CapsulesComponent, CategoriesComponent, ChangePasswordComponent, ChangeUserPassword, ChartConfig, ChartsComponent, CheckComponent, ChecklistComponent, ChipsComponent, ConfigService, Constants, Core, CreateAccountComponent, CreditNoteStatus, CustomersComponent, DataServiceLib, DateComponent, DatetimeComponent, DayBookComponent, DepartmentsComponent, DetailsDialog, DetailsDialogConfig, DetailsDialogProcessor, DetailsSource, DialogService, EditorComponent, EllipsisDirective, EmailComponent, EmployeesComponent, ExportService, FeatureDirective, FieldLoadIndicator, FilterComponent, FiscalPeriodStatus, FiscalPeriodsComponent, FormComponent, FormConfig, FormSkeletonComponent, GeneralModule, GeneralService, GradesComponent, GroupsComponent, HRModule, HtmlComponent, HttpService, ImportDialogComponent, IndexModule, InventoryDashboardComponent, InventoryModule, InventoryService, InvitationsTableComponent, InvoiceDashboardComponent, InvoiceItemType, InvoiceStatus, JournalEntryDialogComponent, LabelComponent, LastRouteService, ListDialogComponent, ListDialogConfig, LoaderComponent, LoaderService, LoanPaymentsComponent, LoanProductsComponent, LoansComponent, LoansModule, LoansService, LogLevel, LogService, LoginComponent, LogsComponent, MONOGRAM_TONES, ManufacturingModule, MembershipComponent, MessageService, MoneyComponent, MonogramComponent, MovementType, NavMenuComponent, NotesComponent, NotesConfig, NotificationsService, NumberComponent, OfflineIndicatorComponent, OfflineService, OnboardingComponent, OptionComponent, OverviewDashboardComponent, OverviewModule, PageComponent, PageConfig, PaginationConfig, PasswordPolicyComponent, PasswordPolicyService, PayrollDashboardComponent, PayrollModule, PayrollService, PerceivedProgress, PlansComponent, PositionsComponent, PrivacyDialogComponent, ProductionService, Profile, ProfileComponent, PurchaseStatus, PurchasingDashboardComponent, PurchasingModule, PurchasingService, PushNotificationService, QUIET_DELAY_MS, QUIET_MIN_MS, QuoteStatus, QuotesComponent, ReceiptDialogComponent, ReceiptStatus, ReceiptsComponent, ReconcileDialogComponent, RecoverAccountComponent, Register, RevenueScheduleDialogComponent, RevenueSchedulesComponent, Role, RoleAccess, RolesComponent, SalesDashboardComponent, SalesModule, SalesService, SearchComponent, SearchConfig, SecurityConfig, SelectBitwiseComponent, SelectComponent, SelectLiteComponent, SelectMultiComponent, SettingsComponent, SetupGuideComponent, SetupService, SignupComponent, SignupData, SpaAdminModule, SpaDayBookModule, SpaHomeModule, SpaIndexModule, SpaLandingComponent, SpaMatModule, SpaUserModule, StatementImportDialogComponent, StatusesComponent, Step, StepConfig, StepsComponent, StorageService, SubCategoriesComponent, SubscriptionPageComponent, SubscriptionService, SuppliersComponent, SyncComponent, TIN_SILENT_REQUEST, TIN_SPA_RUNTIME_CONFIG, TabService, TableComponent, TableConfig, TableDialogComponent, TabsComponent, TasksComponent, TenancyModule, TenantTransferService, TenantsComponent, TermsDialogComponent, TextAreaComponent, TextComponent, TextMaskComponent, TextMultiComponent, TextSingleComponent, TileConfig, TilesComponent, TinSpaComponent, TinSpaModule, TinSpaService, TitleActionsComponent, TransactionTiming, UnitOfMeasure, UpdateService, User, UserModule, UsersComponent, ViewerComponent, WelcomeComponent, WorkflowModule, authGuard, dialogOptions, featureGuard, getTinSpaAppSettings, isSilentRequest, loadTinSpaAppSettings, loginConfig, messageDialog, moduleGuard, monogramInitials, monogramKind, monogramPaletteIndex, provideTinSpaRuntime, readOnlyTableConfig, resolveQuietLoading, silentContext, tinSpaLocationStrategyFactory, tinSpaMsalInstanceFactory, tinSpaRuntimeConfigFactory, viewerDialog };
36215
36483
  //# sourceMappingURL=tin-spa.mjs.map