tin-spa 20.14.99 → 20.14.113
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/core/index.d.ts +3033 -0
- package/fesm2022/tin-spa-accounting.module-Cg4vWaJE.mjs +2231 -0
- package/fesm2022/tin-spa-accounting.module-Cg4vWaJE.mjs.map +1 -0
- package/fesm2022/tin-spa-agent-page.component-DJvMPjCQ.mjs +39 -0
- package/fesm2022/tin-spa-agent-page.component-DJvMPjCQ.mjs.map +1 -0
- package/fesm2022/tin-spa-agent.component-D0CGGnGZ.mjs +1379 -0
- package/fesm2022/tin-spa-agent.component-D0CGGnGZ.mjs.map +1 -0
- package/fesm2022/tin-spa-assets.module-qIWNwJ2E.mjs +167 -0
- package/fesm2022/tin-spa-assets.module-qIWNwJ2E.mjs.map +1 -0
- package/fesm2022/tin-spa-core.mjs +13628 -0
- package/fesm2022/tin-spa-core.mjs.map +1 -0
- package/fesm2022/tin-spa-general-erp.module-CoYxceLj.mjs +138 -0
- package/fesm2022/tin-spa-general-erp.module-CoYxceLj.mjs.map +1 -0
- package/fesm2022/tin-spa-hr.module-CKPZ9ZOj.mjs +134 -0
- package/fesm2022/tin-spa-hr.module-CKPZ9ZOj.mjs.map +1 -0
- package/fesm2022/tin-spa-http.mjs +117 -0
- package/fesm2022/tin-spa-http.mjs.map +1 -0
- package/fesm2022/tin-spa-inventory.module-DRKAcNau.mjs +338 -0
- package/fesm2022/tin-spa-inventory.module-DRKAcNau.mjs.map +1 -0
- package/fesm2022/tin-spa-keys.mjs +14 -0
- package/fesm2022/tin-spa-keys.mjs.map +1 -0
- package/fesm2022/tin-spa-landing.mjs +189 -0
- package/fesm2022/tin-spa-landing.mjs.map +1 -0
- package/fesm2022/tin-spa-loans.module-BTDmgrWo.mjs +115 -0
- package/fesm2022/tin-spa-loans.module-BTDmgrWo.mjs.map +1 -0
- package/fesm2022/tin-spa-login.mjs +719 -0
- package/fesm2022/tin-spa-login.mjs.map +1 -0
- package/fesm2022/tin-spa-manufacturing.module-CHta6oV_.mjs +96 -0
- package/fesm2022/tin-spa-manufacturing.module-CHta6oV_.mjs.map +1 -0
- package/fesm2022/tin-spa-map.mjs +750 -0
- package/fesm2022/tin-spa-map.mjs.map +1 -0
- package/fesm2022/tin-spa-payroll.module-CZ_GO1jF.mjs +847 -0
- package/fesm2022/tin-spa-payroll.module-CZ_GO1jF.mjs.map +1 -0
- package/fesm2022/tin-spa-purchasing.module-NCdwquaF.mjs +161 -0
- package/fesm2022/tin-spa-purchasing.module-NCdwquaF.mjs.map +1 -0
- package/fesm2022/tin-spa-sales.module-D0uEGJW7.mjs +1346 -0
- package/fesm2022/tin-spa-sales.module-D0uEGJW7.mjs.map +1 -0
- package/fesm2022/tin-spa-tenancy-erp.module-GwIv8Wbc.mjs +932 -0
- package/fesm2022/tin-spa-tenancy-erp.module-GwIv8Wbc.mjs.map +1 -0
- package/fesm2022/tin-spa-tin-spa-Cqe5hYTr.mjs +22576 -0
- package/fesm2022/tin-spa-tin-spa-Cqe5hYTr.mjs.map +1 -0
- package/fesm2022/tin-spa.mjs +2 -42079
- package/fesm2022/tin-spa.mjs.map +1 -1
- package/http/index.d.ts +16 -0
- package/index.d.ts +588 -4687
- package/keys/index.d.ts +4 -0
- package/landing/index.d.ts +175 -0
- package/login/index.d.ts +141 -0
- package/map/index.d.ts +148 -0
- package/package.json +25 -1
- package/src/tin-styles.css +81 -2
|
@@ -0,0 +1,2231 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { Component, inject, ViewChild, NgModule } from '@angular/core';
|
|
3
|
+
import * as i1$2 from '@angular/router';
|
|
4
|
+
import { RouterModule } from '@angular/router';
|
|
5
|
+
import * as i1 from 'tin-spa/core';
|
|
6
|
+
import { AccountingService, MessageService, ApiErrorService, PurchasingService, HttpService, DataServiceLib, ReceiptDialogComponent } from 'tin-spa/core';
|
|
7
|
+
import { T as TilesComponent, a as TabsComponent, P as PageComponent, D as DateComponent, b as TableComponent, S as SelectLiteComponent, c as TinSpaModule, C as ChartsComponent, d as TenantTransferService, e as SpaAdminModule } from './tin-spa-tin-spa-Cqe5hYTr.mjs';
|
|
8
|
+
import * as i1$1 from '@angular/common';
|
|
9
|
+
import { CommonModule } from '@angular/common';
|
|
10
|
+
import * as i3 from '@angular/material/button';
|
|
11
|
+
import * as i3$1 from '@angular/material/icon';
|
|
12
|
+
import { HttpClient } from '@angular/common/http';
|
|
13
|
+
import * as i3$2 from '@angular/material/tabs';
|
|
14
|
+
import * as i2 from '@angular/forms';
|
|
15
|
+
import * as i7 from '@angular/material/menu';
|
|
16
|
+
import * as i3$3 from '@angular/material/form-field';
|
|
17
|
+
import * as i4 from '@angular/material/input';
|
|
18
|
+
import * as i6 from '@angular/material/card';
|
|
19
|
+
import * as i4$1 from '@angular/material/dialog';
|
|
20
|
+
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
|
21
|
+
import * as i7$1 from '@angular/material/select';
|
|
22
|
+
import * as i4$2 from '@angular/material/checkbox';
|
|
23
|
+
|
|
24
|
+
class AccountsComponent {
|
|
25
|
+
constructor(dataServiceLib, accountingService) {
|
|
26
|
+
this.dataServiceLib = dataServiceLib;
|
|
27
|
+
this.accountingService = accountingService;
|
|
28
|
+
// Changed: Tile config with Cash, Bank, Income MTD, and Expenses MTD
|
|
29
|
+
this.accountTileConfig = {
|
|
30
|
+
tiles: [
|
|
31
|
+
{ name: 'cashBalance', alias: 'Cash', color: '#4CAF50', icon: 'payments', style: 'icon', info: 'Total balance of all cash accounts' },
|
|
32
|
+
{ name: 'bankBalance', alias: 'Bank', color: '#2196F3', icon: 'account_balance', style: 'icon', info: 'Total balance of all bank accounts' },
|
|
33
|
+
{ name: 'mtdIncome', alias: 'Income (MTD)', color: '#9C27B0', icon: 'trending_up', style: 'icon', info: 'Month-to-date income from revenue accounts' },
|
|
34
|
+
{ name: 'mtdExpenses', alias: 'Expenses (MTD)', color: '#F44336', icon: 'trending_down', style: 'icon', info: 'Month-to-date expenses' },
|
|
35
|
+
],
|
|
36
|
+
loadAction: { url: 'accounts/summary/tiles' },
|
|
37
|
+
loadInit: true
|
|
38
|
+
};
|
|
39
|
+
this.accountTabConfigs = [
|
|
40
|
+
{
|
|
41
|
+
...this.accountingService.accountsTableConfig,
|
|
42
|
+
tabTitle: 'Assets',
|
|
43
|
+
loadAction: { url: 'accounts/type/asset' },
|
|
44
|
+
countAction: { url: 'accounts/count/asset' }
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
...this.accountingService.accountsTableConfig,
|
|
48
|
+
tabTitle: 'Liabilities',
|
|
49
|
+
loadAction: { url: 'accounts/type/liability' },
|
|
50
|
+
countAction: { url: 'accounts/count/liability' }
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
...this.accountingService.accountsTableConfig,
|
|
54
|
+
tabTitle: 'Income',
|
|
55
|
+
loadAction: { url: 'accounts/type/income' },
|
|
56
|
+
countAction: { url: 'accounts/count/income' }
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
...this.accountingService.accountsTableConfig,
|
|
60
|
+
tabTitle: 'Expenses',
|
|
61
|
+
loadAction: { url: 'accounts/type/expense' },
|
|
62
|
+
countAction: { url: 'accounts/count/expense' }
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
...this.accountingService.accountsTableConfig,
|
|
66
|
+
tabTitle: 'Equity',
|
|
67
|
+
loadAction: { url: 'accounts/type/equity' },
|
|
68
|
+
countAction: { url: 'accounts/count/equity' }
|
|
69
|
+
},
|
|
70
|
+
// Removed: Aggregate tab - no longer used
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
ngOnInit() {
|
|
74
|
+
}
|
|
75
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AccountsComponent, deps: [{ token: i1.DataServiceLib }, { token: i1.AccountingService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
76
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: AccountsComponent, isStandalone: false, selector: "spa-accounts", ngImport: i0, template: `
|
|
77
|
+
<h4>Accounts</h4>
|
|
78
|
+
<hr>
|
|
79
|
+
|
|
80
|
+
<spa-tiles [config]="accountTileConfig"></spa-tiles>
|
|
81
|
+
|
|
82
|
+
<spa-tabs [tableConfigs]="accountTabConfigs"></spa-tabs>
|
|
83
|
+
`, isInline: true, dependencies: [{ kind: "component", type: TilesComponent, selector: "spa-tiles", inputs: ["config", "lastSearch", "data", "reload"], outputs: ["tileActionSelected", "tileClick", "tileUnClick"] }, { kind: "component", type: TabsComponent, selector: "spa-tabs", inputs: ["tableConfigs", "reload", "reloadTab", "parentDetails", "localMode", "nestingLevel"], outputs: ["formRefresh", "actionSuccess"] }] }); }
|
|
84
|
+
}
|
|
85
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AccountsComponent, decorators: [{
|
|
86
|
+
type: Component,
|
|
87
|
+
args: [{
|
|
88
|
+
selector: 'spa-accounts',
|
|
89
|
+
template: `
|
|
90
|
+
<h4>Accounts</h4>
|
|
91
|
+
<hr>
|
|
92
|
+
|
|
93
|
+
<spa-tiles [config]="accountTileConfig"></spa-tiles>
|
|
94
|
+
|
|
95
|
+
<spa-tabs [tableConfigs]="accountTabConfigs"></spa-tabs>
|
|
96
|
+
`,
|
|
97
|
+
standalone: false
|
|
98
|
+
}]
|
|
99
|
+
}], ctorParameters: () => [{ type: i1.DataServiceLib }, { type: i1.AccountingService }] });
|
|
100
|
+
|
|
101
|
+
// Accounting Aggregates component with monthly aggregated financial data
|
|
102
|
+
class AggregatesComponent {
|
|
103
|
+
constructor(dataServiceLib, accountingService) {
|
|
104
|
+
this.dataServiceLib = dataServiceLib;
|
|
105
|
+
this.accountingService = accountingService;
|
|
106
|
+
this.aggregateTabConfigs = [
|
|
107
|
+
{
|
|
108
|
+
...this.accountingService.aggregatesTableConfig,
|
|
109
|
+
tabTitle: 'Monthly Summary',
|
|
110
|
+
loadAction: { url: 'accounts/aggregates/monthly' },
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
...this.accountingService.cashFlowAggregatesTableConfig,
|
|
114
|
+
tabTitle: 'Cash Flow',
|
|
115
|
+
loadAction: { url: 'accounts/aggregates/cashflow' },
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
...this.accountingService.cumulativeAggregatesTableConfig,
|
|
119
|
+
tabTitle: 'Cumulative Balances',
|
|
120
|
+
loadAction: { url: 'accounts/aggregates/cumulative' },
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
...this.accountingService.balanceSheetAggregatesTableConfig,
|
|
124
|
+
tabTitle: 'Balance Sheet',
|
|
125
|
+
loadAction: { url: 'accounts/aggregates/balancesheet' },
|
|
126
|
+
}
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
ngOnInit() {
|
|
130
|
+
}
|
|
131
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AggregatesComponent, deps: [{ token: i1.DataServiceLib }, { token: i1.AccountingService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
132
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: AggregatesComponent, isStandalone: false, selector: "spa-aggregates", ngImport: i0, template: `
|
|
133
|
+
<h4>Aggregates</h4>
|
|
134
|
+
<hr>
|
|
135
|
+
|
|
136
|
+
<spa-tabs [tableConfigs]="aggregateTabConfigs"></spa-tabs>
|
|
137
|
+
`, isInline: true, dependencies: [{ kind: "component", type: TabsComponent, selector: "spa-tabs", inputs: ["tableConfigs", "reload", "reloadTab", "parentDetails", "localMode", "nestingLevel"], outputs: ["formRefresh", "actionSuccess"] }] }); }
|
|
138
|
+
}
|
|
139
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AggregatesComponent, decorators: [{
|
|
140
|
+
type: Component,
|
|
141
|
+
args: [{
|
|
142
|
+
selector: 'spa-aggregates',
|
|
143
|
+
template: `
|
|
144
|
+
<h4>Aggregates</h4>
|
|
145
|
+
<hr>
|
|
146
|
+
|
|
147
|
+
<spa-tabs [tableConfigs]="aggregateTabConfigs"></spa-tabs>
|
|
148
|
+
`,
|
|
149
|
+
standalone: false
|
|
150
|
+
}]
|
|
151
|
+
}], ctorParameters: () => [{ type: i1.DataServiceLib }, { type: i1.AccountingService }] });
|
|
152
|
+
|
|
153
|
+
// Aging report component for accounts receivable tracking
|
|
154
|
+
class AgingComponent {
|
|
155
|
+
ngOnInit() {
|
|
156
|
+
this.loadSummaryData();
|
|
157
|
+
}
|
|
158
|
+
loadSummaryData() {
|
|
159
|
+
this.dataServiceLib.CallApi({ url: 'invoices/aging-summary/x' }, '').subscribe((apiResponse) => {
|
|
160
|
+
if (apiResponse.success) {
|
|
161
|
+
this.summaryData = apiResponse.data;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
this.apiErrorService.presentAppFailure(apiResponse, 'load', 'invoices/aging-summary/x'); // Added: was silent. summaryData stayed undefined, the *ngIf dropped the whole tile strip, and an accounts-receivable page with NO aging totals reads as "you are owed nothing" rather than "we could not work it out"
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
// Changed: Post IFRS 9 bad-debt provision — confirms, calls invoices provision custom action, shows returned summary
|
|
169
|
+
postProvision() {
|
|
170
|
+
this.messageService.confirm('Post the bad-debt provision? This posts the allowance delta per the aging matrix as a GL journal.').subscribe((result) => {
|
|
171
|
+
if (result == 'yes') {
|
|
172
|
+
this.dataServiceLib.CallApi({ url: 'invoices?action=provision', method: 'post' }, {}).subscribe((apiResponse) => {
|
|
173
|
+
// Changed: REPLACES the old `'Error: ' + apiResponse.message` toast rather than layering on it.
|
|
174
|
+
// That string prefixed the server's raw words with a second word for "error" and pushed the useful
|
|
175
|
+
// part right; a leaked SQL fragment reached the user unchanged. 'action' rather than 'submit' —
|
|
176
|
+
// this is a confirmed page button, not a form, so the 'submit' copy would describe a screen the
|
|
177
|
+
// user is not looking at.
|
|
178
|
+
if (apiResponse.success) {
|
|
179
|
+
this.messageService.toast(apiResponse.message);
|
|
180
|
+
this.loadSummaryData();
|
|
181
|
+
}
|
|
182
|
+
else
|
|
183
|
+
this.apiErrorService.presentAppFailure(apiResponse, 'action', 'invoices?action=provision');
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
constructor(dataServiceLib) {
|
|
189
|
+
this.dataServiceLib = dataServiceLib;
|
|
190
|
+
this.accountingService = inject(AccountingService);
|
|
191
|
+
this.messageService = inject(MessageService); // Changed: Injected for provision confirm/toast
|
|
192
|
+
this.apiErrorService = inject(ApiErrorService); // Changed: Injected for the summary load and the provision failure
|
|
193
|
+
this.tileConfig = this.accountingService.agingTileConfig;
|
|
194
|
+
// this.tileConfig = this.accountingService.agingTileConfig;
|
|
195
|
+
this.agingTableConfigs = [
|
|
196
|
+
{
|
|
197
|
+
...this.accountingService.agingBaseTableConfig,
|
|
198
|
+
tabTitle: 'Current',
|
|
199
|
+
loadAction: { url: 'invoices/aging-current/x' },
|
|
200
|
+
countAction: { url: 'invoices/count/aging-current' }
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
...this.accountingService.agingBaseTableConfig,
|
|
204
|
+
tabTitle: '1-30 Days',
|
|
205
|
+
loadAction: { url: 'invoices/aging-30/x' },
|
|
206
|
+
countAction: { url: 'invoices/count/aging-30' }
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
...this.accountingService.agingBaseTableConfig,
|
|
210
|
+
tabTitle: '31-60 Days',
|
|
211
|
+
loadAction: { url: 'invoices/aging-60/x' },
|
|
212
|
+
countAction: { url: 'invoices/count/aging-60' }
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
...this.accountingService.agingBaseTableConfig,
|
|
216
|
+
tabTitle: '61-90 Days',
|
|
217
|
+
loadAction: { url: 'invoices/aging-90/x' },
|
|
218
|
+
countAction: { url: 'invoices/count/aging-90' }
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
...this.accountingService.agingBaseTableConfig,
|
|
222
|
+
tabTitle: '90+ Days',
|
|
223
|
+
loadAction: { url: 'invoices/aging-90plus/x' },
|
|
224
|
+
countAction: { url: 'invoices/count/aging-90plus' }
|
|
225
|
+
}
|
|
226
|
+
];
|
|
227
|
+
}
|
|
228
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AgingComponent, deps: [{ token: i1.DataServiceLib }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
229
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: AgingComponent, isStandalone: false, selector: "spa-aging", ngImport: i0, template: `
|
|
230
|
+
<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
231
|
+
<h4>Accounts Receivable Aging</h4>
|
|
232
|
+
<!-- Changed: Post IFRS 9 bad-debt provision delta from the aging matrix -->
|
|
233
|
+
<button mat-stroked-button color="warn" (click)="postProvision()"><mat-icon>account_balance</mat-icon> Post Bad-Debt Provision</button>
|
|
234
|
+
</div>
|
|
235
|
+
<hr>
|
|
236
|
+
<div *ngIf="summaryData" class="mt-3 mb-3">
|
|
237
|
+
<spa-tiles [config]="tileConfig" [data]="summaryData"></spa-tiles>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<spa-tabs [tableConfigs]="agingTableConfigs"></spa-tabs>
|
|
241
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: TilesComponent, selector: "spa-tiles", inputs: ["config", "lastSearch", "data", "reload"], outputs: ["tileActionSelected", "tileClick", "tileUnClick"] }, { kind: "component", type: TabsComponent, selector: "spa-tabs", inputs: ["tableConfigs", "reload", "reloadTab", "parentDetails", "localMode", "nestingLevel"], outputs: ["formRefresh", "actionSuccess"] }] }); }
|
|
242
|
+
}
|
|
243
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AgingComponent, decorators: [{
|
|
244
|
+
type: Component,
|
|
245
|
+
args: [{
|
|
246
|
+
selector: 'spa-aging',
|
|
247
|
+
template: `
|
|
248
|
+
<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
249
|
+
<h4>Accounts Receivable Aging</h4>
|
|
250
|
+
<!-- Changed: Post IFRS 9 bad-debt provision delta from the aging matrix -->
|
|
251
|
+
<button mat-stroked-button color="warn" (click)="postProvision()"><mat-icon>account_balance</mat-icon> Post Bad-Debt Provision</button>
|
|
252
|
+
</div>
|
|
253
|
+
<hr>
|
|
254
|
+
<div *ngIf="summaryData" class="mt-3 mb-3">
|
|
255
|
+
<spa-tiles [config]="tileConfig" [data]="summaryData"></spa-tiles>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<spa-tabs [tableConfigs]="agingTableConfigs"></spa-tabs>
|
|
259
|
+
`,
|
|
260
|
+
standalone: false
|
|
261
|
+
}]
|
|
262
|
+
}], ctorParameters: () => [{ type: i1.DataServiceLib }] });
|
|
263
|
+
|
|
264
|
+
// AP Aging report component for accounts payable tracking — mirrors AR AgingComponent pattern
|
|
265
|
+
class SupplierAgingComponent {
|
|
266
|
+
ngOnInit() {
|
|
267
|
+
this.loadSummaryData();
|
|
268
|
+
}
|
|
269
|
+
loadSummaryData() {
|
|
270
|
+
this.dataServiceLib.CallApi({ url: 'purchases/aging-summary/x' }, '').subscribe((apiResponse) => {
|
|
271
|
+
if (apiResponse.success) {
|
|
272
|
+
this.summaryData = apiResponse.data;
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
this.apiErrorService.presentAppFailure(apiResponse, 'load', 'purchases/aging-summary/x'); // Added: was silent. The tile strip vanished behind its *ngIf, so an accounts-payable page that could not compute its totals looked like a company that owes nothing
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
constructor(dataServiceLib) {
|
|
280
|
+
this.dataServiceLib = dataServiceLib;
|
|
281
|
+
this.purchasingService = inject(PurchasingService);
|
|
282
|
+
this.apiErrorService = inject(ApiErrorService); // Changed: Injected so a failed summary load says so
|
|
283
|
+
this.tileConfig = this.purchasingService.apAgingTileConfig;
|
|
284
|
+
// Changed: Build 5-tab aging table layout using AP aging base config from PurchasingService
|
|
285
|
+
this.agingTableConfigs = [
|
|
286
|
+
{
|
|
287
|
+
...this.purchasingService.apAgingBaseTableConfig,
|
|
288
|
+
tabTitle: 'Current',
|
|
289
|
+
loadAction: { url: 'purchases/aging-current/x' },
|
|
290
|
+
countAction: { url: 'purchases/count/aging-current' }
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
...this.purchasingService.apAgingBaseTableConfig,
|
|
294
|
+
tabTitle: '1-30 Days',
|
|
295
|
+
loadAction: { url: 'purchases/aging-30/x' },
|
|
296
|
+
countAction: { url: 'purchases/count/aging-30' }
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
...this.purchasingService.apAgingBaseTableConfig,
|
|
300
|
+
tabTitle: '31-60 Days',
|
|
301
|
+
loadAction: { url: 'purchases/aging-60/x' },
|
|
302
|
+
countAction: { url: 'purchases/count/aging-60' }
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
...this.purchasingService.apAgingBaseTableConfig,
|
|
306
|
+
tabTitle: '61-90 Days',
|
|
307
|
+
loadAction: { url: 'purchases/aging-90/x' },
|
|
308
|
+
countAction: { url: 'purchases/count/aging-90' }
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
...this.purchasingService.apAgingBaseTableConfig,
|
|
312
|
+
tabTitle: '90+ Days',
|
|
313
|
+
loadAction: { url: 'purchases/aging-90plus/x' },
|
|
314
|
+
countAction: { url: 'purchases/count/aging-90plus' }
|
|
315
|
+
}
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SupplierAgingComponent, deps: [{ token: i1.DataServiceLib }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
319
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SupplierAgingComponent, isStandalone: false, selector: "spa-supplier-aging", ngImport: i0, template: `
|
|
320
|
+
<h4>Accounts Payable Aging</h4>
|
|
321
|
+
<hr>
|
|
322
|
+
<div *ngIf="summaryData" class="mt-3 mb-3">
|
|
323
|
+
<spa-tiles [config]="tileConfig" [data]="summaryData"></spa-tiles>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<spa-tabs [tableConfigs]="agingTableConfigs"></spa-tabs>
|
|
327
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: TilesComponent, selector: "spa-tiles", inputs: ["config", "lastSearch", "data", "reload"], outputs: ["tileActionSelected", "tileClick", "tileUnClick"] }, { kind: "component", type: TabsComponent, selector: "spa-tabs", inputs: ["tableConfigs", "reload", "reloadTab", "parentDetails", "localMode", "nestingLevel"], outputs: ["formRefresh", "actionSuccess"] }] }); }
|
|
328
|
+
}
|
|
329
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SupplierAgingComponent, decorators: [{
|
|
330
|
+
type: Component,
|
|
331
|
+
args: [{
|
|
332
|
+
selector: 'spa-supplier-aging',
|
|
333
|
+
template: `
|
|
334
|
+
<h4>Accounts Payable Aging</h4>
|
|
335
|
+
<hr>
|
|
336
|
+
<div *ngIf="summaryData" class="mt-3 mb-3">
|
|
337
|
+
<spa-tiles [config]="tileConfig" [data]="summaryData"></spa-tiles>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<spa-tabs [tableConfigs]="agingTableConfigs"></spa-tabs>
|
|
341
|
+
`,
|
|
342
|
+
standalone: false
|
|
343
|
+
}]
|
|
344
|
+
}], ctorParameters: () => [{ type: i1.DataServiceLib }] });
|
|
345
|
+
|
|
346
|
+
class TransactionTypesComponent {
|
|
347
|
+
constructor() {
|
|
348
|
+
this.accountingService = inject(AccountingService);
|
|
349
|
+
this.pageConfig = {
|
|
350
|
+
title: 'Transaction Types',
|
|
351
|
+
tableConfig: this.accountingService.transactionTypesTableConfig
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
ngOnInit() {
|
|
355
|
+
}
|
|
356
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TransactionTypesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
357
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TransactionTypesComponent, isStandalone: false, selector: "spa-transaction-types", ngImport: i0, template: '<spa-page [config]="pageConfig"></spa-page>', isInline: true, dependencies: [{ kind: "component", type: PageComponent, selector: "spa-page", inputs: ["config"], outputs: ["searchModeActivated", "searchModeDeactivated", "refreshClick", "actionClick", "actionResponse", "inputChange", "createClick", "searchClick", "dataLoad", "titleActionChange"] }] }); }
|
|
358
|
+
}
|
|
359
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TransactionTypesComponent, decorators: [{
|
|
360
|
+
type: Component,
|
|
361
|
+
args: [{
|
|
362
|
+
selector: 'spa-transaction-types',
|
|
363
|
+
template: '<spa-page [config]="pageConfig"></spa-page>',
|
|
364
|
+
standalone: false
|
|
365
|
+
}]
|
|
366
|
+
}] });
|
|
367
|
+
|
|
368
|
+
// Transactions page
|
|
369
|
+
// Changed (journal-dialog-conform): the "New Journal" button used to be drawn HERE, in a flex row above <spa-page>,
|
|
370
|
+
// which floated it on the app shell outside the page. It is now journalCreateButton on transactionsTableConfig with
|
|
371
|
+
// inHeader: true, so the table header renders it in line with Create Transaction. The hand-rolled
|
|
372
|
+
// JournalEntryDialogComponent it opened is gone — the button now carries the house details dialog config.
|
|
373
|
+
class TransactionsComponent {
|
|
374
|
+
constructor() {
|
|
375
|
+
this.accountingService = inject(AccountingService);
|
|
376
|
+
this.pageConfig = {
|
|
377
|
+
title: 'Transactions',
|
|
378
|
+
tableConfig: this.accountingService.transactionsTableConfig,
|
|
379
|
+
searchTableConfig: this.accountingService.transactionSearchTableConfig // Changed: Enable search mode with date range, description, account filters
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
ngOnInit() {
|
|
383
|
+
}
|
|
384
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TransactionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
385
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TransactionsComponent, isStandalone: false, selector: "spa-transactions", ngImport: i0, template: `<spa-page [config]="pageConfig"></spa-page>`, isInline: true, dependencies: [{ kind: "component", type: PageComponent, selector: "spa-page", inputs: ["config"], outputs: ["searchModeActivated", "searchModeDeactivated", "refreshClick", "actionClick", "actionResponse", "inputChange", "createClick", "searchClick", "dataLoad", "titleActionChange"] }] }); }
|
|
386
|
+
}
|
|
387
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TransactionsComponent, decorators: [{
|
|
388
|
+
type: Component,
|
|
389
|
+
args: [{
|
|
390
|
+
selector: 'spa-transactions',
|
|
391
|
+
template: `<spa-page [config]="pageConfig"></spa-page>`,
|
|
392
|
+
standalone: false
|
|
393
|
+
}]
|
|
394
|
+
}] });
|
|
395
|
+
|
|
396
|
+
// Invoice management component for tin-spa library
|
|
397
|
+
class InvoicesComponent {
|
|
398
|
+
constructor() {
|
|
399
|
+
this.accountingService = inject(AccountingService);
|
|
400
|
+
this.httpService = inject(HttpService);
|
|
401
|
+
this.http = inject(HttpClient);
|
|
402
|
+
this.messageService = inject(MessageService);
|
|
403
|
+
this.pageConfig = {
|
|
404
|
+
title: 'Invoices',
|
|
405
|
+
tableConfig: this.accountingService.invoicesTableConfig
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
ngOnInit() {
|
|
409
|
+
}
|
|
410
|
+
// Handle action click events from the table
|
|
411
|
+
actionClicked(event) {
|
|
412
|
+
if (event.name === 'pdf') {
|
|
413
|
+
this.messageService.confirm(`Download Invoice ?`).subscribe((result) => {
|
|
414
|
+
if (result == "yes") {
|
|
415
|
+
this.download(event.data);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// Download invoice as PDF
|
|
421
|
+
download(invoice) {
|
|
422
|
+
this.http.post(`${this.httpService.apiUrl}invoices/pdf?action=x`, invoice, { responseType: 'blob' })
|
|
423
|
+
.subscribe((response) => {
|
|
424
|
+
const blob = new Blob([response], { type: 'application/pdf' });
|
|
425
|
+
const url = window.URL.createObjectURL(blob);
|
|
426
|
+
const link = document.createElement('a');
|
|
427
|
+
link.href = url;
|
|
428
|
+
link.download = `Invoice_${invoice.invoiceID}.pdf`;
|
|
429
|
+
link.click();
|
|
430
|
+
window.URL.revokeObjectURL(url);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: InvoicesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
434
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: InvoicesComponent, isStandalone: false, selector: "spa-invoices", ngImport: i0, template: '<spa-page [config]="pageConfig" (actionClick)="actionClicked($event)"></spa-page>', isInline: true, dependencies: [{ kind: "component", type: PageComponent, selector: "spa-page", inputs: ["config"], outputs: ["searchModeActivated", "searchModeDeactivated", "refreshClick", "actionClick", "actionResponse", "inputChange", "createClick", "searchClick", "dataLoad", "titleActionChange"] }] }); }
|
|
435
|
+
}
|
|
436
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: InvoicesComponent, decorators: [{
|
|
437
|
+
type: Component,
|
|
438
|
+
args: [{
|
|
439
|
+
selector: 'spa-invoices',
|
|
440
|
+
template: '<spa-page [config]="pageConfig" (actionClick)="actionClicked($event)"></spa-page>',
|
|
441
|
+
standalone: false
|
|
442
|
+
}]
|
|
443
|
+
}] });
|
|
444
|
+
|
|
445
|
+
// Statement page (B4) — invoice history plus open-item customer/supplier statements with Excel/PDF export
|
|
446
|
+
// Changed: the two open-item tabs were hand-written markup (a flex row of totals and a raw <table>). They are
|
|
447
|
+
// now the library's own spa-tiles + spa-table, driven by config in AccountingService, so the page picks up
|
|
448
|
+
// every table behaviour centrally: responsive minColumns, the filter box, money formatting and empty states.
|
|
449
|
+
// The PDF and Excel downloads are untouched — both are produced SERVER-SIDE and nothing here contributes to them.
|
|
450
|
+
class StatementComponent {
|
|
451
|
+
constructor() {
|
|
452
|
+
this.accountingService = inject(AccountingService);
|
|
453
|
+
this.httpService = inject(HttpService);
|
|
454
|
+
this.http = inject(HttpClient);
|
|
455
|
+
this.pageConfig = {
|
|
456
|
+
title: 'Statement',
|
|
457
|
+
tableConfig: this.accountingService.statementTableConfig,
|
|
458
|
+
searchTableConfig: this.accountingService.statementSearchTableConfig
|
|
459
|
+
};
|
|
460
|
+
// Per-kind UI state for the customer/supplier open-item tabs.
|
|
461
|
+
// Changed: each kind carries its OWN cloned table config — two grids are on screen at once and a TableConfig
|
|
462
|
+
// is mutated by the table it belongs to, so sharing one object between the tabs would cross their state.
|
|
463
|
+
this.state = {
|
|
464
|
+
customer: {
|
|
465
|
+
entityLabel: 'Customer', entityId: null, from: null, to: null, data: null, lines: [],
|
|
466
|
+
listAction: { url: 'customers/list/x' },
|
|
467
|
+
tileConfig: this.accountingService.customerStatementTileConfig,
|
|
468
|
+
tableConfig: { ...this.accountingService.statementLinesTableConfig }
|
|
469
|
+
},
|
|
470
|
+
supplier: {
|
|
471
|
+
entityLabel: 'Supplier', entityId: null, from: null, to: null, data: null, lines: [],
|
|
472
|
+
listAction: { url: 'suppliers/list/x' },
|
|
473
|
+
tileConfig: this.accountingService.supplierStatementTileConfig,
|
|
474
|
+
tableConfig: { ...this.accountingService.statementLinesTableConfig }
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
// Changed: the customer/supplier option lists are loaded by spa-select-lite's own loadAction now, so there is
|
|
479
|
+
// no hand-rolled options fetch left here. ngOnInit is kept for the interface and for future per-kind defaults.
|
|
480
|
+
ngOnInit() { }
|
|
481
|
+
query(kind) {
|
|
482
|
+
const s = this.state[kind];
|
|
483
|
+
const params = [];
|
|
484
|
+
if (s.from)
|
|
485
|
+
params.push(`from=${s.from}`);
|
|
486
|
+
if (s.to)
|
|
487
|
+
params.push(`to=${s.to}`);
|
|
488
|
+
return params.length ? `?${params.join('&')}` : '';
|
|
489
|
+
}
|
|
490
|
+
load(kind) {
|
|
491
|
+
const s = this.state[kind];
|
|
492
|
+
if (!s.entityId)
|
|
493
|
+
return; // Added: the header refresh icon routes here too, and it must not fire before an entity is chosen
|
|
494
|
+
this.http.get(`${this.httpService.apiUrl}reports/${kind}-statement/${s.entityId}${this.query(kind)}`)
|
|
495
|
+
.subscribe(res => {
|
|
496
|
+
s.data = res?.data ?? res?.Data ?? null;
|
|
497
|
+
s.lines = s.data?.lines ?? []; // Changed: held as a stable reference — binding `data.lines || []` inline would hand spa-table a new array on every change-detection pass
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
// C8: open-item customer statement as PDF — generated entirely server-side (reports/pdf/customer-statement);
|
|
501
|
+
// this method only asks for the blob and saves it. Unchanged by the page rewrite.
|
|
502
|
+
exportPdf(kind) {
|
|
503
|
+
const s = this.state[kind];
|
|
504
|
+
this.http.get(`${this.httpService.apiUrl}reports/pdf/${kind}-statement${this.query(kind) ? this.query(kind) + '&' : '?'}${kind}Id=${s.entityId}`, { responseType: 'blob' })
|
|
505
|
+
.subscribe((response) => {
|
|
506
|
+
const url = window.URL.createObjectURL(new Blob([response], { type: 'application/pdf' }));
|
|
507
|
+
const link = document.createElement('a');
|
|
508
|
+
link.href = url;
|
|
509
|
+
link.download = `${kind}-statement-${s.entityId}.pdf`;
|
|
510
|
+
link.click();
|
|
511
|
+
window.URL.revokeObjectURL(url);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
export(kind) {
|
|
515
|
+
const s = this.state[kind];
|
|
516
|
+
this.http.get(`${this.httpService.apiUrl}reports/export/${kind}-statement${this.query(kind) ? this.query(kind) + '&' : '?'}${kind}Id=${s.entityId}`, { responseType: 'blob' })
|
|
517
|
+
.subscribe((response) => {
|
|
518
|
+
const url = window.URL.createObjectURL(new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }));
|
|
519
|
+
const link = document.createElement('a');
|
|
520
|
+
link.href = url;
|
|
521
|
+
link.download = `${kind}-statement-${s.entityId}.xlsx`;
|
|
522
|
+
link.click();
|
|
523
|
+
window.URL.revokeObjectURL(url);
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: StatementComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
527
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: StatementComponent, isStandalone: false, selector: "spa-statement", ngImport: i0, template: `
|
|
528
|
+
<mat-tab-group>
|
|
529
|
+
<mat-tab label="Invoice History">
|
|
530
|
+
<div style="padding-top: 8px;">
|
|
531
|
+
<spa-page [config]="pageConfig"></spa-page>
|
|
532
|
+
</div>
|
|
533
|
+
</mat-tab>
|
|
534
|
+
|
|
535
|
+
<mat-tab label="Customer Statement">
|
|
536
|
+
<ng-container *ngTemplateOutlet="statementTab; context: { kind: 'customer' }"></ng-container>
|
|
537
|
+
</mat-tab>
|
|
538
|
+
|
|
539
|
+
<mat-tab label="Supplier Statement">
|
|
540
|
+
<ng-container *ngTemplateOutlet="statementTab; context: { kind: 'supplier' }"></ng-container>
|
|
541
|
+
</mat-tab>
|
|
542
|
+
</mat-tab-group>
|
|
543
|
+
|
|
544
|
+
<ng-template #statementTab let-kind="kind">
|
|
545
|
+
<div style="padding: 16px 0;">
|
|
546
|
+
|
|
547
|
+
<!-- Changed: library inputs instead of raw mat-form-field markup — same three filters, same Run/Export/PDF rules -->
|
|
548
|
+
<div style="display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-bottom: 12px;">
|
|
549
|
+
<spa-select-lite [display]="state[kind].entityLabel" [loadAction]="state[kind].listAction" optionDisplay="name" optionValue="value" [required]="false" [(value)]="state[kind].entityId" width="240px"></spa-select-lite><!-- optionDisplay/optionValue are NOT defaulted by spa-select-common; without them every option binds undefined and nothing can be selected -->
|
|
550
|
+
<spa-date display="From" width="170px" [required]="false" [value]="state[kind].from" (valueChange)="state[kind].from = $event"></spa-date>
|
|
551
|
+
<spa-date display="To" width="170px" [required]="false" [value]="state[kind].to" (valueChange)="state[kind].to = $event"></spa-date>
|
|
552
|
+
<button mat-flat-button color="primary" (click)="load(kind)" [disabled]="!state[kind].entityId">Run</button>
|
|
553
|
+
<button mat-stroked-button (click)="export(kind)" [disabled]="!state[kind].entityId">
|
|
554
|
+
<mat-icon>download</mat-icon> Export to Excel
|
|
555
|
+
</button>
|
|
556
|
+
<button mat-stroked-button *ngIf="kind === 'customer'" (click)="exportPdf(kind)" [disabled]="!state[kind].entityId">
|
|
557
|
+
<mat-icon>picture_as_pdf</mat-icon> Download PDF
|
|
558
|
+
</button>
|
|
559
|
+
</div>
|
|
560
|
+
|
|
561
|
+
<!-- Changed: totals are spa-tiles, lines are spa-table. Nothing renders until a statement has been run,
|
|
562
|
+
which is exactly what the old *ngIf on the results card did. -->
|
|
563
|
+
<ng-container *ngIf="state[kind].data">
|
|
564
|
+
<spa-tiles [config]="state[kind].tileConfig" [data]="state[kind].data"></spa-tiles>
|
|
565
|
+
<div style="margin-top: 12px;">
|
|
566
|
+
<spa-table [config]="state[kind].tableConfig" [data]="state[kind].lines" (refreshClick)="load(kind)"></spa-table>
|
|
567
|
+
</div>
|
|
568
|
+
</ng-container>
|
|
569
|
+
|
|
570
|
+
</div>
|
|
571
|
+
</ng-template>
|
|
572
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$2.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i3$2.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: TilesComponent, selector: "spa-tiles", inputs: ["config", "lastSearch", "data", "reload"], outputs: ["tileActionSelected", "tileClick", "tileUnClick"] }, { kind: "component", type: DateComponent, selector: "spa-date", inputs: ["required", "min", "max", "boundContext", "readonly", "hint", "value", "display", "placeholder", "width", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }, { kind: "component", type: PageComponent, selector: "spa-page", inputs: ["config"], outputs: ["searchModeActivated", "searchModeDeactivated", "refreshClick", "actionClick", "actionResponse", "inputChange", "createClick", "searchClick", "dataLoad", "titleActionChange"] }, { kind: "component", type: SelectLiteComponent, selector: "spa-select-lite" }] }); }
|
|
573
|
+
}
|
|
574
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: StatementComponent, decorators: [{
|
|
575
|
+
type: Component,
|
|
576
|
+
args: [{
|
|
577
|
+
selector: 'spa-statement',
|
|
578
|
+
template: `
|
|
579
|
+
<mat-tab-group>
|
|
580
|
+
<mat-tab label="Invoice History">
|
|
581
|
+
<div style="padding-top: 8px;">
|
|
582
|
+
<spa-page [config]="pageConfig"></spa-page>
|
|
583
|
+
</div>
|
|
584
|
+
</mat-tab>
|
|
585
|
+
|
|
586
|
+
<mat-tab label="Customer Statement">
|
|
587
|
+
<ng-container *ngTemplateOutlet="statementTab; context: { kind: 'customer' }"></ng-container>
|
|
588
|
+
</mat-tab>
|
|
589
|
+
|
|
590
|
+
<mat-tab label="Supplier Statement">
|
|
591
|
+
<ng-container *ngTemplateOutlet="statementTab; context: { kind: 'supplier' }"></ng-container>
|
|
592
|
+
</mat-tab>
|
|
593
|
+
</mat-tab-group>
|
|
594
|
+
|
|
595
|
+
<ng-template #statementTab let-kind="kind">
|
|
596
|
+
<div style="padding: 16px 0;">
|
|
597
|
+
|
|
598
|
+
<!-- Changed: library inputs instead of raw mat-form-field markup — same three filters, same Run/Export/PDF rules -->
|
|
599
|
+
<div style="display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-bottom: 12px;">
|
|
600
|
+
<spa-select-lite [display]="state[kind].entityLabel" [loadAction]="state[kind].listAction" optionDisplay="name" optionValue="value" [required]="false" [(value)]="state[kind].entityId" width="240px"></spa-select-lite><!-- optionDisplay/optionValue are NOT defaulted by spa-select-common; without them every option binds undefined and nothing can be selected -->
|
|
601
|
+
<spa-date display="From" width="170px" [required]="false" [value]="state[kind].from" (valueChange)="state[kind].from = $event"></spa-date>
|
|
602
|
+
<spa-date display="To" width="170px" [required]="false" [value]="state[kind].to" (valueChange)="state[kind].to = $event"></spa-date>
|
|
603
|
+
<button mat-flat-button color="primary" (click)="load(kind)" [disabled]="!state[kind].entityId">Run</button>
|
|
604
|
+
<button mat-stroked-button (click)="export(kind)" [disabled]="!state[kind].entityId">
|
|
605
|
+
<mat-icon>download</mat-icon> Export to Excel
|
|
606
|
+
</button>
|
|
607
|
+
<button mat-stroked-button *ngIf="kind === 'customer'" (click)="exportPdf(kind)" [disabled]="!state[kind].entityId">
|
|
608
|
+
<mat-icon>picture_as_pdf</mat-icon> Download PDF
|
|
609
|
+
</button>
|
|
610
|
+
</div>
|
|
611
|
+
|
|
612
|
+
<!-- Changed: totals are spa-tiles, lines are spa-table. Nothing renders until a statement has been run,
|
|
613
|
+
which is exactly what the old *ngIf on the results card did. -->
|
|
614
|
+
<ng-container *ngIf="state[kind].data">
|
|
615
|
+
<spa-tiles [config]="state[kind].tileConfig" [data]="state[kind].data"></spa-tiles>
|
|
616
|
+
<div style="margin-top: 12px;">
|
|
617
|
+
<spa-table [config]="state[kind].tableConfig" [data]="state[kind].lines" (refreshClick)="load(kind)"></spa-table>
|
|
618
|
+
</div>
|
|
619
|
+
</ng-container>
|
|
620
|
+
|
|
621
|
+
</div>
|
|
622
|
+
</ng-template>
|
|
623
|
+
`,
|
|
624
|
+
standalone: false
|
|
625
|
+
}]
|
|
626
|
+
}] });
|
|
627
|
+
|
|
628
|
+
// Financial Reports component with Trial Balance, P&L, Balance Sheet, General Ledger tabs + Excel exports (B5/B7) and from/to date range (Phase 1)
|
|
629
|
+
class ReportsComponent {
|
|
630
|
+
constructor(dataServiceLib, accountingService) {
|
|
631
|
+
this.dataServiceLib = dataServiceLib;
|
|
632
|
+
this.accountingService = accountingService;
|
|
633
|
+
this.httpService = inject(HttpService); // Changed: For export downloads
|
|
634
|
+
this.http = inject(HttpClient); // Changed: For export downloads
|
|
635
|
+
this.from = ''; // Changed: Optional report period start
|
|
636
|
+
this.to = ''; // Changed: Optional report period end
|
|
637
|
+
this.reportTabConfigs = [];
|
|
638
|
+
this.buildTabs(); // Changed: Tabs built via method so Run can rebuild with date params
|
|
639
|
+
}
|
|
640
|
+
ngOnInit() {
|
|
641
|
+
}
|
|
642
|
+
// Changed: Append from/to query params when set — backends that ignore them are unaffected
|
|
643
|
+
withRange(url) {
|
|
644
|
+
const params = [];
|
|
645
|
+
if (this.from)
|
|
646
|
+
params.push(`from=${this.from}`);
|
|
647
|
+
if (this.to)
|
|
648
|
+
params.push(`to=${this.to}`);
|
|
649
|
+
return params.length ? `${url}?${params.join('&')}` : url;
|
|
650
|
+
}
|
|
651
|
+
// Changed: Rebuild tab configs with the current date range applied to every report URL
|
|
652
|
+
buildTabs() {
|
|
653
|
+
this.reportTabConfigs = [
|
|
654
|
+
{
|
|
655
|
+
...this.accountingService.trialBalanceTableConfig,
|
|
656
|
+
tabTitle: 'Trial Balance',
|
|
657
|
+
loadAction: { url: this.withRange('accounts/reports/trialbalance') },
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
...this.accountingService.pnlTableConfig,
|
|
661
|
+
tabTitle: 'Profit & Loss',
|
|
662
|
+
loadAction: { url: this.withRange('accounts/reports/pnl') },
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
...this.accountingService.balanceSheetReportTableConfig,
|
|
666
|
+
tabTitle: 'Balance Sheet',
|
|
667
|
+
loadAction: { url: this.withRange('accounts/reports/balancesheet') },
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
...this.accountingService.generalLedgerTableConfig,
|
|
671
|
+
tabTitle: 'General Ledger',
|
|
672
|
+
loadAction: { url: this.withRange('reports/gl') },
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
...this.accountingService.cashFlowTableConfig,
|
|
676
|
+
tabTitle: 'Cash Flow',
|
|
677
|
+
loadAction: { url: this.withRange('reports/cash-flow') },
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
...this.accountingService.equityChangesTableConfig,
|
|
681
|
+
tabTitle: 'Changes in Equity',
|
|
682
|
+
loadAction: { url: this.withRange('reports/equity-changes') },
|
|
683
|
+
}
|
|
684
|
+
];
|
|
685
|
+
}
|
|
686
|
+
// Changed: Download an xlsx export from the shared reports endpoint (B7) with the selected date range applied
|
|
687
|
+
export(report) {
|
|
688
|
+
this.http.get(`${this.httpService.apiUrl}${this.withRange(`reports/export/${report}`)}`, { responseType: 'blob' })
|
|
689
|
+
.subscribe((response) => {
|
|
690
|
+
const url = window.URL.createObjectURL(new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }));
|
|
691
|
+
const link = document.createElement('a');
|
|
692
|
+
link.href = url;
|
|
693
|
+
link.download = `${report}.xlsx`;
|
|
694
|
+
link.click();
|
|
695
|
+
window.URL.revokeObjectURL(url);
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ReportsComponent, deps: [{ token: i1.DataServiceLib }, { token: i1.AccountingService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
699
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: ReportsComponent, isStandalone: false, selector: "spa-reports", ngImport: i0, template: `
|
|
700
|
+
<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
701
|
+
<h4>Financial Reports</h4>
|
|
702
|
+
<button mat-stroked-button [matMenuTriggerFor]="exportMenu">
|
|
703
|
+
<mat-icon>download</mat-icon> Export to Excel
|
|
704
|
+
</button>
|
|
705
|
+
<mat-menu #exportMenu="matMenu">
|
|
706
|
+
<button mat-menu-item (click)="export('trial-balance')">Trial Balance</button>
|
|
707
|
+
<button mat-menu-item (click)="export('pnl')">Profit & Loss</button>
|
|
708
|
+
<button mat-menu-item (click)="export('balance-sheet')">Balance Sheet</button>
|
|
709
|
+
<button mat-menu-item (click)="export('gl')">General Ledger</button>
|
|
710
|
+
<button mat-menu-item (click)="export('vat-return')">VAT Return (current month)</button>
|
|
711
|
+
<button mat-menu-item (click)="export('cash-flow')">Cash Flow (current month)</button>
|
|
712
|
+
<button mat-menu-item (click)="export('equity-changes')">Changes in Equity (current month)</button>
|
|
713
|
+
</mat-menu>
|
|
714
|
+
</div>
|
|
715
|
+
<hr>
|
|
716
|
+
|
|
717
|
+
<!-- Changed: From/To date range controls applied to all report tabs (pattern copied from VatReturnComponent) -->
|
|
718
|
+
<div style="display: flex; gap: 16px; align-items: center; margin-bottom: 16px;">
|
|
719
|
+
<mat-form-field>
|
|
720
|
+
<mat-label>From</mat-label>
|
|
721
|
+
<input matInput type="date" [(ngModel)]="from">
|
|
722
|
+
</mat-form-field>
|
|
723
|
+
<mat-form-field>
|
|
724
|
+
<mat-label>To</mat-label>
|
|
725
|
+
<input matInput type="date" [(ngModel)]="to">
|
|
726
|
+
</mat-form-field>
|
|
727
|
+
<button mat-flat-button color="primary" (click)="buildTabs()">Run</button>
|
|
728
|
+
</div>
|
|
729
|
+
|
|
730
|
+
<spa-tabs [tableConfigs]="reportTabConfigs"></spa-tabs>
|
|
731
|
+
`, isInline: true, 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: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i7.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: i7.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7.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: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$3.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: TabsComponent, selector: "spa-tabs", inputs: ["tableConfigs", "reload", "reloadTab", "parentDetails", "localMode", "nestingLevel"], outputs: ["formRefresh", "actionSuccess"] }] }); }
|
|
732
|
+
}
|
|
733
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ReportsComponent, decorators: [{
|
|
734
|
+
type: Component,
|
|
735
|
+
args: [{
|
|
736
|
+
selector: 'spa-reports',
|
|
737
|
+
template: `
|
|
738
|
+
<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
739
|
+
<h4>Financial Reports</h4>
|
|
740
|
+
<button mat-stroked-button [matMenuTriggerFor]="exportMenu">
|
|
741
|
+
<mat-icon>download</mat-icon> Export to Excel
|
|
742
|
+
</button>
|
|
743
|
+
<mat-menu #exportMenu="matMenu">
|
|
744
|
+
<button mat-menu-item (click)="export('trial-balance')">Trial Balance</button>
|
|
745
|
+
<button mat-menu-item (click)="export('pnl')">Profit & Loss</button>
|
|
746
|
+
<button mat-menu-item (click)="export('balance-sheet')">Balance Sheet</button>
|
|
747
|
+
<button mat-menu-item (click)="export('gl')">General Ledger</button>
|
|
748
|
+
<button mat-menu-item (click)="export('vat-return')">VAT Return (current month)</button>
|
|
749
|
+
<button mat-menu-item (click)="export('cash-flow')">Cash Flow (current month)</button>
|
|
750
|
+
<button mat-menu-item (click)="export('equity-changes')">Changes in Equity (current month)</button>
|
|
751
|
+
</mat-menu>
|
|
752
|
+
</div>
|
|
753
|
+
<hr>
|
|
754
|
+
|
|
755
|
+
<!-- Changed: From/To date range controls applied to all report tabs (pattern copied from VatReturnComponent) -->
|
|
756
|
+
<div style="display: flex; gap: 16px; align-items: center; margin-bottom: 16px;">
|
|
757
|
+
<mat-form-field>
|
|
758
|
+
<mat-label>From</mat-label>
|
|
759
|
+
<input matInput type="date" [(ngModel)]="from">
|
|
760
|
+
</mat-form-field>
|
|
761
|
+
<mat-form-field>
|
|
762
|
+
<mat-label>To</mat-label>
|
|
763
|
+
<input matInput type="date" [(ngModel)]="to">
|
|
764
|
+
</mat-form-field>
|
|
765
|
+
<button mat-flat-button color="primary" (click)="buildTabs()">Run</button>
|
|
766
|
+
</div>
|
|
767
|
+
|
|
768
|
+
<spa-tabs [tableConfigs]="reportTabConfigs"></spa-tabs>
|
|
769
|
+
`,
|
|
770
|
+
standalone: false
|
|
771
|
+
}]
|
|
772
|
+
}], ctorParameters: () => [{ type: i1.DataServiceLib }, { type: i1.AccountingService }] });
|
|
773
|
+
|
|
774
|
+
// Tax Rates management page - CRUD for VAT tax rates
|
|
775
|
+
class TaxRatesComponent {
|
|
776
|
+
constructor() {
|
|
777
|
+
this.accountingService = inject(AccountingService);
|
|
778
|
+
this.pageConfig = {
|
|
779
|
+
title: 'Tax Rates',
|
|
780
|
+
tableConfig: this.accountingService.taxRatesTableConfig
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TaxRatesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
784
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TaxRatesComponent, isStandalone: true, selector: "spa-tax-rates", ngImport: i0, template: '<spa-page [config]="pageConfig"></spa-page>', isInline: true, dependencies: [{ kind: "ngmodule", type: TinSpaModule }, { kind: "component", type: PageComponent, selector: "spa-page", inputs: ["config"], outputs: ["searchModeActivated", "searchModeDeactivated", "refreshClick", "actionClick", "actionResponse", "inputChange", "createClick", "searchClick", "dataLoad", "titleActionChange"] }] }); }
|
|
785
|
+
}
|
|
786
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TaxRatesComponent, decorators: [{
|
|
787
|
+
type: Component,
|
|
788
|
+
args: [{
|
|
789
|
+
selector: 'spa-tax-rates',
|
|
790
|
+
standalone: true,
|
|
791
|
+
imports: [TinSpaModule],
|
|
792
|
+
template: '<spa-page [config]="pageConfig"></spa-page>'
|
|
793
|
+
}]
|
|
794
|
+
}] });
|
|
795
|
+
|
|
796
|
+
// Standing Orders management page — CRUD for recurring scheduled transactions
|
|
797
|
+
class StandingOrdersComponent {
|
|
798
|
+
constructor() {
|
|
799
|
+
this.accountingService = inject(AccountingService);
|
|
800
|
+
this.pageConfig = {
|
|
801
|
+
title: 'Standing Orders',
|
|
802
|
+
tableConfig: this.accountingService.standingOrdersTableConfig
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: StandingOrdersComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
806
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: StandingOrdersComponent, isStandalone: true, selector: "spa-standing-orders", ngImport: i0, template: '<spa-page [config]="pageConfig"></spa-page>', isInline: true, dependencies: [{ kind: "ngmodule", type: TinSpaModule }, { kind: "component", type: PageComponent, selector: "spa-page", inputs: ["config"], outputs: ["searchModeActivated", "searchModeDeactivated", "refreshClick", "actionClick", "actionResponse", "inputChange", "createClick", "searchClick", "dataLoad", "titleActionChange"] }] }); }
|
|
807
|
+
}
|
|
808
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: StandingOrdersComponent, decorators: [{
|
|
809
|
+
type: Component,
|
|
810
|
+
args: [{
|
|
811
|
+
selector: 'spa-standing-orders',
|
|
812
|
+
standalone: true,
|
|
813
|
+
imports: [TinSpaModule],
|
|
814
|
+
template: '<spa-page [config]="pageConfig"></spa-page>'
|
|
815
|
+
}]
|
|
816
|
+
}] });
|
|
817
|
+
|
|
818
|
+
// Currency management page - CRUD for multi-currency support
|
|
819
|
+
class CurrenciesComponent {
|
|
820
|
+
constructor() {
|
|
821
|
+
this.accountingService = inject(AccountingService);
|
|
822
|
+
this.pageConfig = {
|
|
823
|
+
title: 'Currencies',
|
|
824
|
+
tableConfig: this.accountingService.currenciesTableConfig
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CurrenciesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
828
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: CurrenciesComponent, isStandalone: true, selector: "spa-currencies", ngImport: i0, template: '<spa-page [config]="pageConfig"></spa-page>', isInline: true, dependencies: [{ kind: "ngmodule", type: TinSpaModule }, { kind: "component", type: PageComponent, selector: "spa-page", inputs: ["config"], outputs: ["searchModeActivated", "searchModeDeactivated", "refreshClick", "actionClick", "actionResponse", "inputChange", "createClick", "searchClick", "dataLoad", "titleActionChange"] }] }); }
|
|
829
|
+
}
|
|
830
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CurrenciesComponent, decorators: [{
|
|
831
|
+
type: Component,
|
|
832
|
+
args: [{
|
|
833
|
+
selector: 'spa-currencies',
|
|
834
|
+
standalone: true,
|
|
835
|
+
imports: [TinSpaModule],
|
|
836
|
+
template: '<spa-page [config]="pageConfig"></spa-page>'
|
|
837
|
+
}]
|
|
838
|
+
}] });
|
|
839
|
+
|
|
840
|
+
// Budget management component for CRUD operations
|
|
841
|
+
class BudgetsComponent {
|
|
842
|
+
constructor() {
|
|
843
|
+
this.accountingService = inject(AccountingService);
|
|
844
|
+
this.pageConfig = {
|
|
845
|
+
title: 'Budgets',
|
|
846
|
+
tableConfig: this.accountingService.budgetsTableConfig
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BudgetsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
850
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: BudgetsComponent, isStandalone: false, selector: "spa-budgets", ngImport: i0, template: '<spa-page [config]="pageConfig"></spa-page>', isInline: true, dependencies: [{ kind: "component", type: PageComponent, selector: "spa-page", inputs: ["config"], outputs: ["searchModeActivated", "searchModeDeactivated", "refreshClick", "actionClick", "actionResponse", "inputChange", "createClick", "searchClick", "dataLoad", "titleActionChange"] }] }); }
|
|
851
|
+
}
|
|
852
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BudgetsComponent, decorators: [{
|
|
853
|
+
type: Component,
|
|
854
|
+
args: [{
|
|
855
|
+
selector: 'spa-budgets',
|
|
856
|
+
template: '<spa-page [config]="pageConfig"></spa-page>',
|
|
857
|
+
standalone: false
|
|
858
|
+
}]
|
|
859
|
+
}] });
|
|
860
|
+
|
|
861
|
+
// Budget vs Actual report component with budget selector, tiles, and tabbed tables
|
|
862
|
+
class BudgetVsActualComponent {
|
|
863
|
+
constructor() {
|
|
864
|
+
this.accountingService = inject(AccountingService);
|
|
865
|
+
this.httpService = inject(HttpService);
|
|
866
|
+
this.selectedBudgetID = null;
|
|
867
|
+
this.reportTabConfigs = [];
|
|
868
|
+
this.summaryTileConfig = { ...this.accountingService.budgetSummaryTileConfig };
|
|
869
|
+
}
|
|
870
|
+
// Load report data when a budget is selected
|
|
871
|
+
onBudgetSelected() {
|
|
872
|
+
if (!this.selectedBudgetID) {
|
|
873
|
+
this.reportTabConfigs = [];
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
// Update tile config to load summary for selected budget
|
|
877
|
+
this.summaryTileConfig = {
|
|
878
|
+
...this.accountingService.budgetSummaryTileConfig,
|
|
879
|
+
loadAction: { url: `budgetreport/summary/${this.selectedBudgetID}` }
|
|
880
|
+
};
|
|
881
|
+
// Build tab configs with dynamic URLs for selected budget
|
|
882
|
+
this.reportTabConfigs = [
|
|
883
|
+
{
|
|
884
|
+
...this.accountingService.budgetVsActualByPeriodTableConfig,
|
|
885
|
+
loadAction: { url: `budgetreport/vsactual/${this.selectedBudgetID}` }
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
...this.accountingService.budgetVsActualByAccountTableConfig,
|
|
889
|
+
loadAction: { url: `budgetreport/summary/${this.selectedBudgetID}` }
|
|
890
|
+
}
|
|
891
|
+
];
|
|
892
|
+
}
|
|
893
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BudgetVsActualComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
894
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: BudgetVsActualComponent, isStandalone: false, selector: "spa-budget-vs-actual", ngImport: i0, template: `
|
|
895
|
+
<h4>Budget vs Actual</h4>
|
|
896
|
+
<hr>
|
|
897
|
+
|
|
898
|
+
<div style="max-width: 300px; margin-bottom: 16px;">
|
|
899
|
+
<spa-select-lite display="Select Budget" [loadAction]="{ url: 'budgets/list/x' }" optionDisplay="name" optionValue="value" [(value)]="selectedBudgetID" (valueChange)="onBudgetSelected()"></spa-select-lite><!-- Changed: optionDisplay/optionValue are NOT defaulted by spa-select-common (both default to ""), so every option rendered blank and bound undefined — no budget could be picked. 'name'/'value' match BaseController.GetList's ListOption shape, same as the statement page -->
|
|
900
|
+
</div>
|
|
901
|
+
|
|
902
|
+
<spa-tiles *ngIf="summaryTileConfig.loadAction" [config]="summaryTileConfig"></spa-tiles>
|
|
903
|
+
|
|
904
|
+
<spa-tabs *ngIf="reportTabConfigs.length > 0" [tableConfigs]="reportTabConfigs"></spa-tabs>
|
|
905
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: TilesComponent, selector: "spa-tiles", inputs: ["config", "lastSearch", "data", "reload"], outputs: ["tileActionSelected", "tileClick", "tileUnClick"] }, { kind: "component", type: TabsComponent, selector: "spa-tabs", inputs: ["tableConfigs", "reload", "reloadTab", "parentDetails", "localMode", "nestingLevel"], outputs: ["formRefresh", "actionSuccess"] }, { kind: "component", type: SelectLiteComponent, selector: "spa-select-lite" }] }); }
|
|
906
|
+
}
|
|
907
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BudgetVsActualComponent, decorators: [{
|
|
908
|
+
type: Component,
|
|
909
|
+
args: [{
|
|
910
|
+
selector: 'spa-budget-vs-actual',
|
|
911
|
+
template: `
|
|
912
|
+
<h4>Budget vs Actual</h4>
|
|
913
|
+
<hr>
|
|
914
|
+
|
|
915
|
+
<div style="max-width: 300px; margin-bottom: 16px;">
|
|
916
|
+
<spa-select-lite display="Select Budget" [loadAction]="{ url: 'budgets/list/x' }" optionDisplay="name" optionValue="value" [(value)]="selectedBudgetID" (valueChange)="onBudgetSelected()"></spa-select-lite><!-- Changed: optionDisplay/optionValue are NOT defaulted by spa-select-common (both default to ""), so every option rendered blank and bound undefined — no budget could be picked. 'name'/'value' match BaseController.GetList's ListOption shape, same as the statement page -->
|
|
917
|
+
</div>
|
|
918
|
+
|
|
919
|
+
<spa-tiles *ngIf="summaryTileConfig.loadAction" [config]="summaryTileConfig"></spa-tiles>
|
|
920
|
+
|
|
921
|
+
<spa-tabs *ngIf="reportTabConfigs.length > 0" [tableConfigs]="reportTabConfigs"></spa-tabs>
|
|
922
|
+
`,
|
|
923
|
+
standalone: false
|
|
924
|
+
}]
|
|
925
|
+
}] });
|
|
926
|
+
|
|
927
|
+
// Accounting module dashboard — pure financial/account performance (invoice-specific moved to invoicing)
|
|
928
|
+
class AccountingDashboardComponent {
|
|
929
|
+
constructor() {
|
|
930
|
+
this.summaryTiles = {
|
|
931
|
+
tiles: [
|
|
932
|
+
{ name: 'inflows', alias: 'Inflows (MTD)', color: '#4caf50', icon: 'arrow_downward', style: 'icon', info: 'MTD cash inflows' }, // Changed: Cash inflows tile
|
|
933
|
+
{ name: 'outflows', alias: 'Outflows (MTD)', color: '#f44336', icon: 'arrow_upward', style: 'icon', info: 'MTD cash outflows' }, // Changed: Cash outflows tile
|
|
934
|
+
{ name: 'netCashFlow', alias: 'Net Cash Flow', color: '#2196f3', icon: 'swap_vert', style: 'icon', info: 'Net cash movement' }, // Changed: Net cash flow tile
|
|
935
|
+
{ name: 'revenue', alias: 'Revenue (MTD)', color: '#4caf50', icon: 'trending_up', style: 'icon', info: 'MTD revenue' },
|
|
936
|
+
{ name: 'expenses', alias: 'Expenses (MTD)', color: '#f44336', icon: 'trending_down', style: 'icon', info: 'MTD expenses' },
|
|
937
|
+
{ name: 'cashBalance', alias: 'Cash & Bank', color: '#ff9800', icon: 'account_balance_wallet', style: 'icon', info: 'Cash + bank total' },
|
|
938
|
+
],
|
|
939
|
+
loadAction: { url: 'accounts/dashboard-summary/x' },
|
|
940
|
+
loadInit: true
|
|
941
|
+
};
|
|
942
|
+
// Changed: Replaced collectionRate/invoiceStatusSplit with profitMargin/expenseBreakdown
|
|
943
|
+
this.chartTiles = {
|
|
944
|
+
tiles: [
|
|
945
|
+
{ name: 'profitMargin', alias: 'Profit Margin', color: '#4caf50', chart: { type: 'doughnut', gaugeColor: '#4caf50', height: 130, dataField: 'profitMargin' }, footer: 'Net profit / revenue', footerIcon: 'pie_chart' }, // Changed: Replaced collectionRate
|
|
946
|
+
{ name: 'revenueTrendSpark', alias: 'Revenue Trend', color: '#2196f3', chart: { type: 'line', color: '#2196f3', height: 100, dataField: 'revenueTrendSpark' }, footer: 'Last 12 months', footerIcon: 'show_chart' },
|
|
947
|
+
{ name: 'expenseBreakdown', alias: 'Expense Breakdown', color: '#ff9800', chart: { type: 'pie', colors: ['#f44336', '#ff5722', '#ff9800', '#ffc107', '#ffeb3b', '#9e9e9e'], height: 130, dataField: 'expenseBreakdown' }, footer: 'Top 5 + Other', footerIcon: 'donut_large' }, // Changed: Replaced invoiceStatusSplit
|
|
948
|
+
{ name: 'expenseTrendSpark', alias: 'Expense Trend', color: '#f44336', chart: { type: 'bar', colors: ['#f44336', '#ef5350', '#e57373', '#ef9a9a', '#ffcdd2', '#f44336'], height: 100, dataField: 'expenseTrendSpark' }, footer: 'Last 6 months', footerIcon: 'trending_down' },
|
|
949
|
+
],
|
|
950
|
+
loadAction: { url: 'accounts/dashboard-chart-tiles/x' },
|
|
951
|
+
loadInit: true
|
|
952
|
+
};
|
|
953
|
+
// Changed: Replaced arAging/invoiceStatus with cashFlow/transactionVolume
|
|
954
|
+
this.chartConfig = {
|
|
955
|
+
charts: [
|
|
956
|
+
{ name: 'revenueTrend', title: 'Revenue vs Expenses Trend', type: 'line', height: '300px', showPoints: true, tension: 0.4, colors: ['#4caf50', '#f44336'], showLegend: true },
|
|
957
|
+
{ name: 'cashFlow', title: 'Monthly Cash Flow', type: 'bar', height: '300px', colors: ['#4caf50', '#f44336'], showLegend: true }, // Changed: Replaced arAging
|
|
958
|
+
{ name: 'accountBalances', title: 'Account Balances by Type', type: 'bar', height: '300px', colors: ['#2196f3', '#f44336', '#4caf50', '#ff9800', '#9c27b0'] },
|
|
959
|
+
{ name: 'transactionVolume', title: 'Monthly Transaction Volume', type: 'bar', height: '300px', colors: ['#7c4dff'] }, // Changed: Replaced invoiceStatus
|
|
960
|
+
],
|
|
961
|
+
loadAction: { url: 'accounts/dashboard-charts/x' },
|
|
962
|
+
loadInit: true,
|
|
963
|
+
columns: 2
|
|
964
|
+
};
|
|
965
|
+
// Changed: New cash/bank running balance line chart with separate data source
|
|
966
|
+
this.cashBankChartConfig = {
|
|
967
|
+
charts: [
|
|
968
|
+
{ name: 'cashBankBalances', title: 'Cash & Bank Balances (30 Days)', type: 'line', height: '500px', showPoints: true, tension: 0.4, showLegend: true,
|
|
969
|
+
options: { scales: { y: { grid: { color: '#f0f0f0', drawBorder: false }, ticks: { font: { size: 11 }, color: '#999', maxTicksLimit: 12 }, border: { display: false } } } } }, // Changed: More granular Y-axis ticks for better readability
|
|
970
|
+
],
|
|
971
|
+
loadAction: { url: 'accounts/dashboard-cash-balances/x' },
|
|
972
|
+
loadInit: true,
|
|
973
|
+
columns: 1
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AccountingDashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
977
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: AccountingDashboardComponent, isStandalone: false, selector: "spa-accounting-dashboard", ngImport: i0, template: `
|
|
978
|
+
<div class="dashboard-container">
|
|
979
|
+
<h4 class="dashboard-title"><mat-icon>account_balance</mat-icon> Accounting Dashboard</h4>
|
|
980
|
+
<spa-tiles [config]="summaryTiles"></spa-tiles>
|
|
981
|
+
<div style="margin-top: 16px;"></div>
|
|
982
|
+
<spa-tiles [config]="chartTiles"></spa-tiles>
|
|
983
|
+
<spa-charts [config]="chartConfig"></spa-charts>
|
|
984
|
+
<!-- Changed: Separate cash/bank balance chart with own data source -->
|
|
985
|
+
<spa-charts [config]="cashBankChartConfig"></spa-charts>
|
|
986
|
+
</div>
|
|
987
|
+
`, isInline: true, styles: [".dashboard-container{padding:16px}.dashboard-title{display:flex;align-items:center;gap:8px;margin-bottom:16px;color:#333;font-weight:500}\n"], dependencies: [{ kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: TilesComponent, selector: "spa-tiles", inputs: ["config", "lastSearch", "data", "reload"], outputs: ["tileActionSelected", "tileClick", "tileUnClick"] }, { kind: "component", type: ChartsComponent, selector: "spa-charts", inputs: ["config", "data", "reload"] }] }); }
|
|
988
|
+
}
|
|
989
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AccountingDashboardComponent, decorators: [{
|
|
990
|
+
type: Component,
|
|
991
|
+
args: [{ selector: 'spa-accounting-dashboard', template: `
|
|
992
|
+
<div class="dashboard-container">
|
|
993
|
+
<h4 class="dashboard-title"><mat-icon>account_balance</mat-icon> Accounting Dashboard</h4>
|
|
994
|
+
<spa-tiles [config]="summaryTiles"></spa-tiles>
|
|
995
|
+
<div style="margin-top: 16px;"></div>
|
|
996
|
+
<spa-tiles [config]="chartTiles"></spa-tiles>
|
|
997
|
+
<spa-charts [config]="chartConfig"></spa-charts>
|
|
998
|
+
<!-- Changed: Separate cash/bank balance chart with own data source -->
|
|
999
|
+
<spa-charts [config]="cashBankChartConfig"></spa-charts>
|
|
1000
|
+
</div>
|
|
1001
|
+
`, standalone: false, styles: [".dashboard-container{padding:16px}.dashboard-title{display:flex;align-items:center;gap:8px;margin-bottom:16px;color:#333;font-weight:500}\n"] }]
|
|
1002
|
+
}] });
|
|
1003
|
+
|
|
1004
|
+
// Invoice module dashboard with pipeline visibility, aging, and collection KPIs
|
|
1005
|
+
class InvoiceDashboardComponent {
|
|
1006
|
+
constructor(dataService) {
|
|
1007
|
+
this.dataService = dataService;
|
|
1008
|
+
this.attentionItems = [];
|
|
1009
|
+
// Changed: Summary tiles — invoice pipeline KPIs
|
|
1010
|
+
this.summaryTiles = {
|
|
1011
|
+
tiles: [
|
|
1012
|
+
{ name: 'totalInvoices', alias: 'Total Invoices', color: '#1565c0', icon: 'receipt_long', style: 'icon', info: 'All active invoices' },
|
|
1013
|
+
{ name: 'drafts', alias: 'Drafts', color: '#9e9e9e', icon: 'edit_note', style: 'icon', info: 'Invoices awaiting submission' },
|
|
1014
|
+
{ name: 'outstandingAmount', alias: 'Outstanding', color: '#ff9800', icon: 'pending', style: 'icon', prefix: '$', info: 'Total unpaid amount' },
|
|
1015
|
+
{ name: 'overdueCount', alias: 'Overdue', color: '#f44336', icon: 'warning', style: 'icon', info: 'Invoices past due date' },
|
|
1016
|
+
{ name: 'paidThisMonth', alias: 'Paid (Month)', color: '#4caf50', icon: 'check_circle', style: 'icon', info: 'Fully paid this month' },
|
|
1017
|
+
{ name: 'avgDaysToPay', alias: 'Avg Days to Pay', color: '#7b1fa2', icon: 'schedule', style: 'icon', suffix: ' days', info: 'Average payment turnaround' },
|
|
1018
|
+
],
|
|
1019
|
+
loadAction: { url: 'invoices/dashboard-summary/x' },
|
|
1020
|
+
loadInit: true
|
|
1021
|
+
};
|
|
1022
|
+
// Changed: Chart tiles — collection gauge, trend sparkline, status pie, aging bar
|
|
1023
|
+
this.chartTiles = {
|
|
1024
|
+
tiles: [
|
|
1025
|
+
{ name: 'collectionRate', alias: 'Collection Rate', color: '#4caf50', chart: { type: 'doughnut', gaugeColor: '#4caf50', height: 130, dataField: 'collectionRate' }, footer: 'Amount collected vs invoiced', footerIcon: 'check_circle' },
|
|
1026
|
+
{ name: 'invoiceTrend', alias: 'Invoice Trend', color: '#2196f3', chart: { type: 'line', color: '#2196f3', height: 100, dataField: 'invoiceTrend' }, footer: 'Last 12 months', footerIcon: 'timeline' },
|
|
1027
|
+
{ name: 'statusDistribution', alias: 'Status Split', color: '#ff9800', chart: { type: 'pie', colors: ['#9e9e9e', '#2196f3', '#ff9800', '#4caf50', '#f44336'], height: 130, dataField: 'statusDistribution' }, footer: 'Current distribution', footerIcon: 'pie_chart' },
|
|
1028
|
+
{ name: 'agingSnapshot', alias: 'Aging Snapshot', color: '#f44336', chart: { type: 'bar', colors: ['#4caf50', '#ff9800', '#ff5722', '#f44336', '#b71c1c'], height: 100, dataField: 'agingSnapshot' }, footer: 'Outstanding by age', footerIcon: 'bar_chart' },
|
|
1029
|
+
],
|
|
1030
|
+
loadAction: { url: 'invoices/dashboard-chart-tiles/x' },
|
|
1031
|
+
loadInit: true
|
|
1032
|
+
};
|
|
1033
|
+
// Changed: Full charts — revenue pipeline, aging breakdown, top customers, payment trend
|
|
1034
|
+
this.chartConfig = {
|
|
1035
|
+
charts: [
|
|
1036
|
+
{ name: 'revenuePipeline', title: 'Invoiced vs Collected', type: 'bar', height: '300px', showLegend: true, colors: ['#2196f3', '#4caf50'] },
|
|
1037
|
+
{ name: 'agingBreakdown', title: 'AR Aging Breakdown', type: 'bar', height: '300px', colors: ['#4caf50', '#ff9800', '#ff5722', '#f44336', '#b71c1c'] },
|
|
1038
|
+
{ name: 'topCustomers', title: 'Top Customers by Outstanding', type: 'bar', height: '300px', colors: ['#1565c0', '#1976d2', '#1e88e5', '#2196f3', '#42a5f5'] },
|
|
1039
|
+
{ name: 'paymentTrend', title: 'Payment Collection Trend', type: 'line', height: '300px', showPoints: true, tension: 0.4, colors: ['#4caf50'], showLegend: true },
|
|
1040
|
+
{ name: 'collectionPerformance', title: 'Collection Performance (12 Months)', type: 'line', height: '300px', showPoints: true, tension: 0.4, colors: ['#f57c00', '#4caf50'], showLegend: true }, // Changed: Avg days to pay + on-time rate trend
|
|
1041
|
+
],
|
|
1042
|
+
loadAction: { url: 'invoices/dashboard-charts/x' },
|
|
1043
|
+
loadInit: true,
|
|
1044
|
+
columns: 2
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
ngOnInit() {
|
|
1048
|
+
// Load attention items — top overdue invoices needing action
|
|
1049
|
+
this.dataService.CallApi({ url: 'invoices/dashboard-attention/x' }).subscribe((res) => {
|
|
1050
|
+
if (res.success)
|
|
1051
|
+
this.attentionItems = res.data || [];
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: InvoiceDashboardComponent, deps: [{ token: i1.DataServiceLib }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1055
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: InvoiceDashboardComponent, isStandalone: false, selector: "spa-invoice-dashboard", ngImport: i0, template: `
|
|
1056
|
+
<div class="dashboard-container">
|
|
1057
|
+
<h4 class="dashboard-title"><mat-icon>receipt_long</mat-icon> Invoice Dashboard</h4>
|
|
1058
|
+
<spa-tiles [config]="summaryTiles"></spa-tiles>
|
|
1059
|
+
<div style="margin-top: 16px;"></div>
|
|
1060
|
+
<spa-tiles [config]="chartTiles"></spa-tiles>
|
|
1061
|
+
|
|
1062
|
+
<!-- Attention Required sub-table -->
|
|
1063
|
+
<div *ngIf="attentionItems.length > 0" class="attention-section">
|
|
1064
|
+
<h5 class="section-title">
|
|
1065
|
+
<mat-icon style="vertical-align: middle; margin-right: 4px; color: #f44336;">warning</mat-icon>
|
|
1066
|
+
Needs Attention
|
|
1067
|
+
</h5>
|
|
1068
|
+
<table class="kpi-table">
|
|
1069
|
+
<thead>
|
|
1070
|
+
<tr>
|
|
1071
|
+
<th>Invoice</th>
|
|
1072
|
+
<th>Customer</th>
|
|
1073
|
+
<th>Due Date</th>
|
|
1074
|
+
<th>Days Overdue</th>
|
|
1075
|
+
<th>Outstanding</th>
|
|
1076
|
+
</tr>
|
|
1077
|
+
</thead>
|
|
1078
|
+
<tbody>
|
|
1079
|
+
<tr *ngFor="let item of attentionItems">
|
|
1080
|
+
<td>{{ item.invoiceNumber }}</td>
|
|
1081
|
+
<td>{{ item.customerName }}</td>
|
|
1082
|
+
<td>{{ item.dueDate | date:'mediumDate' }}</td>
|
|
1083
|
+
<td style="color: #f44336; font-weight: 500;">{{ item.daysOverdue }}</td>
|
|
1084
|
+
<td>{{ item.outstandingAmount | currency }}</td>
|
|
1085
|
+
</tr>
|
|
1086
|
+
</tbody>
|
|
1087
|
+
</table>
|
|
1088
|
+
</div>
|
|
1089
|
+
|
|
1090
|
+
<spa-charts [config]="chartConfig"></spa-charts>
|
|
1091
|
+
</div>
|
|
1092
|
+
`, isInline: true, styles: [".dashboard-container{padding:16px}.dashboard-title{display:flex;align-items:center;gap:8px;margin-bottom:16px;color:#333;font-weight:500}.attention-section{margin-top:16px}.section-title{display:flex;align-items:center;margin-bottom:8px;color:#333;font-weight:500}.kpi-table{width:100%;border-collapse:collapse;font-size:13px}.kpi-table th{text-align:left;padding:8px 12px;background:#f5f5f5;border-bottom:2px solid #e0e0e0;font-weight:500}.kpi-table td{padding:8px 12px;border-bottom:1px solid #eee}.kpi-table tr:hover{background:#fafafa}\n"], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: TilesComponent, selector: "spa-tiles", inputs: ["config", "lastSearch", "data", "reload"], outputs: ["tileActionSelected", "tileClick", "tileUnClick"] }, { kind: "component", type: ChartsComponent, selector: "spa-charts", inputs: ["config", "data", "reload"] }, { kind: "pipe", type: i1$1.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1$1.DatePipe, name: "date" }] }); }
|
|
1093
|
+
}
|
|
1094
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: InvoiceDashboardComponent, decorators: [{
|
|
1095
|
+
type: Component,
|
|
1096
|
+
args: [{ selector: 'spa-invoice-dashboard', template: `
|
|
1097
|
+
<div class="dashboard-container">
|
|
1098
|
+
<h4 class="dashboard-title"><mat-icon>receipt_long</mat-icon> Invoice Dashboard</h4>
|
|
1099
|
+
<spa-tiles [config]="summaryTiles"></spa-tiles>
|
|
1100
|
+
<div style="margin-top: 16px;"></div>
|
|
1101
|
+
<spa-tiles [config]="chartTiles"></spa-tiles>
|
|
1102
|
+
|
|
1103
|
+
<!-- Attention Required sub-table -->
|
|
1104
|
+
<div *ngIf="attentionItems.length > 0" class="attention-section">
|
|
1105
|
+
<h5 class="section-title">
|
|
1106
|
+
<mat-icon style="vertical-align: middle; margin-right: 4px; color: #f44336;">warning</mat-icon>
|
|
1107
|
+
Needs Attention
|
|
1108
|
+
</h5>
|
|
1109
|
+
<table class="kpi-table">
|
|
1110
|
+
<thead>
|
|
1111
|
+
<tr>
|
|
1112
|
+
<th>Invoice</th>
|
|
1113
|
+
<th>Customer</th>
|
|
1114
|
+
<th>Due Date</th>
|
|
1115
|
+
<th>Days Overdue</th>
|
|
1116
|
+
<th>Outstanding</th>
|
|
1117
|
+
</tr>
|
|
1118
|
+
</thead>
|
|
1119
|
+
<tbody>
|
|
1120
|
+
<tr *ngFor="let item of attentionItems">
|
|
1121
|
+
<td>{{ item.invoiceNumber }}</td>
|
|
1122
|
+
<td>{{ item.customerName }}</td>
|
|
1123
|
+
<td>{{ item.dueDate | date:'mediumDate' }}</td>
|
|
1124
|
+
<td style="color: #f44336; font-weight: 500;">{{ item.daysOverdue }}</td>
|
|
1125
|
+
<td>{{ item.outstandingAmount | currency }}</td>
|
|
1126
|
+
</tr>
|
|
1127
|
+
</tbody>
|
|
1128
|
+
</table>
|
|
1129
|
+
</div>
|
|
1130
|
+
|
|
1131
|
+
<spa-charts [config]="chartConfig"></spa-charts>
|
|
1132
|
+
</div>
|
|
1133
|
+
`, standalone: false, styles: [".dashboard-container{padding:16px}.dashboard-title{display:flex;align-items:center;gap:8px;margin-bottom:16px;color:#333;font-weight:500}.attention-section{margin-top:16px}.section-title{display:flex;align-items:center;margin-bottom:8px;color:#333;font-weight:500}.kpi-table{width:100%;border-collapse:collapse;font-size:13px}.kpi-table th{text-align:left;padding:8px 12px;background:#f5f5f5;border-bottom:2px solid #e0e0e0;font-weight:500}.kpi-table td{padding:8px 12px;border-bottom:1px solid #eee}.kpi-table tr:hover{background:#fafafa}\n"] }]
|
|
1134
|
+
}], ctorParameters: () => [{ type: i1.DataServiceLib }] });
|
|
1135
|
+
|
|
1136
|
+
// Credit Notes page (B6) — invoice corrections/returns with GL reversal posting
|
|
1137
|
+
class CreditNotesComponent {
|
|
1138
|
+
constructor() {
|
|
1139
|
+
this.accountingService = inject(AccountingService);
|
|
1140
|
+
this.pageConfig = {
|
|
1141
|
+
title: 'Credit Notes',
|
|
1142
|
+
tableConfig: this.accountingService.creditNotesTableConfig
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CreditNotesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1146
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: CreditNotesComponent, isStandalone: false, selector: "spa-credit-notes", ngImport: i0, template: '<spa-page [config]="pageConfig"></spa-page>', isInline: true, dependencies: [{ kind: "component", type: PageComponent, selector: "spa-page", inputs: ["config"], outputs: ["searchModeActivated", "searchModeDeactivated", "refreshClick", "actionClick", "actionResponse", "inputChange", "createClick", "searchClick", "dataLoad", "titleActionChange"] }] }); }
|
|
1147
|
+
}
|
|
1148
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CreditNotesComponent, decorators: [{
|
|
1149
|
+
type: Component,
|
|
1150
|
+
args: [{
|
|
1151
|
+
selector: 'spa-credit-notes',
|
|
1152
|
+
template: '<spa-page [config]="pageConfig"></spa-page>',
|
|
1153
|
+
standalone: false
|
|
1154
|
+
}]
|
|
1155
|
+
}] });
|
|
1156
|
+
|
|
1157
|
+
// VAT Return page (B2) — ZIMRA VAT7-style summary for a selected tax period
|
|
1158
|
+
// Phase 2 compliance: sales splits (standard/zero-rated/exempt), per-currency breakdown, GL reconciliation, WHT remittance tab
|
|
1159
|
+
class VatReturnComponent {
|
|
1160
|
+
constructor() {
|
|
1161
|
+
this.httpService = inject(HttpService);
|
|
1162
|
+
this.http = inject(HttpClient);
|
|
1163
|
+
this.from = '';
|
|
1164
|
+
this.to = '';
|
|
1165
|
+
this.data = null;
|
|
1166
|
+
this.whtData = []; // Added: WHT remittance rows for the selected period
|
|
1167
|
+
}
|
|
1168
|
+
ngOnInit() {
|
|
1169
|
+
// Default period: current calendar month
|
|
1170
|
+
const now = new Date();
|
|
1171
|
+
this.from = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().substring(0, 10);
|
|
1172
|
+
this.to = new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString().substring(0, 10);
|
|
1173
|
+
this.load();
|
|
1174
|
+
}
|
|
1175
|
+
load() {
|
|
1176
|
+
this.http.get(`${this.httpService.apiUrl}reports/vat-return?from=${this.from}&to=${this.to}`)
|
|
1177
|
+
.subscribe(res => this.data = res?.data ?? res?.Data ?? null);
|
|
1178
|
+
// Added: load WHT remittance rows for the same period (SupplierPayments criteria, value = "from,to")
|
|
1179
|
+
this.http.get(`${this.httpService.apiUrl}supplierpayments/wht-remittance/${this.from},${this.to}`) // Changed: aligned with backend SupplierPaymentsController.CustomCriteria route shape
|
|
1180
|
+
.subscribe(res => this.whtData = res?.data ?? res?.Data ?? []);
|
|
1181
|
+
}
|
|
1182
|
+
export() {
|
|
1183
|
+
this.http.get(`${this.httpService.apiUrl}reports/export/vat-return?from=${this.from}&to=${this.to}`, { responseType: 'blob' })
|
|
1184
|
+
.subscribe((response) => {
|
|
1185
|
+
const url = window.URL.createObjectURL(new Blob([response]));
|
|
1186
|
+
const link = document.createElement('a');
|
|
1187
|
+
link.href = url;
|
|
1188
|
+
link.download = `vat-return-${this.from}-to-${this.to}.xlsx`;
|
|
1189
|
+
link.click();
|
|
1190
|
+
window.URL.revokeObjectURL(url);
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: VatReturnComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1194
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: VatReturnComponent, isStandalone: false, selector: "spa-vat-return", ngImport: i0, template: `
|
|
1195
|
+
<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
1196
|
+
<h4>VAT Return</h4>
|
|
1197
|
+
<button mat-stroked-button (click)="export()" [disabled]="!data">
|
|
1198
|
+
<mat-icon>download</mat-icon> Export to Excel
|
|
1199
|
+
</button>
|
|
1200
|
+
</div>
|
|
1201
|
+
<hr>
|
|
1202
|
+
|
|
1203
|
+
<div style="display: flex; gap: 16px; align-items: center; margin-bottom: 16px;">
|
|
1204
|
+
<mat-form-field>
|
|
1205
|
+
<mat-label>From</mat-label>
|
|
1206
|
+
<input matInput type="date" [(ngModel)]="from">
|
|
1207
|
+
</mat-form-field>
|
|
1208
|
+
<mat-form-field>
|
|
1209
|
+
<mat-label>To</mat-label>
|
|
1210
|
+
<input matInput type="date" [(ngModel)]="to">
|
|
1211
|
+
</mat-form-field>
|
|
1212
|
+
<button mat-flat-button color="primary" (click)="load()">Run</button>
|
|
1213
|
+
</div>
|
|
1214
|
+
|
|
1215
|
+
<mat-tab-group>
|
|
1216
|
+
<mat-tab label="VAT Return">
|
|
1217
|
+
<mat-card *ngIf="data" style="max-width: 640px; padding: 16px; margin-top: 16px;">
|
|
1218
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
1219
|
+
<tr><td style="padding: 6px 0;">Standard Rated Sales (excl. VAT)</td><td style="text-align: right;">{{ data.standardRatedSales | number:'1.2-2' }}</td></tr>
|
|
1220
|
+
<tr><td style="padding: 6px 0;">Zero Rated Sales</td><td style="text-align: right;">{{ data.zeroRatedSales | number:'1.2-2' }}</td></tr>
|
|
1221
|
+
<tr *ngIf="data.exemptSales != null"><td style="padding: 6px 0;">Exempt Sales</td><td style="text-align: right;">{{ data.exemptSales | number:'1.2-2' }}</td></tr>
|
|
1222
|
+
<tr style="border-top: 1px solid #ddd;"><td style="padding: 6px 0;"><b>Total Sales</b></td><td style="text-align: right;"><b>{{ data.totalSales | number:'1.2-2' }}</b></td></tr>
|
|
1223
|
+
<tr><td style="padding: 6px 0;">Output VAT</td><td style="text-align: right;">{{ data.outputVat | number:'1.2-2' }}</td></tr>
|
|
1224
|
+
<tr><td style="padding: 6px 0;">Total Purchases (excl. VAT)</td><td style="text-align: right;">{{ data.totalPurchases | number:'1.2-2' }}</td></tr>
|
|
1225
|
+
<tr><td style="padding: 6px 0;">Input VAT</td><td style="text-align: right;">{{ data.inputVat | number:'1.2-2' }}</td></tr>
|
|
1226
|
+
<tr style="border-top: 2px solid #333;">
|
|
1227
|
+
<td style="padding: 8px 0;"><b>{{ data.isPayable ? 'VAT Payable to ZIMRA' : 'VAT Refundable' }}</b></td>
|
|
1228
|
+
<td style="text-align: right;"><b [style.color]="data.isPayable ? 'red' : 'green'">{{ data.netVat | number:'1.2-2' }}</b></td>
|
|
1229
|
+
</tr>
|
|
1230
|
+
</table>
|
|
1231
|
+
</mat-card>
|
|
1232
|
+
|
|
1233
|
+
<!-- Added: per-currency breakdown from byCurrency -->
|
|
1234
|
+
<mat-card *ngIf="data?.byCurrency?.length" style="max-width: 900px; padding: 16px; margin-top: 16px;">
|
|
1235
|
+
<h5 style="margin-top: 0;">Breakdown by Currency</h5>
|
|
1236
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
1237
|
+
<tr style="border-bottom: 1px solid #ddd;">
|
|
1238
|
+
<th style="text-align: left; padding: 6px 0;">Currency</th>
|
|
1239
|
+
<th style="text-align: right;">Standard Sales</th>
|
|
1240
|
+
<th style="text-align: right;">Zero Rated</th>
|
|
1241
|
+
<th style="text-align: right;">Exempt</th>
|
|
1242
|
+
<th style="text-align: right;">Output VAT</th>
|
|
1243
|
+
<th style="text-align: right;">Input VAT</th>
|
|
1244
|
+
<th style="text-align: right;">Net VAT</th>
|
|
1245
|
+
</tr>
|
|
1246
|
+
<tr *ngFor="let c of data.byCurrency">
|
|
1247
|
+
<td style="padding: 6px 0;">{{ c.currencyCode }}</td>
|
|
1248
|
+
<td style="text-align: right;">{{ c.standardRatedSales | number:'1.2-2' }}</td>
|
|
1249
|
+
<td style="text-align: right;">{{ c.zeroRatedSales | number:'1.2-2' }}</td>
|
|
1250
|
+
<td style="text-align: right;">{{ c.exemptSales | number:'1.2-2' }}</td>
|
|
1251
|
+
<td style="text-align: right;">{{ c.outputVat | number:'1.2-2' }}</td>
|
|
1252
|
+
<td style="text-align: right;">{{ c.inputVat | number:'1.2-2' }}</td>
|
|
1253
|
+
<td style="text-align: right;"><b>{{ c.netVat | number:'1.2-2' }}</b></td>
|
|
1254
|
+
</tr>
|
|
1255
|
+
</table>
|
|
1256
|
+
</mat-card>
|
|
1257
|
+
|
|
1258
|
+
<!-- Added: GL reconciliation — output VAT per GL vs per documents -->
|
|
1259
|
+
<mat-card *ngIf="data?.glReconciliation" style="max-width: 640px; padding: 16px; margin-top: 16px;">
|
|
1260
|
+
<h5 style="margin-top: 0;">GL Reconciliation (Output VAT)</h5>
|
|
1261
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
1262
|
+
<tr><td style="padding: 6px 0;">Output VAT per GL</td><td style="text-align: right;">{{ data.glReconciliation.outputVatGL | number:'1.2-2' }}</td></tr>
|
|
1263
|
+
<tr><td style="padding: 6px 0;">Output VAT per Documents</td><td style="text-align: right;">{{ data.glReconciliation.outputVatFromDocuments | number:'1.2-2' }}</td></tr>
|
|
1264
|
+
<tr style="border-top: 1px solid #ddd;">
|
|
1265
|
+
<td style="padding: 6px 0;"><b>Difference</b></td>
|
|
1266
|
+
<td style="text-align: right;"><b [style.color]="(data.glReconciliation.difference | number:'1.2-2') !== '0.00' ? '#e65100' : 'inherit'">{{ data.glReconciliation.difference | number:'1.2-2' }}</b></td>
|
|
1267
|
+
</tr>
|
|
1268
|
+
</table>
|
|
1269
|
+
<p *ngIf="(data.glReconciliation.difference | number:'1.2-2') !== '0.00'" style="color: #e65100; margin: 8px 0 0;">
|
|
1270
|
+
<mat-icon style="vertical-align: middle; font-size: 18px;">warning</mat-icon>
|
|
1271
|
+
GL does not agree with document totals — investigate before filing.
|
|
1272
|
+
</p>
|
|
1273
|
+
</mat-card>
|
|
1274
|
+
</mat-tab>
|
|
1275
|
+
|
|
1276
|
+
<!-- Added: WHT remittance report — 30% withholding on payments to suppliers without valid ITF263 -->
|
|
1277
|
+
<mat-tab label="Withholding Tax">
|
|
1278
|
+
<mat-card style="max-width: 900px; padding: 16px; margin-top: 16px;">
|
|
1279
|
+
<h5 style="margin-top: 0;">Withholding Tax Remittance (30% — no valid ITF263)</h5>
|
|
1280
|
+
<table *ngIf="whtData?.length" style="width: 100%; border-collapse: collapse;">
|
|
1281
|
+
<tr style="border-bottom: 1px solid #ddd;">
|
|
1282
|
+
<th style="text-align: left; padding: 6px 0;">Supplier</th>
|
|
1283
|
+
<th style="text-align: left;">Date</th>
|
|
1284
|
+
<th style="text-align: right;">Gross</th>
|
|
1285
|
+
<th style="text-align: right;">WHT</th>
|
|
1286
|
+
<th style="text-align: right;">Net Paid</th>
|
|
1287
|
+
</tr>
|
|
1288
|
+
<tr *ngFor="let w of whtData">
|
|
1289
|
+
<td style="padding: 6px 0;">{{ w.supplierName || w.supplier }}</td>
|
|
1290
|
+
<td>{{ (w.paymentDate || w.date) | date:'mediumDate' }}</td>
|
|
1291
|
+
<td style="text-align: right;">{{ (w.grossAmount ?? w.gross) | number:'1.2-2' }}</td>
|
|
1292
|
+
<td style="text-align: right;">{{ (w.whtAmount ?? w.wht) | number:'1.2-2' }}</td>
|
|
1293
|
+
<td style="text-align: right;">{{ (w.netAmount ?? w.net) | number:'1.2-2' }}</td>
|
|
1294
|
+
</tr>
|
|
1295
|
+
</table>
|
|
1296
|
+
<p *ngIf="!whtData?.length" style="color: #757575;">No withheld supplier payments in this period.</p>
|
|
1297
|
+
</mat-card>
|
|
1298
|
+
</mat-tab>
|
|
1299
|
+
</mat-tab-group>
|
|
1300
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.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: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$2.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i3$2.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: i3$3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$3.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: i6.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$1.DatePipe, name: "date" }] }); }
|
|
1301
|
+
}
|
|
1302
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: VatReturnComponent, decorators: [{
|
|
1303
|
+
type: Component,
|
|
1304
|
+
args: [{
|
|
1305
|
+
selector: 'spa-vat-return',
|
|
1306
|
+
template: `
|
|
1307
|
+
<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
1308
|
+
<h4>VAT Return</h4>
|
|
1309
|
+
<button mat-stroked-button (click)="export()" [disabled]="!data">
|
|
1310
|
+
<mat-icon>download</mat-icon> Export to Excel
|
|
1311
|
+
</button>
|
|
1312
|
+
</div>
|
|
1313
|
+
<hr>
|
|
1314
|
+
|
|
1315
|
+
<div style="display: flex; gap: 16px; align-items: center; margin-bottom: 16px;">
|
|
1316
|
+
<mat-form-field>
|
|
1317
|
+
<mat-label>From</mat-label>
|
|
1318
|
+
<input matInput type="date" [(ngModel)]="from">
|
|
1319
|
+
</mat-form-field>
|
|
1320
|
+
<mat-form-field>
|
|
1321
|
+
<mat-label>To</mat-label>
|
|
1322
|
+
<input matInput type="date" [(ngModel)]="to">
|
|
1323
|
+
</mat-form-field>
|
|
1324
|
+
<button mat-flat-button color="primary" (click)="load()">Run</button>
|
|
1325
|
+
</div>
|
|
1326
|
+
|
|
1327
|
+
<mat-tab-group>
|
|
1328
|
+
<mat-tab label="VAT Return">
|
|
1329
|
+
<mat-card *ngIf="data" style="max-width: 640px; padding: 16px; margin-top: 16px;">
|
|
1330
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
1331
|
+
<tr><td style="padding: 6px 0;">Standard Rated Sales (excl. VAT)</td><td style="text-align: right;">{{ data.standardRatedSales | number:'1.2-2' }}</td></tr>
|
|
1332
|
+
<tr><td style="padding: 6px 0;">Zero Rated Sales</td><td style="text-align: right;">{{ data.zeroRatedSales | number:'1.2-2' }}</td></tr>
|
|
1333
|
+
<tr *ngIf="data.exemptSales != null"><td style="padding: 6px 0;">Exempt Sales</td><td style="text-align: right;">{{ data.exemptSales | number:'1.2-2' }}</td></tr>
|
|
1334
|
+
<tr style="border-top: 1px solid #ddd;"><td style="padding: 6px 0;"><b>Total Sales</b></td><td style="text-align: right;"><b>{{ data.totalSales | number:'1.2-2' }}</b></td></tr>
|
|
1335
|
+
<tr><td style="padding: 6px 0;">Output VAT</td><td style="text-align: right;">{{ data.outputVat | number:'1.2-2' }}</td></tr>
|
|
1336
|
+
<tr><td style="padding: 6px 0;">Total Purchases (excl. VAT)</td><td style="text-align: right;">{{ data.totalPurchases | number:'1.2-2' }}</td></tr>
|
|
1337
|
+
<tr><td style="padding: 6px 0;">Input VAT</td><td style="text-align: right;">{{ data.inputVat | number:'1.2-2' }}</td></tr>
|
|
1338
|
+
<tr style="border-top: 2px solid #333;">
|
|
1339
|
+
<td style="padding: 8px 0;"><b>{{ data.isPayable ? 'VAT Payable to ZIMRA' : 'VAT Refundable' }}</b></td>
|
|
1340
|
+
<td style="text-align: right;"><b [style.color]="data.isPayable ? 'red' : 'green'">{{ data.netVat | number:'1.2-2' }}</b></td>
|
|
1341
|
+
</tr>
|
|
1342
|
+
</table>
|
|
1343
|
+
</mat-card>
|
|
1344
|
+
|
|
1345
|
+
<!-- Added: per-currency breakdown from byCurrency -->
|
|
1346
|
+
<mat-card *ngIf="data?.byCurrency?.length" style="max-width: 900px; padding: 16px; margin-top: 16px;">
|
|
1347
|
+
<h5 style="margin-top: 0;">Breakdown by Currency</h5>
|
|
1348
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
1349
|
+
<tr style="border-bottom: 1px solid #ddd;">
|
|
1350
|
+
<th style="text-align: left; padding: 6px 0;">Currency</th>
|
|
1351
|
+
<th style="text-align: right;">Standard Sales</th>
|
|
1352
|
+
<th style="text-align: right;">Zero Rated</th>
|
|
1353
|
+
<th style="text-align: right;">Exempt</th>
|
|
1354
|
+
<th style="text-align: right;">Output VAT</th>
|
|
1355
|
+
<th style="text-align: right;">Input VAT</th>
|
|
1356
|
+
<th style="text-align: right;">Net VAT</th>
|
|
1357
|
+
</tr>
|
|
1358
|
+
<tr *ngFor="let c of data.byCurrency">
|
|
1359
|
+
<td style="padding: 6px 0;">{{ c.currencyCode }}</td>
|
|
1360
|
+
<td style="text-align: right;">{{ c.standardRatedSales | number:'1.2-2' }}</td>
|
|
1361
|
+
<td style="text-align: right;">{{ c.zeroRatedSales | number:'1.2-2' }}</td>
|
|
1362
|
+
<td style="text-align: right;">{{ c.exemptSales | number:'1.2-2' }}</td>
|
|
1363
|
+
<td style="text-align: right;">{{ c.outputVat | number:'1.2-2' }}</td>
|
|
1364
|
+
<td style="text-align: right;">{{ c.inputVat | number:'1.2-2' }}</td>
|
|
1365
|
+
<td style="text-align: right;"><b>{{ c.netVat | number:'1.2-2' }}</b></td>
|
|
1366
|
+
</tr>
|
|
1367
|
+
</table>
|
|
1368
|
+
</mat-card>
|
|
1369
|
+
|
|
1370
|
+
<!-- Added: GL reconciliation — output VAT per GL vs per documents -->
|
|
1371
|
+
<mat-card *ngIf="data?.glReconciliation" style="max-width: 640px; padding: 16px; margin-top: 16px;">
|
|
1372
|
+
<h5 style="margin-top: 0;">GL Reconciliation (Output VAT)</h5>
|
|
1373
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
1374
|
+
<tr><td style="padding: 6px 0;">Output VAT per GL</td><td style="text-align: right;">{{ data.glReconciliation.outputVatGL | number:'1.2-2' }}</td></tr>
|
|
1375
|
+
<tr><td style="padding: 6px 0;">Output VAT per Documents</td><td style="text-align: right;">{{ data.glReconciliation.outputVatFromDocuments | number:'1.2-2' }}</td></tr>
|
|
1376
|
+
<tr style="border-top: 1px solid #ddd;">
|
|
1377
|
+
<td style="padding: 6px 0;"><b>Difference</b></td>
|
|
1378
|
+
<td style="text-align: right;"><b [style.color]="(data.glReconciliation.difference | number:'1.2-2') !== '0.00' ? '#e65100' : 'inherit'">{{ data.glReconciliation.difference | number:'1.2-2' }}</b></td>
|
|
1379
|
+
</tr>
|
|
1380
|
+
</table>
|
|
1381
|
+
<p *ngIf="(data.glReconciliation.difference | number:'1.2-2') !== '0.00'" style="color: #e65100; margin: 8px 0 0;">
|
|
1382
|
+
<mat-icon style="vertical-align: middle; font-size: 18px;">warning</mat-icon>
|
|
1383
|
+
GL does not agree with document totals — investigate before filing.
|
|
1384
|
+
</p>
|
|
1385
|
+
</mat-card>
|
|
1386
|
+
</mat-tab>
|
|
1387
|
+
|
|
1388
|
+
<!-- Added: WHT remittance report — 30% withholding on payments to suppliers without valid ITF263 -->
|
|
1389
|
+
<mat-tab label="Withholding Tax">
|
|
1390
|
+
<mat-card style="max-width: 900px; padding: 16px; margin-top: 16px;">
|
|
1391
|
+
<h5 style="margin-top: 0;">Withholding Tax Remittance (30% — no valid ITF263)</h5>
|
|
1392
|
+
<table *ngIf="whtData?.length" style="width: 100%; border-collapse: collapse;">
|
|
1393
|
+
<tr style="border-bottom: 1px solid #ddd;">
|
|
1394
|
+
<th style="text-align: left; padding: 6px 0;">Supplier</th>
|
|
1395
|
+
<th style="text-align: left;">Date</th>
|
|
1396
|
+
<th style="text-align: right;">Gross</th>
|
|
1397
|
+
<th style="text-align: right;">WHT</th>
|
|
1398
|
+
<th style="text-align: right;">Net Paid</th>
|
|
1399
|
+
</tr>
|
|
1400
|
+
<tr *ngFor="let w of whtData">
|
|
1401
|
+
<td style="padding: 6px 0;">{{ w.supplierName || w.supplier }}</td>
|
|
1402
|
+
<td>{{ (w.paymentDate || w.date) | date:'mediumDate' }}</td>
|
|
1403
|
+
<td style="text-align: right;">{{ (w.grossAmount ?? w.gross) | number:'1.2-2' }}</td>
|
|
1404
|
+
<td style="text-align: right;">{{ (w.whtAmount ?? w.wht) | number:'1.2-2' }}</td>
|
|
1405
|
+
<td style="text-align: right;">{{ (w.netAmount ?? w.net) | number:'1.2-2' }}</td>
|
|
1406
|
+
</tr>
|
|
1407
|
+
</table>
|
|
1408
|
+
<p *ngIf="!whtData?.length" style="color: #757575;">No withheld supplier payments in this period.</p>
|
|
1409
|
+
</mat-card>
|
|
1410
|
+
</mat-tab>
|
|
1411
|
+
</mat-tab-group>
|
|
1412
|
+
`,
|
|
1413
|
+
standalone: false
|
|
1414
|
+
}]
|
|
1415
|
+
}] });
|
|
1416
|
+
|
|
1417
|
+
// Fiscal Periods page (C2) — period create/close/reopen/lock management
|
|
1418
|
+
class FiscalPeriodsComponent {
|
|
1419
|
+
constructor() {
|
|
1420
|
+
this.accountingService = inject(AccountingService);
|
|
1421
|
+
this.pageConfig = {
|
|
1422
|
+
title: 'Fiscal Periods',
|
|
1423
|
+
tableConfig: this.accountingService.fiscalPeriodsTableConfig
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
ngOnInit() {
|
|
1427
|
+
}
|
|
1428
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: FiscalPeriodsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1429
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: FiscalPeriodsComponent, isStandalone: false, selector: "spa-fiscal-periods", ngImport: i0, template: '<spa-page [config]="pageConfig"></spa-page>', isInline: true, dependencies: [{ kind: "component", type: PageComponent, selector: "spa-page", inputs: ["config"], outputs: ["searchModeActivated", "searchModeDeactivated", "refreshClick", "actionClick", "actionResponse", "inputChange", "createClick", "searchClick", "dataLoad", "titleActionChange"] }] }); }
|
|
1430
|
+
}
|
|
1431
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: FiscalPeriodsComponent, decorators: [{
|
|
1432
|
+
type: Component,
|
|
1433
|
+
args: [{
|
|
1434
|
+
selector: 'spa-fiscal-periods',
|
|
1435
|
+
template: '<spa-page [config]="pageConfig"></spa-page>',
|
|
1436
|
+
standalone: false
|
|
1437
|
+
}]
|
|
1438
|
+
}] });
|
|
1439
|
+
|
|
1440
|
+
// Customer Receipts page (C3) — receipts list with new-receipt and allocate actions
|
|
1441
|
+
class ReceiptsComponent {
|
|
1442
|
+
constructor() {
|
|
1443
|
+
this.accountingService = inject(AccountingService);
|
|
1444
|
+
this.dataService = inject(DataServiceLib); // Added: supplies dialogPanelClass so this dialog honours appConfig.dialogStyle
|
|
1445
|
+
this.dialog = inject(MatDialog);
|
|
1446
|
+
this.pageConfig = {
|
|
1447
|
+
title: 'Customer Receipts',
|
|
1448
|
+
tableConfig: this.accountingService.customerReceiptsTableConfig
|
|
1449
|
+
};
|
|
1450
|
+
// Changed: the allocate wiring is GONE from here. AccountingService now owns receiptAllocateButton.onClick,
|
|
1451
|
+
// so the button works in every consumer instead of only on this page — this constructor was the single
|
|
1452
|
+
// reason shift's and grip's Day Book allocate lanes could not use it. Re-assigning it here would just
|
|
1453
|
+
// narrow it back to this page's dialog, and the service's handler is the same dialog with the same data.
|
|
1454
|
+
// Changed: the PDF wiring is GONE from here too — AccountingService now owns receiptPdfButton.onClick via
|
|
1455
|
+
// downloadReceiptPdf, so the button works in every consumer. That was the last library backlog row of the
|
|
1456
|
+
// pair. This constructor is now empty of wiring and exists only to satisfy the class.
|
|
1457
|
+
}
|
|
1458
|
+
ngOnInit() {
|
|
1459
|
+
}
|
|
1460
|
+
// Changed: this is now the NEW RECEIPT path only — allocation moved to AccountingService.openReceiptAllocation.
|
|
1461
|
+
// The optional row parameter is kept so the signature stays compatible for any consumer holding a reference.
|
|
1462
|
+
openDialog(row) {
|
|
1463
|
+
this.dialog.open(ReceiptDialogComponent, { width: '860px', maxWidth: '95vw', data: row ?? null, panelClass: this.dataService?.dialogPanelClass ?? [] }) // Changed: was missing panelClass, so this dialog never picked up the modern surface
|
|
1464
|
+
.afterClosed().subscribe(saved => { if (saved)
|
|
1465
|
+
this.page?.tableReload.next(true); });
|
|
1466
|
+
}
|
|
1467
|
+
// Changed: the blob download itself moved to AccountingService.downloadReceiptPdf. This method is kept as a
|
|
1468
|
+
// thin delegate so the signature stays available to any consumer holding a reference to this component,
|
|
1469
|
+
// exactly as openDialog's optional row parameter was kept in 3a681d6.
|
|
1470
|
+
downloadPdf(receipt) {
|
|
1471
|
+
this.accountingService.downloadReceiptPdf(receipt);
|
|
1472
|
+
}
|
|
1473
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ReceiptsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1474
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: ReceiptsComponent, isStandalone: false, selector: "spa-receipts", viewQueries: [{ propertyName: "page", first: true, predicate: PageComponent, descendants: true }], ngImport: i0, template: `
|
|
1475
|
+
<div style="display: flex; justify-content: flex-end;">
|
|
1476
|
+
<button mat-stroked-button (click)="openDialog()">
|
|
1477
|
+
<mat-icon>receipt</mat-icon> New Receipt
|
|
1478
|
+
</button>
|
|
1479
|
+
</div>
|
|
1480
|
+
<spa-page [config]="pageConfig"></spa-page>
|
|
1481
|
+
`, isInline: true, dependencies: [{ kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: PageComponent, selector: "spa-page", inputs: ["config"], outputs: ["searchModeActivated", "searchModeDeactivated", "refreshClick", "actionClick", "actionResponse", "inputChange", "createClick", "searchClick", "dataLoad", "titleActionChange"] }] }); }
|
|
1482
|
+
}
|
|
1483
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ReceiptsComponent, decorators: [{
|
|
1484
|
+
type: Component,
|
|
1485
|
+
args: [{
|
|
1486
|
+
selector: 'spa-receipts',
|
|
1487
|
+
template: `
|
|
1488
|
+
<div style="display: flex; justify-content: flex-end;">
|
|
1489
|
+
<button mat-stroked-button (click)="openDialog()">
|
|
1490
|
+
<mat-icon>receipt</mat-icon> New Receipt
|
|
1491
|
+
</button>
|
|
1492
|
+
</div>
|
|
1493
|
+
<spa-page [config]="pageConfig"></spa-page>
|
|
1494
|
+
`,
|
|
1495
|
+
standalone: false
|
|
1496
|
+
}]
|
|
1497
|
+
}], ctorParameters: () => [], propDecorators: { page: [{
|
|
1498
|
+
type: ViewChild,
|
|
1499
|
+
args: [PageComponent]
|
|
1500
|
+
}] } });
|
|
1501
|
+
|
|
1502
|
+
// Bank statement import dialog (C4) — paste CSV lines (date, description, reference, amount) for a GL cash/bank account
|
|
1503
|
+
class StatementImportDialogComponent {
|
|
1504
|
+
constructor() {
|
|
1505
|
+
this.dialogRef = inject((MatDialogRef));
|
|
1506
|
+
this.httpService = inject(HttpService);
|
|
1507
|
+
this.http = inject(HttpClient);
|
|
1508
|
+
this.messageService = inject(MessageService);
|
|
1509
|
+
this.accountID = null;
|
|
1510
|
+
this.statementFrom = '';
|
|
1511
|
+
this.statementTo = '';
|
|
1512
|
+
this.closingBalance = 0;
|
|
1513
|
+
this.csv = '';
|
|
1514
|
+
this.parseError = '';
|
|
1515
|
+
this.accounts = [];
|
|
1516
|
+
// Changed: materialized — parsedLines/netMovement were getters read 4x per change-detection cycle
|
|
1517
|
+
// (twice in the template, plus netMovement and canSave), each call re-parsing the whole CSV, allocating a
|
|
1518
|
+
// new array AND writing this.parseError from inside a getter (a side effect during change detection).
|
|
1519
|
+
this.parsedLines = [];
|
|
1520
|
+
this.netMovement = 0;
|
|
1521
|
+
}
|
|
1522
|
+
ngOnInit() {
|
|
1523
|
+
this.http.get(`${this.httpService.apiUrl}accounts/list/x`)
|
|
1524
|
+
.subscribe(res => this.accounts = res?.data ?? []);
|
|
1525
|
+
const now = new Date();
|
|
1526
|
+
this.statementFrom = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().substring(0, 10);
|
|
1527
|
+
this.statementTo = new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString().substring(0, 10);
|
|
1528
|
+
}
|
|
1529
|
+
// Added: recomputed only when the CSV text actually changes (bound via (ngModelChange))
|
|
1530
|
+
parseCsv() {
|
|
1531
|
+
this.parseError = '';
|
|
1532
|
+
const lines = [];
|
|
1533
|
+
for (const raw of this.csv.split('\n').map(l => l.trim()).filter(l => l)) {
|
|
1534
|
+
const parts = raw.split(',').map(p => p.trim());
|
|
1535
|
+
if (parts.length < 2) {
|
|
1536
|
+
this.parseError = `Cannot parse: "${raw}"`;
|
|
1537
|
+
this.parsedLines = [];
|
|
1538
|
+
this.netMovement = 0;
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
const amount = parseFloat(parts[parts.length - 1]);
|
|
1542
|
+
if (isNaN(amount)) {
|
|
1543
|
+
this.parseError = `Invalid amount in: "${raw}"`;
|
|
1544
|
+
this.parsedLines = [];
|
|
1545
|
+
this.netMovement = 0;
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
lines.push({ date: parts[0], description: parts[1] ?? '', reference: parts.length > 3 ? parts[2] : '', amount });
|
|
1549
|
+
}
|
|
1550
|
+
this.parsedLines = lines;
|
|
1551
|
+
this.netMovement = lines.reduce((sum, line) => sum + line.amount, 0);
|
|
1552
|
+
}
|
|
1553
|
+
get canSave() { return !!this.accountID && this.parsedLines.length > 0 && !this.parseError; } // Returns a primitive — safe to leave as a getter
|
|
1554
|
+
save() {
|
|
1555
|
+
const dto = {
|
|
1556
|
+
accountID: this.accountID,
|
|
1557
|
+
statementFrom: this.statementFrom,
|
|
1558
|
+
statementTo: this.statementTo,
|
|
1559
|
+
closingBalance: this.closingBalance,
|
|
1560
|
+
lines: this.parsedLines
|
|
1561
|
+
};
|
|
1562
|
+
this.http.post(`${this.httpService.apiUrl}bankstatementimports/dto?action=create`, dto)
|
|
1563
|
+
.subscribe(res => {
|
|
1564
|
+
this.messageService.toast(res?.message ?? (res?.success ? 'Statement imported' : 'Import failed'));
|
|
1565
|
+
if (res?.success)
|
|
1566
|
+
this.dialogRef.close(true);
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: StatementImportDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1570
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: StatementImportDialogComponent, isStandalone: false, selector: "spa-statement-import-dialog", ngImport: i0, template: `
|
|
1571
|
+
<h2 mat-dialog-title>Import Bank Statement</h2>
|
|
1572
|
+
<mat-dialog-content>
|
|
1573
|
+
<div style="display: flex; gap: 16px; flex-wrap: wrap;">
|
|
1574
|
+
<mat-form-field style="min-width: 220px;">
|
|
1575
|
+
<mat-label>Account</mat-label>
|
|
1576
|
+
<mat-select [(ngModel)]="accountID">
|
|
1577
|
+
<mat-option *ngFor="let option of accounts" [value]="option.value">{{ option.name }}</mat-option>
|
|
1578
|
+
</mat-select>
|
|
1579
|
+
</mat-form-field>
|
|
1580
|
+
<mat-form-field>
|
|
1581
|
+
<mat-label>From</mat-label>
|
|
1582
|
+
<input matInput type="date" [(ngModel)]="statementFrom">
|
|
1583
|
+
</mat-form-field>
|
|
1584
|
+
<mat-form-field>
|
|
1585
|
+
<mat-label>To</mat-label>
|
|
1586
|
+
<input matInput type="date" [(ngModel)]="statementTo">
|
|
1587
|
+
</mat-form-field>
|
|
1588
|
+
<mat-form-field>
|
|
1589
|
+
<mat-label>Closing Balance</mat-label>
|
|
1590
|
+
<input matInput type="number" [(ngModel)]="closingBalance">
|
|
1591
|
+
</mat-form-field>
|
|
1592
|
+
</div>
|
|
1593
|
+
<mat-form-field style="width: 100%;">
|
|
1594
|
+
<mat-label>Statement lines (CSV: date, description, reference, amount — money out negative)</mat-label>
|
|
1595
|
+
<textarea matInput rows="10" [(ngModel)]="csv" (ngModelChange)="parseCsv()" placeholder="2026-07-01, Deposit ABC, REF001, 1500.00 2026-07-03, Bank charges, , -25.50"></textarea>
|
|
1596
|
+
</mat-form-field>
|
|
1597
|
+
<p *ngIf="parseError" style="color: red;"><em>{{ parseError }}</em></p>
|
|
1598
|
+
<p *ngIf="parsedLines.length"><em>{{ parsedLines.length }} line(s) parsed, net movement {{ netMovement | number:'1.2-2' }}</em></p>
|
|
1599
|
+
</mat-dialog-content>
|
|
1600
|
+
<mat-dialog-actions align="end">
|
|
1601
|
+
<button mat-button (click)="dialogRef.close(false)">Cancel</button>
|
|
1602
|
+
<button mat-flat-button color="primary" (click)="save()" [disabled]="!canSave">Import</button>
|
|
1603
|
+
</mat-dialog-actions>
|
|
1604
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.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.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.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: i4$1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i4$1.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i4$1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "component", type: i3$3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$3.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: i7$1.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$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }] }); }
|
|
1605
|
+
}
|
|
1606
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: StatementImportDialogComponent, decorators: [{
|
|
1607
|
+
type: Component,
|
|
1608
|
+
args: [{
|
|
1609
|
+
selector: 'spa-statement-import-dialog',
|
|
1610
|
+
template: `
|
|
1611
|
+
<h2 mat-dialog-title>Import Bank Statement</h2>
|
|
1612
|
+
<mat-dialog-content>
|
|
1613
|
+
<div style="display: flex; gap: 16px; flex-wrap: wrap;">
|
|
1614
|
+
<mat-form-field style="min-width: 220px;">
|
|
1615
|
+
<mat-label>Account</mat-label>
|
|
1616
|
+
<mat-select [(ngModel)]="accountID">
|
|
1617
|
+
<mat-option *ngFor="let option of accounts" [value]="option.value">{{ option.name }}</mat-option>
|
|
1618
|
+
</mat-select>
|
|
1619
|
+
</mat-form-field>
|
|
1620
|
+
<mat-form-field>
|
|
1621
|
+
<mat-label>From</mat-label>
|
|
1622
|
+
<input matInput type="date" [(ngModel)]="statementFrom">
|
|
1623
|
+
</mat-form-field>
|
|
1624
|
+
<mat-form-field>
|
|
1625
|
+
<mat-label>To</mat-label>
|
|
1626
|
+
<input matInput type="date" [(ngModel)]="statementTo">
|
|
1627
|
+
</mat-form-field>
|
|
1628
|
+
<mat-form-field>
|
|
1629
|
+
<mat-label>Closing Balance</mat-label>
|
|
1630
|
+
<input matInput type="number" [(ngModel)]="closingBalance">
|
|
1631
|
+
</mat-form-field>
|
|
1632
|
+
</div>
|
|
1633
|
+
<mat-form-field style="width: 100%;">
|
|
1634
|
+
<mat-label>Statement lines (CSV: date, description, reference, amount — money out negative)</mat-label>
|
|
1635
|
+
<textarea matInput rows="10" [(ngModel)]="csv" (ngModelChange)="parseCsv()" placeholder="2026-07-01, Deposit ABC, REF001, 1500.00 2026-07-03, Bank charges, , -25.50"></textarea>
|
|
1636
|
+
</mat-form-field>
|
|
1637
|
+
<p *ngIf="parseError" style="color: red;"><em>{{ parseError }}</em></p>
|
|
1638
|
+
<p *ngIf="parsedLines.length"><em>{{ parsedLines.length }} line(s) parsed, net movement {{ netMovement | number:'1.2-2' }}</em></p>
|
|
1639
|
+
</mat-dialog-content>
|
|
1640
|
+
<mat-dialog-actions align="end">
|
|
1641
|
+
<button mat-button (click)="dialogRef.close(false)">Cancel</button>
|
|
1642
|
+
<button mat-flat-button color="primary" (click)="save()" [disabled]="!canSave">Import</button>
|
|
1643
|
+
</mat-dialog-actions>
|
|
1644
|
+
`,
|
|
1645
|
+
standalone: false
|
|
1646
|
+
}]
|
|
1647
|
+
}] });
|
|
1648
|
+
|
|
1649
|
+
// Bank reconciliation match screen (C4) — statement lines vs unreconciled book entries; auto/manual match, unmatch, complete
|
|
1650
|
+
class ReconcileDialogComponent {
|
|
1651
|
+
constructor() {
|
|
1652
|
+
this.dialogRef = inject((MatDialogRef));
|
|
1653
|
+
this.data = inject(MAT_DIALOG_DATA);
|
|
1654
|
+
this.httpService = inject(HttpService);
|
|
1655
|
+
this.http = inject(HttpClient);
|
|
1656
|
+
this.messageService = inject(MessageService);
|
|
1657
|
+
this.rec = null;
|
|
1658
|
+
this.selectedLine = null;
|
|
1659
|
+
this.selectedEntry = null;
|
|
1660
|
+
this.changed = false;
|
|
1661
|
+
}
|
|
1662
|
+
get difference() { return (this.rec?.bookBalance ?? 0) - (this.rec?.statementBalance ?? 0); }
|
|
1663
|
+
ngOnInit() { this.load(); }
|
|
1664
|
+
load() {
|
|
1665
|
+
this.selectedLine = null;
|
|
1666
|
+
this.selectedEntry = null;
|
|
1667
|
+
this.http.get(`${this.httpService.apiUrl}bankstatementimports/reconciliation/${this.data.bankStatementImportID}`)
|
|
1668
|
+
.subscribe(res => this.rec = res?.data ?? null);
|
|
1669
|
+
}
|
|
1670
|
+
autoMatch() {
|
|
1671
|
+
this.http.post(`${this.httpService.apiUrl}bankstatementimports/dto?action=auto-match`, { bankStatementImportID: this.data.bankStatementImportID })
|
|
1672
|
+
.subscribe(res => { this.messageService.toast(res?.message ?? ''); this.changed = true; this.load(); });
|
|
1673
|
+
}
|
|
1674
|
+
match() {
|
|
1675
|
+
this.http.post(`${this.httpService.apiUrl}bankstatementimports/match?action=match`, { bankStatementLineID: this.selectedLine.bankStatementLineID, transactionEntryID: this.selectedEntry.transactionEntryID })
|
|
1676
|
+
.subscribe(res => { this.messageService.toast(res?.message ?? ''); this.changed = true; this.load(); });
|
|
1677
|
+
}
|
|
1678
|
+
unmatch(line) {
|
|
1679
|
+
this.http.post(`${this.httpService.apiUrl}bankstatementimports/match?action=unmatch`, { bankStatementLineID: line.bankStatementLineID, transactionEntryID: 0 })
|
|
1680
|
+
.subscribe(res => { this.messageService.toast(res?.message ?? ''); this.changed = true; this.load(); });
|
|
1681
|
+
}
|
|
1682
|
+
complete() {
|
|
1683
|
+
this.http.post(`${this.httpService.apiUrl}bankstatementimports/dto?action=complete`, { bankStatementImportID: this.data.bankStatementImportID })
|
|
1684
|
+
.subscribe(res => {
|
|
1685
|
+
this.messageService.toast(res?.message ?? '');
|
|
1686
|
+
if (res?.success)
|
|
1687
|
+
this.dialogRef.close(true);
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ReconcileDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1691
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: ReconcileDialogComponent, isStandalone: false, selector: "spa-reconcile-dialog", ngImport: i0, template: `
|
|
1692
|
+
<h2 mat-dialog-title>Reconcile: {{ rec?.name }}</h2>
|
|
1693
|
+
<mat-dialog-content>
|
|
1694
|
+
<div *ngIf="rec" style="display: flex; gap: 32px; flex-wrap: wrap; margin-bottom: 12px;">
|
|
1695
|
+
<div><small>Book Balance</small><br><b>{{ rec.bookBalance | number:'1.2-2' }}</b></div>
|
|
1696
|
+
<div><small>Statement Balance</small><br><b>{{ rec.statementBalance | number:'1.2-2' }}</b></div>
|
|
1697
|
+
<div><small>Difference</small><br><b [style.color]="difference ? 'red' : 'green'">{{ difference | number:'1.2-2' }}</b></div>
|
|
1698
|
+
<div><small>Matched</small><br><b>{{ rec.matchedCount }} / {{ rec.lineCount }}</b></div>
|
|
1699
|
+
</div>
|
|
1700
|
+
|
|
1701
|
+
<div style="display: flex; gap: 24px; align-items: flex-start; flex-wrap: wrap;" *ngIf="rec">
|
|
1702
|
+
<div style="flex: 1; min-width: 380px;">
|
|
1703
|
+
<h4>Statement Lines</h4>
|
|
1704
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
1705
|
+
<tr style="text-align: left; border-bottom: 1px solid #ccc;">
|
|
1706
|
+
<th style="padding: 4px;"></th><th style="padding: 4px;">Date</th><th style="padding: 4px;">Description</th>
|
|
1707
|
+
<th style="padding: 4px; text-align: right;">Amount</th><th style="padding: 4px;">Status</th>
|
|
1708
|
+
</tr>
|
|
1709
|
+
<tr *ngFor="let line of rec.lines" style="border-bottom: 1px solid #eee;"
|
|
1710
|
+
[style.background]="selectedLine === line ? '#E3F2FD' : line.isMatched ? '#F1F8E9' : ''">
|
|
1711
|
+
<td style="padding: 4px;">
|
|
1712
|
+
<mat-checkbox *ngIf="!line.isMatched" [checked]="selectedLine === line" (change)="selectedLine = $event.checked ? line : null"></mat-checkbox>
|
|
1713
|
+
</td>
|
|
1714
|
+
<td style="padding: 4px;">{{ line.date }}</td>
|
|
1715
|
+
<td style="padding: 4px;">{{ line.description }}</td>
|
|
1716
|
+
<td style="padding: 4px; text-align: right;">{{ line.amount | number:'1.2-2' }}</td>
|
|
1717
|
+
<td style="padding: 4px;">
|
|
1718
|
+
<span *ngIf="line.isMatched" title="{{ line.matchedDescription }}">✔</span>
|
|
1719
|
+
<button mat-button *ngIf="line.isMatched" (click)="unmatch(line)">Unmatch</button>
|
|
1720
|
+
</td>
|
|
1721
|
+
</tr>
|
|
1722
|
+
</table>
|
|
1723
|
+
</div>
|
|
1724
|
+
|
|
1725
|
+
<div style="flex: 1; min-width: 380px;">
|
|
1726
|
+
<h4>Unreconciled Book Entries</h4>
|
|
1727
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
1728
|
+
<tr style="text-align: left; border-bottom: 1px solid #ccc;">
|
|
1729
|
+
<th style="padding: 4px;"></th><th style="padding: 4px;">Date</th><th style="padding: 4px;">Description</th>
|
|
1730
|
+
<th style="padding: 4px; text-align: right;">Amount</th>
|
|
1731
|
+
</tr>
|
|
1732
|
+
<tr *ngFor="let entry of rec.unreconciledEntries" style="border-bottom: 1px solid #eee;"
|
|
1733
|
+
[style.background]="selectedEntry === entry ? '#E3F2FD' : ''">
|
|
1734
|
+
<td style="padding: 4px;">
|
|
1735
|
+
<mat-checkbox [checked]="selectedEntry === entry" (change)="selectedEntry = $event.checked ? entry : null"></mat-checkbox>
|
|
1736
|
+
</td>
|
|
1737
|
+
<td style="padding: 4px;">{{ entry.date }}</td>
|
|
1738
|
+
<td style="padding: 4px;">{{ entry.description }}</td>
|
|
1739
|
+
<td style="padding: 4px; text-align: right;">{{ entry.amount | number:'1.2-2' }}</td>
|
|
1740
|
+
</tr>
|
|
1741
|
+
</table>
|
|
1742
|
+
</div>
|
|
1743
|
+
</div>
|
|
1744
|
+
</mat-dialog-content>
|
|
1745
|
+
<mat-dialog-actions align="end">
|
|
1746
|
+
<button mat-button (click)="dialogRef.close(changed)">Close</button>
|
|
1747
|
+
<button mat-stroked-button (click)="autoMatch()">Auto Match</button>
|
|
1748
|
+
<button mat-stroked-button (click)="match()" [disabled]="!selectedLine || !selectedEntry">Match Selected</button>
|
|
1749
|
+
<button mat-flat-button color="primary" (click)="complete()" [disabled]="!rec || rec.matchedCount != rec.lineCount">Complete Reconciliation</button>
|
|
1750
|
+
</mat-dialog-actions>
|
|
1751
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i4$2.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: i4$1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i4$1.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i4$1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }] }); }
|
|
1752
|
+
}
|
|
1753
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ReconcileDialogComponent, decorators: [{
|
|
1754
|
+
type: Component,
|
|
1755
|
+
args: [{
|
|
1756
|
+
selector: 'spa-reconcile-dialog',
|
|
1757
|
+
template: `
|
|
1758
|
+
<h2 mat-dialog-title>Reconcile: {{ rec?.name }}</h2>
|
|
1759
|
+
<mat-dialog-content>
|
|
1760
|
+
<div *ngIf="rec" style="display: flex; gap: 32px; flex-wrap: wrap; margin-bottom: 12px;">
|
|
1761
|
+
<div><small>Book Balance</small><br><b>{{ rec.bookBalance | number:'1.2-2' }}</b></div>
|
|
1762
|
+
<div><small>Statement Balance</small><br><b>{{ rec.statementBalance | number:'1.2-2' }}</b></div>
|
|
1763
|
+
<div><small>Difference</small><br><b [style.color]="difference ? 'red' : 'green'">{{ difference | number:'1.2-2' }}</b></div>
|
|
1764
|
+
<div><small>Matched</small><br><b>{{ rec.matchedCount }} / {{ rec.lineCount }}</b></div>
|
|
1765
|
+
</div>
|
|
1766
|
+
|
|
1767
|
+
<div style="display: flex; gap: 24px; align-items: flex-start; flex-wrap: wrap;" *ngIf="rec">
|
|
1768
|
+
<div style="flex: 1; min-width: 380px;">
|
|
1769
|
+
<h4>Statement Lines</h4>
|
|
1770
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
1771
|
+
<tr style="text-align: left; border-bottom: 1px solid #ccc;">
|
|
1772
|
+
<th style="padding: 4px;"></th><th style="padding: 4px;">Date</th><th style="padding: 4px;">Description</th>
|
|
1773
|
+
<th style="padding: 4px; text-align: right;">Amount</th><th style="padding: 4px;">Status</th>
|
|
1774
|
+
</tr>
|
|
1775
|
+
<tr *ngFor="let line of rec.lines" style="border-bottom: 1px solid #eee;"
|
|
1776
|
+
[style.background]="selectedLine === line ? '#E3F2FD' : line.isMatched ? '#F1F8E9' : ''">
|
|
1777
|
+
<td style="padding: 4px;">
|
|
1778
|
+
<mat-checkbox *ngIf="!line.isMatched" [checked]="selectedLine === line" (change)="selectedLine = $event.checked ? line : null"></mat-checkbox>
|
|
1779
|
+
</td>
|
|
1780
|
+
<td style="padding: 4px;">{{ line.date }}</td>
|
|
1781
|
+
<td style="padding: 4px;">{{ line.description }}</td>
|
|
1782
|
+
<td style="padding: 4px; text-align: right;">{{ line.amount | number:'1.2-2' }}</td>
|
|
1783
|
+
<td style="padding: 4px;">
|
|
1784
|
+
<span *ngIf="line.isMatched" title="{{ line.matchedDescription }}">✔</span>
|
|
1785
|
+
<button mat-button *ngIf="line.isMatched" (click)="unmatch(line)">Unmatch</button>
|
|
1786
|
+
</td>
|
|
1787
|
+
</tr>
|
|
1788
|
+
</table>
|
|
1789
|
+
</div>
|
|
1790
|
+
|
|
1791
|
+
<div style="flex: 1; min-width: 380px;">
|
|
1792
|
+
<h4>Unreconciled Book Entries</h4>
|
|
1793
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
1794
|
+
<tr style="text-align: left; border-bottom: 1px solid #ccc;">
|
|
1795
|
+
<th style="padding: 4px;"></th><th style="padding: 4px;">Date</th><th style="padding: 4px;">Description</th>
|
|
1796
|
+
<th style="padding: 4px; text-align: right;">Amount</th>
|
|
1797
|
+
</tr>
|
|
1798
|
+
<tr *ngFor="let entry of rec.unreconciledEntries" style="border-bottom: 1px solid #eee;"
|
|
1799
|
+
[style.background]="selectedEntry === entry ? '#E3F2FD' : ''">
|
|
1800
|
+
<td style="padding: 4px;">
|
|
1801
|
+
<mat-checkbox [checked]="selectedEntry === entry" (change)="selectedEntry = $event.checked ? entry : null"></mat-checkbox>
|
|
1802
|
+
</td>
|
|
1803
|
+
<td style="padding: 4px;">{{ entry.date }}</td>
|
|
1804
|
+
<td style="padding: 4px;">{{ entry.description }}</td>
|
|
1805
|
+
<td style="padding: 4px; text-align: right;">{{ entry.amount | number:'1.2-2' }}</td>
|
|
1806
|
+
</tr>
|
|
1807
|
+
</table>
|
|
1808
|
+
</div>
|
|
1809
|
+
</div>
|
|
1810
|
+
</mat-dialog-content>
|
|
1811
|
+
<mat-dialog-actions align="end">
|
|
1812
|
+
<button mat-button (click)="dialogRef.close(changed)">Close</button>
|
|
1813
|
+
<button mat-stroked-button (click)="autoMatch()">Auto Match</button>
|
|
1814
|
+
<button mat-stroked-button (click)="match()" [disabled]="!selectedLine || !selectedEntry">Match Selected</button>
|
|
1815
|
+
<button mat-flat-button color="primary" (click)="complete()" [disabled]="!rec || rec.matchedCount != rec.lineCount">Complete Reconciliation</button>
|
|
1816
|
+
</mat-dialog-actions>
|
|
1817
|
+
`,
|
|
1818
|
+
standalone: false
|
|
1819
|
+
}]
|
|
1820
|
+
}] });
|
|
1821
|
+
|
|
1822
|
+
// Bank Reconciliation page (C4) — statement imports list + import/reconcile dialogs
|
|
1823
|
+
class BankReconciliationComponent {
|
|
1824
|
+
constructor() {
|
|
1825
|
+
this.accountingService = inject(AccountingService);
|
|
1826
|
+
this.dialog = inject(MatDialog);
|
|
1827
|
+
this.dataService = inject(DataServiceLib); // Added: supplies dialogPanelClass so these dialogs honour appConfig.dialogStyle
|
|
1828
|
+
this.pageConfig = {
|
|
1829
|
+
title: 'Bank Reconciliation',
|
|
1830
|
+
tableConfig: this.accountingService.bankStatementImportsTableConfig
|
|
1831
|
+
};
|
|
1832
|
+
// Reconcile row button opens the match screen (configs live in the service, dialogs in the component)
|
|
1833
|
+
this.accountingService.bankReconcileButton.onClick = (row) => this.dialog.open(ReconcileDialogComponent, { width: '1100px', maxWidth: '95vw', data: row, panelClass: this.dataService?.dialogPanelClass ?? [] }) // Changed: was missing panelClass, so this dialog never picked up the modern surface
|
|
1834
|
+
.afterClosed().subscribe(changed => { if (changed)
|
|
1835
|
+
this.page?.tableReload.next(true); });
|
|
1836
|
+
}
|
|
1837
|
+
ngOnInit() {
|
|
1838
|
+
}
|
|
1839
|
+
newImport() {
|
|
1840
|
+
this.dialog.open(StatementImportDialogComponent, { width: '860px', maxWidth: '95vw', panelClass: this.dataService?.dialogPanelClass ?? [] }) // Changed: was missing panelClass, so this dialog never picked up the modern surface
|
|
1841
|
+
.afterClosed().subscribe(saved => { if (saved)
|
|
1842
|
+
this.page?.tableReload.next(true); });
|
|
1843
|
+
}
|
|
1844
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BankReconciliationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1845
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: BankReconciliationComponent, isStandalone: false, selector: "spa-bank-reconciliation", viewQueries: [{ propertyName: "page", first: true, predicate: PageComponent, descendants: true }], ngImport: i0, template: `
|
|
1846
|
+
<div style="display: flex; justify-content: flex-end;">
|
|
1847
|
+
<button mat-stroked-button (click)="newImport()">
|
|
1848
|
+
<mat-icon>upload_file</mat-icon> Import Statement
|
|
1849
|
+
</button>
|
|
1850
|
+
</div>
|
|
1851
|
+
<spa-page [config]="pageConfig"></spa-page>
|
|
1852
|
+
`, isInline: true, dependencies: [{ kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: PageComponent, selector: "spa-page", inputs: ["config"], outputs: ["searchModeActivated", "searchModeDeactivated", "refreshClick", "actionClick", "actionResponse", "inputChange", "createClick", "searchClick", "dataLoad", "titleActionChange"] }] }); }
|
|
1853
|
+
}
|
|
1854
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BankReconciliationComponent, decorators: [{
|
|
1855
|
+
type: Component,
|
|
1856
|
+
args: [{
|
|
1857
|
+
selector: 'spa-bank-reconciliation',
|
|
1858
|
+
template: `
|
|
1859
|
+
<div style="display: flex; justify-content: flex-end;">
|
|
1860
|
+
<button mat-stroked-button (click)="newImport()">
|
|
1861
|
+
<mat-icon>upload_file</mat-icon> Import Statement
|
|
1862
|
+
</button>
|
|
1863
|
+
</div>
|
|
1864
|
+
<spa-page [config]="pageConfig"></spa-page>
|
|
1865
|
+
`,
|
|
1866
|
+
standalone: false
|
|
1867
|
+
}]
|
|
1868
|
+
}], ctorParameters: () => [], propDecorators: { page: [{
|
|
1869
|
+
type: ViewChild,
|
|
1870
|
+
args: [PageComponent]
|
|
1871
|
+
}] } });
|
|
1872
|
+
|
|
1873
|
+
// Moved (N11) from grip-spa into tin-spa, under Accounting (owner directive 2026-08-17). The design notes
|
|
1874
|
+
// from the Grip original hold: the two tables are one story, so an action on the arrangements table
|
|
1875
|
+
// refreshes the transfers list beneath it via the service's shared Subject.
|
|
1876
|
+
class TenantTransfersComponent {
|
|
1877
|
+
constructor(tenantTransferService) {
|
|
1878
|
+
this.tenantTransferService = tenantTransferService;
|
|
1879
|
+
}
|
|
1880
|
+
// Any successful action on an arrangement can change the transfers list — sending money adds a row, and
|
|
1881
|
+
// revoking is worth re-reading the list after too. Refreshing unconditionally is correct and cheap.
|
|
1882
|
+
arrangementActioned() {
|
|
1883
|
+
this.tenantTransferService.transfersReload.next(true);
|
|
1884
|
+
}
|
|
1885
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TenantTransfersComponent, deps: [{ token: TenantTransferService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1886
|
+
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{max-width:100%;min-width:0}.tenant-transfers .tt-lead{max-width:74ch;margin:0 0 20px;line-height:1.55;opacity:.85;overflow-wrap:break-word}.tenant-transfers spa-table{display:block;max-width:100%;min-width:0;overflow-x:auto}.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"] }] }); }
|
|
1887
|
+
}
|
|
1888
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TenantTransfersComponent, decorators: [{
|
|
1889
|
+
type: Component,
|
|
1890
|
+
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{max-width:100%;min-width:0}.tenant-transfers .tt-lead{max-width:74ch;margin:0 0 20px;line-height:1.55;opacity:.85;overflow-wrap:break-word}.tenant-transfers spa-table{display:block;max-width:100%;min-width:0;overflow-x:auto}.tenant-transfers .tt-gap{height:28px}\n"] }]
|
|
1891
|
+
}], ctorParameters: () => [{ type: TenantTransferService }] });
|
|
1892
|
+
|
|
1893
|
+
// New revenue schedule dialog (C7) — defer a submitted invoice's net revenue over a date range
|
|
1894
|
+
class RevenueScheduleDialogComponent {
|
|
1895
|
+
constructor() {
|
|
1896
|
+
this.dialogRef = inject((MatDialogRef));
|
|
1897
|
+
this.httpService = inject(HttpService);
|
|
1898
|
+
this.http = inject(HttpClient);
|
|
1899
|
+
this.messageService = inject(MessageService);
|
|
1900
|
+
this.invoiceID = null;
|
|
1901
|
+
this.startDate = '';
|
|
1902
|
+
this.endDate = '';
|
|
1903
|
+
this.invoices = [];
|
|
1904
|
+
}
|
|
1905
|
+
ngOnInit() {
|
|
1906
|
+
this.http.get(`${this.httpService.apiUrl}revenueschedules/eligible-invoices/x`)
|
|
1907
|
+
.subscribe(res => this.invoices = res?.data ?? []);
|
|
1908
|
+
}
|
|
1909
|
+
save() {
|
|
1910
|
+
const dto = { invoiceID: this.invoiceID, startDate: this.startDate, endDate: this.endDate };
|
|
1911
|
+
this.http.post(`${this.httpService.apiUrl}revenueschedules/dto?action=create`, dto)
|
|
1912
|
+
.subscribe(res => {
|
|
1913
|
+
this.messageService.toast(res?.message ?? (res?.success ? 'Schedule created' : 'Failed'));
|
|
1914
|
+
if (res?.success)
|
|
1915
|
+
this.dialogRef.close(true);
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: RevenueScheduleDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1919
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: RevenueScheduleDialogComponent, isStandalone: false, selector: "spa-revenue-schedule-dialog", ngImport: i0, template: `
|
|
1920
|
+
<h2 mat-dialog-title>Defer Invoice Revenue</h2>
|
|
1921
|
+
<mat-dialog-content>
|
|
1922
|
+
<div style="display: flex; gap: 16px; flex-wrap: wrap;">
|
|
1923
|
+
<mat-form-field style="min-width: 280px;">
|
|
1924
|
+
<mat-label>Invoice</mat-label>
|
|
1925
|
+
<mat-select [(ngModel)]="invoiceID">
|
|
1926
|
+
<mat-option *ngFor="let option of invoices" [value]="option.value">{{ option.name }}</mat-option>
|
|
1927
|
+
</mat-select>
|
|
1928
|
+
</mat-form-field>
|
|
1929
|
+
<mat-form-field>
|
|
1930
|
+
<mat-label>Service Period Start</mat-label>
|
|
1931
|
+
<input matInput type="date" [(ngModel)]="startDate">
|
|
1932
|
+
</mat-form-field>
|
|
1933
|
+
<mat-form-field>
|
|
1934
|
+
<mat-label>Service Period End</mat-label>
|
|
1935
|
+
<input matInput type="date" [(ngModel)]="endDate">
|
|
1936
|
+
</mat-form-field>
|
|
1937
|
+
</div>
|
|
1938
|
+
<p><em>The invoice's net revenue (excl. VAT) moves to Deferred Revenue and is recognized monthly, pro-rata by days, as each period ends.</em></p>
|
|
1939
|
+
</mat-dialog-content>
|
|
1940
|
+
<mat-dialog-actions align="end">
|
|
1941
|
+
<button mat-button (click)="dialogRef.close(false)">Cancel</button>
|
|
1942
|
+
<button mat-flat-button color="primary" (click)="save()" [disabled]="!invoiceID || !startDate || !endDate || endDate <= startDate">Defer Revenue</button>
|
|
1943
|
+
</mat-dialog-actions>
|
|
1944
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { 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: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: i4$1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i4$1.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i4$1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "component", type: i3$3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$3.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: i7$1.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$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }] }); }
|
|
1945
|
+
}
|
|
1946
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: RevenueScheduleDialogComponent, decorators: [{
|
|
1947
|
+
type: Component,
|
|
1948
|
+
args: [{
|
|
1949
|
+
selector: 'spa-revenue-schedule-dialog',
|
|
1950
|
+
template: `
|
|
1951
|
+
<h2 mat-dialog-title>Defer Invoice Revenue</h2>
|
|
1952
|
+
<mat-dialog-content>
|
|
1953
|
+
<div style="display: flex; gap: 16px; flex-wrap: wrap;">
|
|
1954
|
+
<mat-form-field style="min-width: 280px;">
|
|
1955
|
+
<mat-label>Invoice</mat-label>
|
|
1956
|
+
<mat-select [(ngModel)]="invoiceID">
|
|
1957
|
+
<mat-option *ngFor="let option of invoices" [value]="option.value">{{ option.name }}</mat-option>
|
|
1958
|
+
</mat-select>
|
|
1959
|
+
</mat-form-field>
|
|
1960
|
+
<mat-form-field>
|
|
1961
|
+
<mat-label>Service Period Start</mat-label>
|
|
1962
|
+
<input matInput type="date" [(ngModel)]="startDate">
|
|
1963
|
+
</mat-form-field>
|
|
1964
|
+
<mat-form-field>
|
|
1965
|
+
<mat-label>Service Period End</mat-label>
|
|
1966
|
+
<input matInput type="date" [(ngModel)]="endDate">
|
|
1967
|
+
</mat-form-field>
|
|
1968
|
+
</div>
|
|
1969
|
+
<p><em>The invoice's net revenue (excl. VAT) moves to Deferred Revenue and is recognized monthly, pro-rata by days, as each period ends.</em></p>
|
|
1970
|
+
</mat-dialog-content>
|
|
1971
|
+
<mat-dialog-actions align="end">
|
|
1972
|
+
<button mat-button (click)="dialogRef.close(false)">Cancel</button>
|
|
1973
|
+
<button mat-flat-button color="primary" (click)="save()" [disabled]="!invoiceID || !startDate || !endDate || endDate <= startDate">Defer Revenue</button>
|
|
1974
|
+
</mat-dialog-actions>
|
|
1975
|
+
`,
|
|
1976
|
+
standalone: false
|
|
1977
|
+
}]
|
|
1978
|
+
}] });
|
|
1979
|
+
|
|
1980
|
+
// Revenue Schedules page (C7) — deferred revenue schedules with recognize-due sweep and cancel
|
|
1981
|
+
class RevenueSchedulesComponent {
|
|
1982
|
+
constructor() {
|
|
1983
|
+
this.accountingService = inject(AccountingService);
|
|
1984
|
+
this.dataService = inject(DataServiceLib); // Added: supplies dialogPanelClass so this dialog honours appConfig.dialogStyle
|
|
1985
|
+
this.dialog = inject(MatDialog);
|
|
1986
|
+
this.pageConfig = {
|
|
1987
|
+
title: 'Revenue Schedules',
|
|
1988
|
+
tableConfig: this.accountingService.revenueSchedulesTableConfig
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
ngOnInit() {
|
|
1992
|
+
}
|
|
1993
|
+
newSchedule() {
|
|
1994
|
+
this.dialog.open(RevenueScheduleDialogComponent, { width: '760px', maxWidth: '95vw', panelClass: this.dataService?.dialogPanelClass ?? [] }) // Changed: was missing panelClass, so this dialog never picked up the modern surface
|
|
1995
|
+
.afterClosed().subscribe(saved => { if (saved)
|
|
1996
|
+
this.page?.tableReload.next(true); });
|
|
1997
|
+
}
|
|
1998
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: RevenueSchedulesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1999
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: RevenueSchedulesComponent, isStandalone: false, selector: "spa-revenue-schedules", viewQueries: [{ propertyName: "page", first: true, predicate: PageComponent, descendants: true }], ngImport: i0, template: `
|
|
2000
|
+
<div style="display: flex; justify-content: flex-end;">
|
|
2001
|
+
<button mat-stroked-button (click)="newSchedule()">
|
|
2002
|
+
<mat-icon>schedule_send</mat-icon> Defer Invoice Revenue
|
|
2003
|
+
</button>
|
|
2004
|
+
</div>
|
|
2005
|
+
<spa-page [config]="pageConfig"></spa-page>
|
|
2006
|
+
`, isInline: true, dependencies: [{ kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: PageComponent, selector: "spa-page", inputs: ["config"], outputs: ["searchModeActivated", "searchModeDeactivated", "refreshClick", "actionClick", "actionResponse", "inputChange", "createClick", "searchClick", "dataLoad", "titleActionChange"] }] }); }
|
|
2007
|
+
}
|
|
2008
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: RevenueSchedulesComponent, decorators: [{
|
|
2009
|
+
type: Component,
|
|
2010
|
+
args: [{
|
|
2011
|
+
selector: 'spa-revenue-schedules',
|
|
2012
|
+
template: `
|
|
2013
|
+
<div style="display: flex; justify-content: flex-end;">
|
|
2014
|
+
<button mat-stroked-button (click)="newSchedule()">
|
|
2015
|
+
<mat-icon>schedule_send</mat-icon> Defer Invoice Revenue
|
|
2016
|
+
</button>
|
|
2017
|
+
</div>
|
|
2018
|
+
<spa-page [config]="pageConfig"></spa-page>
|
|
2019
|
+
`,
|
|
2020
|
+
standalone: false
|
|
2021
|
+
}]
|
|
2022
|
+
}], propDecorators: { page: [{
|
|
2023
|
+
type: ViewChild,
|
|
2024
|
+
args: [PageComponent]
|
|
2025
|
+
}] } });
|
|
2026
|
+
|
|
2027
|
+
// Quotations page (C10) — two-step create pattern with send/accept/reject/convert lifecycle + PDF download (C8)
|
|
2028
|
+
class QuotesComponent {
|
|
2029
|
+
constructor() {
|
|
2030
|
+
this.accountingService = inject(AccountingService);
|
|
2031
|
+
this.httpService = inject(HttpService);
|
|
2032
|
+
this.http = inject(HttpClient);
|
|
2033
|
+
this.pageConfig = {
|
|
2034
|
+
title: 'Quotations',
|
|
2035
|
+
tableConfig: this.accountingService.quotesTableConfig
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
ngOnInit() {
|
|
2039
|
+
}
|
|
2040
|
+
// PDF button emits actionClick; download the blob (mirrors invoice PDF flow)
|
|
2041
|
+
actionClicked(event) {
|
|
2042
|
+
if (event.name === 'pdf')
|
|
2043
|
+
this.download(event.data);
|
|
2044
|
+
}
|
|
2045
|
+
download(quote) {
|
|
2046
|
+
this.http.post(`${this.httpService.apiUrl}quotes/pdf?action=x`, quote, { responseType: 'blob' })
|
|
2047
|
+
.subscribe((response) => {
|
|
2048
|
+
const blob = new Blob([response], { type: 'application/pdf' });
|
|
2049
|
+
const url = window.URL.createObjectURL(blob);
|
|
2050
|
+
const link = document.createElement('a');
|
|
2051
|
+
link.href = url;
|
|
2052
|
+
link.download = `Quote_${quote.quoteID}.pdf`;
|
|
2053
|
+
link.click();
|
|
2054
|
+
window.URL.revokeObjectURL(url);
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: QuotesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2058
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: QuotesComponent, isStandalone: false, selector: "spa-quotes", ngImport: i0, template: '<spa-page [config]="pageConfig" (actionClick)="actionClicked($event)"></spa-page>', isInline: true, dependencies: [{ kind: "component", type: PageComponent, selector: "spa-page", inputs: ["config"], outputs: ["searchModeActivated", "searchModeDeactivated", "refreshClick", "actionClick", "actionResponse", "inputChange", "createClick", "searchClick", "dataLoad", "titleActionChange"] }] }); }
|
|
2059
|
+
}
|
|
2060
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: QuotesComponent, decorators: [{
|
|
2061
|
+
type: Component,
|
|
2062
|
+
args: [{
|
|
2063
|
+
selector: 'spa-quotes',
|
|
2064
|
+
template: '<spa-page [config]="pageConfig" (actionClick)="actionClicked($event)"></spa-page>',
|
|
2065
|
+
standalone: false
|
|
2066
|
+
}]
|
|
2067
|
+
}] });
|
|
2068
|
+
|
|
2069
|
+
// Quotation rate card admin page (quotations-suite) — the quotable services a quotation is priced from, with
|
|
2070
|
+
// their variant rates on a nested tab. Two-step create: the service header is created first, then the details
|
|
2071
|
+
// dialog opens on its Rates tab. Every config lives on AccountingService beside the quotations configs, so
|
|
2072
|
+
// this page is the same nine lines the Quotations page is.
|
|
2073
|
+
class RateCardComponent {
|
|
2074
|
+
constructor() {
|
|
2075
|
+
this.accountingService = inject(AccountingService);
|
|
2076
|
+
this.pageConfig = {
|
|
2077
|
+
title: 'Quotation Rate Card',
|
|
2078
|
+
tableConfig: this.accountingService.quotableServicesTableConfig
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: RateCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2082
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: RateCardComponent, isStandalone: false, selector: "spa-rate-card", ngImport: i0, template: '<spa-page [config]="pageConfig"></spa-page>', isInline: true, dependencies: [{ kind: "component", type: PageComponent, selector: "spa-page", inputs: ["config"], outputs: ["searchModeActivated", "searchModeDeactivated", "refreshClick", "actionClick", "actionResponse", "inputChange", "createClick", "searchClick", "dataLoad", "titleActionChange"] }] }); }
|
|
2083
|
+
}
|
|
2084
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: RateCardComponent, decorators: [{
|
|
2085
|
+
type: Component,
|
|
2086
|
+
args: [{
|
|
2087
|
+
selector: 'spa-rate-card',
|
|
2088
|
+
template: '<spa-page [config]="pageConfig"></spa-page>',
|
|
2089
|
+
standalone: false
|
|
2090
|
+
}]
|
|
2091
|
+
}] });
|
|
2092
|
+
|
|
2093
|
+
// Changed: Exported routes array for use by SpaHomeRoutingModule
|
|
2094
|
+
//
|
|
2095
|
+
// Added (F3): leaf-level data.moduleKey. INERT metadata — nothing in this library reads it and no guard is
|
|
2096
|
+
// attached here; a consumer opts in from its own home route. The path says "accounting" but three separate
|
|
2097
|
+
// ModuleCatalog modules have pages under it, so the leaves that do NOT belong to Accounting say so explicitly
|
|
2098
|
+
// (deepest data wins in the resolver, so a leaf overrides the area). Keys are transcribed from the CapItem
|
|
2099
|
+
// tree in datalib.service.ts — the same source the nav menu already gates on — not invented:
|
|
2100
|
+
// invoicing -> capInvoicing.capSubItems (datalib.service.ts:372) — dashboard, quotes, invoices,
|
|
2101
|
+
// credit notes, receipts, revenue schedules, aging, statements
|
|
2102
|
+
// purchasing -> capSupplierAging (:520) is a child of capPurchasingModule (:456) despite its accounting path
|
|
2103
|
+
// Everything left bare here is a capAccounting sub-item (:267) and correctly inherits the area's "accounting".
|
|
2104
|
+
const ACCOUNTING_ROUTES = [
|
|
2105
|
+
{ path: "accounts", component: AccountsComponent },
|
|
2106
|
+
{ path: "aggregates", component: AggregatesComponent },
|
|
2107
|
+
{ path: "transaction-types", component: TransactionTypesComponent },
|
|
2108
|
+
{ path: "transactions", component: TransactionsComponent },
|
|
2109
|
+
{ path: "invoices", component: InvoicesComponent, data: { moduleKey: 'invoicing' } }, // Added (F3): Invoicing, not Accounting
|
|
2110
|
+
{ path: "aging", component: AgingComponent, data: { moduleKey: 'invoicing', capName: 'cap58' } }, // Added (F3): capAging is a capInvoicing sub-item // Changed: added INERT capName — nothing in this library reads it; cap58 is capAging, whose menu link is exactly "home/accounting/aging" (datalib.service.ts:489,491). A consumer that attaches capabilityGuard() in its OWN route table refuses the deep link; an app that never adds that line is unaffected
|
|
2111
|
+
{ path: "supplier-aging", component: SupplierAgingComponent, data: { moduleKey: 'purchasing', capName: 'cap63' } }, // Added (F3): AP Aging belongs to Purchasing // Changed: added INERT capName — cap63 is capSupplierAging, menu link exactly "home/accounting/supplier-aging" (datalib.service.ts:597,599)
|
|
2112
|
+
{ path: "statements", component: StatementComponent, data: { moduleKey: 'invoicing' } }, // Added (F3): customer statement is an Invoicing page
|
|
2113
|
+
{ path: "reports", component: ReportsComponent },
|
|
2114
|
+
{ path: "tax-rates", component: TaxRatesComponent },
|
|
2115
|
+
{ path: "standing-orders", component: StandingOrdersComponent },
|
|
2116
|
+
{ path: "currencies", component: CurrenciesComponent }, // Changed: Removed Fixed Assets routes — moved to Assets module
|
|
2117
|
+
{ path: "budgets", component: BudgetsComponent },
|
|
2118
|
+
{ path: "budget-vs-actual", component: BudgetVsActualComponent },
|
|
2119
|
+
{ path: "dashboard", component: AccountingDashboardComponent },
|
|
2120
|
+
{ path: "invoice-dashboard", component: InvoiceDashboardComponent, data: { moduleKey: 'invoicing' } }, // Added (F3): Invoicing dashboard
|
|
2121
|
+
{ path: "credit-notes", component: CreditNotesComponent, data: { moduleKey: 'invoicing' } }, // Changed: Credit notes route (B6) // Added (F3)
|
|
2122
|
+
{ path: "vat-return", component: VatReturnComponent }, // Changed: VAT return route (B2)
|
|
2123
|
+
{ path: "fiscal-periods", component: FiscalPeriodsComponent }, // Changed: Fiscal periods route (C2)
|
|
2124
|
+
{ path: "receipts", component: ReceiptsComponent, data: { moduleKey: 'invoicing' } }, // Changed: Customer receipts route (C3) // Added (F3)
|
|
2125
|
+
{ path: "bank-reconciliation", component: BankReconciliationComponent }, // Changed: Bank reconciliation route (C4)
|
|
2126
|
+
{ path: "tenant-transfers", component: TenantTransfersComponent }, // Added (N11): Organisation Transfers
|
|
2127
|
+
{ path: "revenue-schedules", component: RevenueSchedulesComponent, data: { moduleKey: 'invoicing' } }, // Changed: Deferred revenue route (C7) // Added (F3)
|
|
2128
|
+
{ path: "quotes", component: QuotesComponent, data: { moduleKey: 'invoicing' } }, // Changed: Quotations route (C10) // Added (F3)
|
|
2129
|
+
{ path: "rate-card", component: RateCardComponent, data: { moduleKey: 'invoicing' } } // Added (quotations-suite): the rate card the guided quote builder prices from — same module key and same cap53 server gate as Quotations
|
|
2130
|
+
];
|
|
2131
|
+
class AccountingRoutingModule {
|
|
2132
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AccountingRoutingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
2133
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.14", ngImport: i0, type: AccountingRoutingModule, imports: [i1$2.RouterModule], exports: [RouterModule] }); }
|
|
2134
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AccountingRoutingModule, imports: [RouterModule.forChild(ACCOUNTING_ROUTES), RouterModule] }); }
|
|
2135
|
+
}
|
|
2136
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AccountingRoutingModule, decorators: [{
|
|
2137
|
+
type: NgModule,
|
|
2138
|
+
args: [{
|
|
2139
|
+
imports: [RouterModule.forChild(ACCOUNTING_ROUTES)],
|
|
2140
|
+
exports: [RouterModule]
|
|
2141
|
+
}]
|
|
2142
|
+
}] });
|
|
2143
|
+
|
|
2144
|
+
class AccountingModule {
|
|
2145
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AccountingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
2146
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.14", ngImport: i0, type: AccountingModule, declarations: [AccountsComponent,
|
|
2147
|
+
AggregatesComponent,
|
|
2148
|
+
AgingComponent,
|
|
2149
|
+
SupplierAgingComponent,
|
|
2150
|
+
TransactionTypesComponent,
|
|
2151
|
+
TransactionsComponent,
|
|
2152
|
+
InvoicesComponent,
|
|
2153
|
+
StatementComponent,
|
|
2154
|
+
ReportsComponent,
|
|
2155
|
+
BudgetsComponent,
|
|
2156
|
+
BudgetVsActualComponent,
|
|
2157
|
+
AccountingDashboardComponent,
|
|
2158
|
+
InvoiceDashboardComponent,
|
|
2159
|
+
CreditNotesComponent, // Changed: Credit notes page (B6)
|
|
2160
|
+
VatReturnComponent, // Changed: VAT return page (B2)
|
|
2161
|
+
FiscalPeriodsComponent, // Changed: Fiscal periods page (C2)
|
|
2162
|
+
ReceiptsComponent, // Changed: Customer receipts page (C3)
|
|
2163
|
+
BankReconciliationComponent, // Changed: Bank reconciliation page (C4)
|
|
2164
|
+
TenantTransfersComponent, // Added (N11): Organisation Transfers page
|
|
2165
|
+
StatementImportDialogComponent, // Changed: Statement import dialog (C4)
|
|
2166
|
+
ReconcileDialogComponent, // Changed: Reconcile match screen (C4)
|
|
2167
|
+
RevenueSchedulesComponent, // Changed: Deferred revenue page (C7)
|
|
2168
|
+
RevenueScheduleDialogComponent, // Changed: Defer revenue dialog (C7)
|
|
2169
|
+
QuotesComponent, // Changed: Quotations page (C10)
|
|
2170
|
+
RateCardComponent // Added (quotations-suite): quotation rate card admin page
|
|
2171
|
+
], imports: [AccountingRoutingModule,
|
|
2172
|
+
CommonModule,
|
|
2173
|
+
SpaAdminModule,
|
|
2174
|
+
// Changed: Standalone components must be imported, not declared
|
|
2175
|
+
TaxRatesComponent,
|
|
2176
|
+
StandingOrdersComponent,
|
|
2177
|
+
CurrenciesComponent // Changed: Removed Fixed Assets — moved to Assets module
|
|
2178
|
+
] }); }
|
|
2179
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AccountingModule, imports: [AccountingRoutingModule,
|
|
2180
|
+
CommonModule,
|
|
2181
|
+
SpaAdminModule,
|
|
2182
|
+
// Changed: Standalone components must be imported, not declared
|
|
2183
|
+
TaxRatesComponent,
|
|
2184
|
+
StandingOrdersComponent,
|
|
2185
|
+
CurrenciesComponent // Changed: Removed Fixed Assets — moved to Assets module
|
|
2186
|
+
] }); }
|
|
2187
|
+
}
|
|
2188
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AccountingModule, decorators: [{
|
|
2189
|
+
type: NgModule,
|
|
2190
|
+
args: [{
|
|
2191
|
+
declarations: [
|
|
2192
|
+
AccountsComponent,
|
|
2193
|
+
AggregatesComponent,
|
|
2194
|
+
AgingComponent,
|
|
2195
|
+
SupplierAgingComponent,
|
|
2196
|
+
TransactionTypesComponent,
|
|
2197
|
+
TransactionsComponent,
|
|
2198
|
+
InvoicesComponent,
|
|
2199
|
+
StatementComponent,
|
|
2200
|
+
ReportsComponent,
|
|
2201
|
+
BudgetsComponent,
|
|
2202
|
+
BudgetVsActualComponent,
|
|
2203
|
+
AccountingDashboardComponent,
|
|
2204
|
+
InvoiceDashboardComponent,
|
|
2205
|
+
CreditNotesComponent, // Changed: Credit notes page (B6)
|
|
2206
|
+
VatReturnComponent, // Changed: VAT return page (B2)
|
|
2207
|
+
FiscalPeriodsComponent, // Changed: Fiscal periods page (C2)
|
|
2208
|
+
ReceiptsComponent, // Changed: Customer receipts page (C3)
|
|
2209
|
+
BankReconciliationComponent, // Changed: Bank reconciliation page (C4)
|
|
2210
|
+
TenantTransfersComponent, // Added (N11): Organisation Transfers page
|
|
2211
|
+
StatementImportDialogComponent, // Changed: Statement import dialog (C4)
|
|
2212
|
+
ReconcileDialogComponent, // Changed: Reconcile match screen (C4)
|
|
2213
|
+
RevenueSchedulesComponent, // Changed: Deferred revenue page (C7)
|
|
2214
|
+
RevenueScheduleDialogComponent, // Changed: Defer revenue dialog (C7)
|
|
2215
|
+
QuotesComponent, // Changed: Quotations page (C10)
|
|
2216
|
+
RateCardComponent // Added (quotations-suite): quotation rate card admin page
|
|
2217
|
+
],
|
|
2218
|
+
imports: [
|
|
2219
|
+
AccountingRoutingModule,
|
|
2220
|
+
CommonModule,
|
|
2221
|
+
SpaAdminModule,
|
|
2222
|
+
// Changed: Standalone components must be imported, not declared
|
|
2223
|
+
TaxRatesComponent,
|
|
2224
|
+
StandingOrdersComponent,
|
|
2225
|
+
CurrenciesComponent // Changed: Removed Fixed Assets — moved to Assets module
|
|
2226
|
+
]
|
|
2227
|
+
}]
|
|
2228
|
+
}] });
|
|
2229
|
+
|
|
2230
|
+
export { AccountingModule };
|
|
2231
|
+
//# sourceMappingURL=tin-spa-accounting.module-Cg4vWaJE.mjs.map
|