@mj-biz-apps/orders-ng 5.2.0 → 5.3.0

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.
@@ -0,0 +1,1229 @@
1
+ import { Component, EventEmitter, Input, Output } from '@angular/core';
2
+ import { CompositeKey, Metadata, RunView } from '@memberjunction/core';
3
+ import { AccountingEngineBase, pickActiveLinkIndex } from '@mj-biz-apps/accounting-engine-base';
4
+ import * as i0 from "@angular/core";
5
+ import * as i1 from "@angular/forms";
6
+ import * as i2 from "@angular/common";
7
+ const _forTrack0 = ($index, $item) => $item.ID;
8
+ function BizAppsProductGLLinksComponent_Conditional_6_Template(rf, ctx) { if (rf & 1) {
9
+ const _r1 = i0.ɵɵgetCurrentView();
10
+ i0.ɵɵelementStart(0, "button", 12);
11
+ i0.ɵɵlistener("click", function BizAppsProductGLLinksComponent_Conditional_6_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OpenDraft()); });
12
+ i0.ɵɵelement(1, "i", 13);
13
+ i0.ɵɵelementEnd();
14
+ } }
15
+ function BizAppsProductGLLinksComponent_Conditional_9_Template(rf, ctx) { if (rf & 1) {
16
+ i0.ɵɵelementStart(0, "p", 8);
17
+ i0.ɵɵelement(1, "i", 14);
18
+ i0.ɵɵtext(2);
19
+ i0.ɵɵelementEnd();
20
+ } if (rf & 2) {
21
+ const ctx_r1 = i0.ɵɵnextContext();
22
+ i0.ɵɵadvance(2);
23
+ i0.ɵɵtextInterpolate1(" ", ctx_r1.WriteError, " ");
24
+ } }
25
+ function BizAppsProductGLLinksComponent_Conditional_10_For_5_Template(rf, ctx) { if (rf & 1) {
26
+ i0.ɵɵelementStart(0, "option", 17);
27
+ i0.ɵɵtext(1);
28
+ i0.ɵɵelementEnd();
29
+ } if (rf & 2) {
30
+ const r_r4 = ctx.$implicit;
31
+ i0.ɵɵproperty("value", r_r4.ID);
32
+ i0.ɵɵadvance();
33
+ i0.ɵɵtextInterpolate(r_r4.Name);
34
+ } }
35
+ function BizAppsProductGLLinksComponent_Conditional_10_Conditional_6_Template(rf, ctx) { if (rf & 1) {
36
+ i0.ɵɵelementStart(0, "span", 10);
37
+ i0.ɵɵtext(1, " No GL accounts exist for this product's company yet, so there is nothing to link to. They are created in Accounting. ");
38
+ i0.ɵɵelementEnd();
39
+ } }
40
+ function BizAppsProductGLLinksComponent_Conditional_10_For_11_Template(rf, ctx) { if (rf & 1) {
41
+ i0.ɵɵelementStart(0, "option", 17);
42
+ i0.ɵɵtext(1);
43
+ i0.ɵɵelementEnd();
44
+ } if (rf & 2) {
45
+ const a_r5 = ctx.$implicit;
46
+ i0.ɵɵproperty("value", a_r5.ID);
47
+ i0.ɵɵadvance();
48
+ i0.ɵɵtextInterpolate(a_r5.Label);
49
+ } }
50
+ function BizAppsProductGLLinksComponent_Conditional_10_Template(rf, ctx) { if (rf & 1) {
51
+ const _r3 = i0.ɵɵgetCurrentView();
52
+ i0.ɵɵelementStart(0, "div", 9)(1, "select", 15);
53
+ i0.ɵɵtwoWayListener("ngModelChange", function BizAppsProductGLLinksComponent_Conditional_10_Template_select_ngModelChange_1_listener($event) { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); i0.ɵɵtwoWayBindingSet(ctx_r1.Draft.RoleID, $event) || (ctx_r1.Draft.RoleID = $event); return i0.ɵɵresetView($event); });
54
+ i0.ɵɵelementStart(2, "option", 16);
55
+ i0.ɵɵtext(3, "\u2014 role \u2014");
56
+ i0.ɵɵelementEnd();
57
+ i0.ɵɵrepeaterCreate(4, BizAppsProductGLLinksComponent_Conditional_10_For_5_Template, 2, 2, "option", 17, _forTrack0);
58
+ i0.ɵɵelementEnd();
59
+ i0.ɵɵconditionalCreate(6, BizAppsProductGLLinksComponent_Conditional_10_Conditional_6_Template, 2, 0, "span", 10);
60
+ i0.ɵɵelementStart(7, "select", 18);
61
+ i0.ɵɵtwoWayListener("ngModelChange", function BizAppsProductGLLinksComponent_Conditional_10_Template_select_ngModelChange_7_listener($event) { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); i0.ɵɵtwoWayBindingSet(ctx_r1.Draft.AccountID, $event) || (ctx_r1.Draft.AccountID = $event); return i0.ɵɵresetView($event); });
62
+ i0.ɵɵelementStart(8, "option", 16);
63
+ i0.ɵɵtext(9, "\u2014 account \u2014");
64
+ i0.ɵɵelementEnd();
65
+ i0.ɵɵrepeaterCreate(10, BizAppsProductGLLinksComponent_Conditional_10_For_11_Template, 2, 2, "option", 17, _forTrack0);
66
+ i0.ɵɵelementEnd();
67
+ i0.ɵɵelementStart(12, "input", 19);
68
+ i0.ɵɵtwoWayListener("ngModelChange", function BizAppsProductGLLinksComponent_Conditional_10_Template_input_ngModelChange_12_listener($event) { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); i0.ɵɵtwoWayBindingSet(ctx_r1.Draft.StartedAt, $event) || (ctx_r1.Draft.StartedAt = $event); return i0.ɵɵresetView($event); });
69
+ i0.ɵɵelementEnd();
70
+ i0.ɵɵelementStart(13, "button", 20);
71
+ i0.ɵɵlistener("click", function BizAppsProductGLLinksComponent_Conditional_10_Template_button_click_13_listener() { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.AddLink()); });
72
+ i0.ɵɵtext(14);
73
+ i0.ɵɵelementEnd();
74
+ i0.ɵɵelementStart(15, "button", 20);
75
+ i0.ɵɵlistener("click", function BizAppsProductGLLinksComponent_Conditional_10_Template_button_click_15_listener() { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.CancelDraft()); });
76
+ i0.ɵɵtext(16, "Cancel");
77
+ i0.ɵɵelementEnd()();
78
+ } if (rf & 2) {
79
+ const ctx_r1 = i0.ɵɵnextContext();
80
+ i0.ɵɵadvance();
81
+ i0.ɵɵtwoWayProperty("ngModel", ctx_r1.Draft.RoleID);
82
+ i0.ɵɵproperty("disabled", ctx_r1.Saving);
83
+ i0.ɵɵadvance(3);
84
+ i0.ɵɵrepeater(ctx_r1.Roles);
85
+ i0.ɵɵadvance(2);
86
+ i0.ɵɵconditional(ctx_r1.Accounts.length === 0 ? 6 : -1);
87
+ i0.ɵɵadvance();
88
+ i0.ɵɵtwoWayProperty("ngModel", ctx_r1.Draft.AccountID);
89
+ i0.ɵɵproperty("disabled", ctx_r1.Saving);
90
+ i0.ɵɵadvance(3);
91
+ i0.ɵɵrepeater(ctx_r1.Accounts);
92
+ i0.ɵɵadvance(2);
93
+ i0.ɵɵtwoWayProperty("ngModel", ctx_r1.Draft.StartedAt);
94
+ i0.ɵɵproperty("disabled", ctx_r1.Saving);
95
+ i0.ɵɵadvance();
96
+ i0.ɵɵproperty("disabled", ctx_r1.Saving || !ctx_r1.Draft.RoleID || !ctx_r1.Draft.AccountID);
97
+ i0.ɵɵadvance();
98
+ i0.ɵɵtextInterpolate(ctx_r1.Saving ? "Linking\u2026" : "Link");
99
+ i0.ɵɵadvance();
100
+ i0.ɵɵproperty("disabled", ctx_r1.Saving);
101
+ } }
102
+ function BizAppsProductGLLinksComponent_Conditional_11_For_3_Template(rf, ctx) { if (rf & 1) {
103
+ i0.ɵɵelementStart(0, "option", 21);
104
+ i0.ɵɵtext(1);
105
+ i0.ɵɵelementEnd();
106
+ } if (rf & 2) {
107
+ const r_r7 = ctx.$implicit;
108
+ i0.ɵɵproperty("value", r_r7.ID)("disabled", r_r7.Disabled === true);
109
+ i0.ɵɵadvance();
110
+ i0.ɵɵtextInterpolate(r_r7.Label);
111
+ } }
112
+ function BizAppsProductGLLinksComponent_Conditional_11_For_6_Template(rf, ctx) { if (rf & 1) {
113
+ i0.ɵɵelementStart(0, "option", 21);
114
+ i0.ɵɵtext(1);
115
+ i0.ɵɵelementEnd();
116
+ } if (rf & 2) {
117
+ const a_r8 = ctx.$implicit;
118
+ i0.ɵɵproperty("value", a_r8.ID)("disabled", a_r8.Disabled === true);
119
+ i0.ɵɵadvance();
120
+ i0.ɵɵtextInterpolate(a_r8.Label);
121
+ } }
122
+ function BizAppsProductGLLinksComponent_Conditional_11_Conditional_17_Template(rf, ctx) { if (rf & 1) {
123
+ i0.ɵɵelementStart(0, "span", 10);
124
+ i0.ɵɵtext(1, " This link has been in force, so only its end date can change here. To book a different account, add a link with a later start date. ");
125
+ i0.ɵɵelementEnd();
126
+ } }
127
+ function BizAppsProductGLLinksComponent_Conditional_11_Template(rf, ctx) { if (rf & 1) {
128
+ const _r6 = i0.ɵɵgetCurrentView();
129
+ i0.ɵɵelementStart(0, "div", 9)(1, "select", 15);
130
+ i0.ɵɵtwoWayListener("ngModelChange", function BizAppsProductGLLinksComponent_Conditional_11_Template_select_ngModelChange_1_listener($event) { i0.ɵɵrestoreView(_r6); const ctx_r1 = i0.ɵɵnextContext(); i0.ɵɵtwoWayBindingSet(ctx_r1.Editing.RoleID, $event) || (ctx_r1.Editing.RoleID = $event); return i0.ɵɵresetView($event); });
131
+ i0.ɵɵrepeaterCreate(2, BizAppsProductGLLinksComponent_Conditional_11_For_3_Template, 2, 3, "option", 21, _forTrack0);
132
+ i0.ɵɵelementEnd();
133
+ i0.ɵɵelementStart(4, "select", 18);
134
+ i0.ɵɵtwoWayListener("ngModelChange", function BizAppsProductGLLinksComponent_Conditional_11_Template_select_ngModelChange_4_listener($event) { i0.ɵɵrestoreView(_r6); const ctx_r1 = i0.ɵɵnextContext(); i0.ɵɵtwoWayBindingSet(ctx_r1.Editing.AccountID, $event) || (ctx_r1.Editing.AccountID = $event); return i0.ɵɵresetView($event); });
135
+ i0.ɵɵrepeaterCreate(5, BizAppsProductGLLinksComponent_Conditional_11_For_6_Template, 2, 3, "option", 21, _forTrack0);
136
+ i0.ɵɵelementEnd();
137
+ i0.ɵɵelementStart(7, "label", 22);
138
+ i0.ɵɵtext(8, "from ");
139
+ i0.ɵɵelementStart(9, "input", 19);
140
+ i0.ɵɵtwoWayListener("ngModelChange", function BizAppsProductGLLinksComponent_Conditional_11_Template_input_ngModelChange_9_listener($event) { i0.ɵɵrestoreView(_r6); const ctx_r1 = i0.ɵɵnextContext(); i0.ɵɵtwoWayBindingSet(ctx_r1.Editing.StartedAt, $event) || (ctx_r1.Editing.StartedAt = $event); return i0.ɵɵresetView($event); });
141
+ i0.ɵɵelementEnd()();
142
+ i0.ɵɵelementStart(10, "label", 22);
143
+ i0.ɵɵtext(11, "to ");
144
+ i0.ɵɵelementStart(12, "input", 23);
145
+ i0.ɵɵtwoWayListener("ngModelChange", function BizAppsProductGLLinksComponent_Conditional_11_Template_input_ngModelChange_12_listener($event) { i0.ɵɵrestoreView(_r6); const ctx_r1 = i0.ɵɵnextContext(); i0.ɵɵtwoWayBindingSet(ctx_r1.Editing.EndedAt, $event) || (ctx_r1.Editing.EndedAt = $event); return i0.ɵɵresetView($event); });
146
+ i0.ɵɵelementEnd()();
147
+ i0.ɵɵelementStart(13, "button", 20);
148
+ i0.ɵɵlistener("click", function BizAppsProductGLLinksComponent_Conditional_11_Template_button_click_13_listener() { i0.ɵɵrestoreView(_r6); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.SaveEdit()); });
149
+ i0.ɵɵtext(14);
150
+ i0.ɵɵelementEnd();
151
+ i0.ɵɵelementStart(15, "button", 20);
152
+ i0.ɵɵlistener("click", function BizAppsProductGLLinksComponent_Conditional_11_Template_button_click_15_listener() { i0.ɵɵrestoreView(_r6); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.CancelEdit()); });
153
+ i0.ɵɵtext(16, "Cancel");
154
+ i0.ɵɵelementEnd();
155
+ i0.ɵɵconditionalCreate(17, BizAppsProductGLLinksComponent_Conditional_11_Conditional_17_Template, 2, 0, "span", 10);
156
+ i0.ɵɵelementEnd();
157
+ } if (rf & 2) {
158
+ const ctx_r1 = i0.ɵɵnextContext();
159
+ i0.ɵɵadvance();
160
+ i0.ɵɵtwoWayProperty("ngModel", ctx_r1.Editing.RoleID);
161
+ i0.ɵɵproperty("disabled", ctx_r1.Saving || ctx_r1.Editing.Applied);
162
+ i0.ɵɵadvance();
163
+ i0.ɵɵrepeater(ctx_r1.EditRoles);
164
+ i0.ɵɵadvance(2);
165
+ i0.ɵɵtwoWayProperty("ngModel", ctx_r1.Editing.AccountID);
166
+ i0.ɵɵproperty("disabled", ctx_r1.Saving || ctx_r1.Editing.Applied);
167
+ i0.ɵɵadvance();
168
+ i0.ɵɵrepeater(ctx_r1.EditAccounts);
169
+ i0.ɵɵadvance(4);
170
+ i0.ɵɵtwoWayProperty("ngModel", ctx_r1.Editing.StartedAt);
171
+ i0.ɵɵproperty("disabled", ctx_r1.Saving || ctx_r1.Editing.Applied);
172
+ i0.ɵɵadvance(3);
173
+ i0.ɵɵtwoWayProperty("ngModel", ctx_r1.Editing.EndedAt);
174
+ i0.ɵɵproperty("disabled", ctx_r1.Saving);
175
+ i0.ɵɵadvance();
176
+ i0.ɵɵproperty("disabled", ctx_r1.Saving);
177
+ i0.ɵɵadvance();
178
+ i0.ɵɵtextInterpolate(ctx_r1.Saving ? "Saving\u2026" : "Save");
179
+ i0.ɵɵadvance();
180
+ i0.ɵɵproperty("disabled", ctx_r1.Saving);
181
+ i0.ɵɵadvance(2);
182
+ i0.ɵɵconditional(ctx_r1.Editing.Applied ? 17 : -1);
183
+ } }
184
+ function BizAppsProductGLLinksComponent_Conditional_12_Template(rf, ctx) { if (rf & 1) {
185
+ i0.ɵɵelementStart(0, "p", 10);
186
+ i0.ɵɵtext(1, "Loading\u2026");
187
+ i0.ɵɵelementEnd();
188
+ } }
189
+ function BizAppsProductGLLinksComponent_Conditional_13_Template(rf, ctx) { if (rf & 1) {
190
+ i0.ɵɵelementStart(0, "p", 8);
191
+ i0.ɵɵelement(1, "i", 14);
192
+ i0.ɵɵtext(2);
193
+ i0.ɵɵelementEnd();
194
+ } if (rf & 2) {
195
+ const ctx_r1 = i0.ɵɵnextContext();
196
+ i0.ɵɵadvance(2);
197
+ i0.ɵɵtextInterpolate1(" The GL account links could not be read: ", ctx_r1.LoadError, " ");
198
+ } }
199
+ function BizAppsProductGLLinksComponent_Conditional_14_Template(rf, ctx) { if (rf & 1) {
200
+ i0.ɵɵelementStart(0, "p", 10);
201
+ i0.ɵɵtext(1, " No GL accounts are linked directly to this product. It may still book through its category or the company default \u2014 open Accounting \u2192 Account Links to see the full chain. ");
202
+ i0.ɵɵelementEnd();
203
+ } }
204
+ function BizAppsProductGLLinksComponent_Conditional_15_For_19_Conditional_18_Template(rf, ctx) { if (rf & 1) {
205
+ const _r11 = i0.ɵɵgetCurrentView();
206
+ i0.ɵɵelementStart(0, "button", 33);
207
+ i0.ɵɵlistener("click", function BizAppsProductGLLinksComponent_Conditional_15_For_19_Conditional_18_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r11); const row_r10 = i0.ɵɵnextContext().$implicit; const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.OpenEdit(row_r10)); });
208
+ i0.ɵɵtext(1, "Edit");
209
+ i0.ɵɵelementEnd();
210
+ } if (rf & 2) {
211
+ const ctx_r1 = i0.ɵɵnextContext(3);
212
+ i0.ɵɵproperty("disabled", ctx_r1.Saving);
213
+ } }
214
+ function BizAppsProductGLLinksComponent_Conditional_15_For_19_Conditional_19_Template(rf, ctx) { if (rf & 1) {
215
+ const _r12 = i0.ɵɵgetCurrentView();
216
+ i0.ɵɵelementStart(0, "button", 34);
217
+ i0.ɵɵlistener("click", function BizAppsProductGLLinksComponent_Conditional_15_For_19_Conditional_19_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r12); const row_r10 = i0.ɵɵnextContext().$implicit; const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.RetireLink(row_r10)); });
218
+ i0.ɵɵtext(1, "Retire");
219
+ i0.ɵɵelementEnd();
220
+ } if (rf & 2) {
221
+ const ctx_r1 = i0.ɵɵnextContext(3);
222
+ i0.ɵɵproperty("disabled", ctx_r1.Saving);
223
+ } }
224
+ function BizAppsProductGLLinksComponent_Conditional_15_For_19_Conditional_20_Template(rf, ctx) { if (rf & 1) {
225
+ const _r13 = i0.ɵɵgetCurrentView();
226
+ i0.ɵɵelementStart(0, "button", 35);
227
+ i0.ɵɵlistener("click", function BizAppsProductGLLinksComponent_Conditional_15_For_19_Conditional_20_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r13); const row_r10 = i0.ɵɵnextContext().$implicit; const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.RemoveLink(row_r10)); });
228
+ i0.ɵɵtext(1, "Remove");
229
+ i0.ɵɵelementEnd();
230
+ } if (rf & 2) {
231
+ const row_r10 = i0.ɵɵnextContext().$implicit;
232
+ const ctx_r1 = i0.ɵɵnextContext(2);
233
+ i0.ɵɵproperty("disabled", ctx_r1.Saving)("title", ctx_r1.RemoveWarning(row_r10));
234
+ } }
235
+ function BizAppsProductGLLinksComponent_Conditional_15_For_19_Template(rf, ctx) { if (rf & 1) {
236
+ const _r9 = i0.ɵɵgetCurrentView();
237
+ i0.ɵɵelementStart(0, "tr")(1, "td");
238
+ i0.ɵɵtext(2);
239
+ i0.ɵɵelementEnd();
240
+ i0.ɵɵelementStart(3, "td")(4, "button", 27);
241
+ i0.ɵɵlistener("click", function BizAppsProductGLLinksComponent_Conditional_15_For_19_Template_button_click_4_listener() { const row_r10 = i0.ɵɵrestoreView(_r9).$implicit; const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.OpenLink(row_r10)); });
242
+ i0.ɵɵtext(5);
243
+ i0.ɵɵelementStart(6, "span", 28);
244
+ i0.ɵɵtext(7);
245
+ i0.ɵɵelementEnd()()();
246
+ i0.ɵɵelementStart(8, "td")(9, "span", 29);
247
+ i0.ɵɵtext(10);
248
+ i0.ɵɵelementEnd()();
249
+ i0.ɵɵelementStart(11, "td");
250
+ i0.ɵɵtext(12);
251
+ i0.ɵɵpipe(13, "date");
252
+ i0.ɵɵelementEnd();
253
+ i0.ɵɵelementStart(14, "td");
254
+ i0.ɵɵtext(15);
255
+ i0.ɵɵpipe(16, "date");
256
+ i0.ɵɵelementEnd();
257
+ i0.ɵɵelementStart(17, "td");
258
+ i0.ɵɵconditionalCreate(18, BizAppsProductGLLinksComponent_Conditional_15_For_19_Conditional_18_Template, 2, 1, "button", 30);
259
+ i0.ɵɵconditionalCreate(19, BizAppsProductGLLinksComponent_Conditional_15_For_19_Conditional_19_Template, 2, 1, "button", 31);
260
+ i0.ɵɵconditionalCreate(20, BizAppsProductGLLinksComponent_Conditional_15_For_19_Conditional_20_Template, 2, 2, "button", 32);
261
+ i0.ɵɵelementEnd()();
262
+ } if (rf & 2) {
263
+ const row_r10 = ctx.$implicit;
264
+ const ctx_r1 = i0.ɵɵnextContext(2);
265
+ i0.ɵɵclassProp("mjo-gl-links__row--inactive", !row_r10.Active);
266
+ i0.ɵɵadvance(2);
267
+ i0.ɵɵtextInterpolate(row_r10.RoleName);
268
+ i0.ɵɵadvance(3);
269
+ i0.ɵɵtextInterpolate1(" ", row_r10.AccountCode);
270
+ i0.ɵɵadvance(2);
271
+ i0.ɵɵtextInterpolate1(" ", row_r10.AccountName);
272
+ i0.ɵɵadvance(2);
273
+ i0.ɵɵclassProp("mjo-gl-links__chip--on", row_r10.Active);
274
+ i0.ɵɵadvance();
275
+ i0.ɵɵtextInterpolate1(" ", ctx_r1.StatusLabel(row_r10), " ");
276
+ i0.ɵɵadvance(2);
277
+ i0.ɵɵtextInterpolate(row_r10.StartedAt ? i0.ɵɵpipeBind3(13, 13, row_r10.StartedAt, "yyyy-MM-dd", "UTC") : "\u2014");
278
+ i0.ɵɵadvance(3);
279
+ i0.ɵɵtextInterpolate(row_r10.EndedAt ? i0.ɵɵpipeBind3(16, 17, row_r10.EndedAt, "yyyy-MM-dd", "UTC") : "\u2014");
280
+ i0.ɵɵadvance(3);
281
+ i0.ɵɵconditional(ctx_r1.CanWrite && (ctx_r1.Editing == null ? null : ctx_r1.Editing.ID) !== row_r10.ID ? 18 : -1);
282
+ i0.ɵɵadvance();
283
+ i0.ɵɵconditional(ctx_r1.CanWrite && row_r10.Active && ctx_r1.HasApplied(row_r10) ? 19 : -1);
284
+ i0.ɵɵadvance();
285
+ i0.ɵɵconditional(ctx_r1.CanWrite && !ctx_r1.HasApplied(row_r10) ? 20 : -1);
286
+ } }
287
+ function BizAppsProductGLLinksComponent_Conditional_15_Template(rf, ctx) { if (rf & 1) {
288
+ i0.ɵɵelementStart(0, "div", 11)(1, "table", 24)(2, "thead")(3, "tr")(4, "th");
289
+ i0.ɵɵtext(5, "Role");
290
+ i0.ɵɵelementEnd();
291
+ i0.ɵɵelementStart(6, "th");
292
+ i0.ɵɵtext(7, "Account");
293
+ i0.ɵɵelementEnd();
294
+ i0.ɵɵelementStart(8, "th");
295
+ i0.ɵɵtext(9, "Status");
296
+ i0.ɵɵelementEnd();
297
+ i0.ɵɵelementStart(10, "th");
298
+ i0.ɵɵtext(11, "From");
299
+ i0.ɵɵelementEnd();
300
+ i0.ɵɵelementStart(12, "th");
301
+ i0.ɵɵtext(13, "To");
302
+ i0.ɵɵelementEnd();
303
+ i0.ɵɵelementStart(14, "th")(15, "span", 25);
304
+ i0.ɵɵtext(16, "Row actions");
305
+ i0.ɵɵelementEnd()()()();
306
+ i0.ɵɵelementStart(17, "tbody");
307
+ i0.ɵɵrepeaterCreate(18, BizAppsProductGLLinksComponent_Conditional_15_For_19_Template, 21, 21, "tr", 26, _forTrack0);
308
+ i0.ɵɵelementEnd()()();
309
+ } if (rf & 2) {
310
+ const ctx_r1 = i0.ɵɵnextContext();
311
+ i0.ɵɵadvance(18);
312
+ i0.ɵɵrepeater(ctx_r1.Rows);
313
+ } }
314
+ /** Accounting's polymorphic link entity. A SOFT reference — no FK crosses the schema boundary. */
315
+ const LINK_ENTITY = 'MJ_BizApps_Accounting: GL Account Links';
316
+ /** This app's Products entity, which is what a link's `(EntityID, RecordID)` pair points AT here. */
317
+ const PRODUCT_ENTITY = 'MJ_BizApps_Orders: Products';
318
+ /**
319
+ * Ids reach a filter string as SQL text, so they are SHAPE-CHECKED rather than escaped.
320
+ *
321
+ * `CLAUDE.md` forbids inline `.replace(/'/g, "''")` in an `ExtraFilter` outright — it misses
322
+ * null-byte injection, breaks on null/undefined, and spreads divergent ad-hoc sanitisation. Its
323
+ * prescribed helpers live in `sql-guards.ts` under CoreEntitiesServer, which an Angular package must
324
+ * not depend on, so this mirrors the browser-side guard this package already uses in
325
+ * `lib/data/orders-queries.ts` instead of inventing a third spelling.
326
+ */
327
+ const UUID_PATTERN = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
328
+ export function idKey(id) {
329
+ return String(id ?? '').trim().toLowerCase();
330
+ }
331
+ export function AccountIsOfferable(accountCompanyID, productCompanyID) {
332
+ const product = idKey(productCompanyID);
333
+ if (!product)
334
+ return true;
335
+ return idKey(accountCompanyID) === product;
336
+ }
337
+ /**
338
+ * The GL accounts a product books to — read from the product's own form (bizapps-orders#113).
339
+ *
340
+ * ── WHY THIS LIVES IN ORDERS AND NOT IN ACCOUNTING ──
341
+ *
342
+ * `GLAccountLink` is accounting's entity, so the instinct is to put its UI there. Accounting's own
343
+ * links screen explains why that cannot work for products, in its `LinkRow` comment:
344
+ *
345
+ * > These links are POLYMORPHIC — `(EntityID, RecordID)` can point at anything, including orders'
346
+ * > Products and Product Categories. Accounting must not depend on orders, so those stay unnamed
347
+ * > here BY DESIGN.
348
+ *
349
+ * So on accounting's screen a product-linked row shows a bare UUID and says so honestly. Andrew's
350
+ * report (#113) is that same boundary seen from the other side: he could not find a product's accounts,
351
+ * and creating a link by hand meant copying the Product's ID into a text box.
352
+ *
353
+ * Rendering it HERE inverts the dependency the right way round. Orders already depends on accounting
354
+ * — `AccountingBridge` calls the engine on both booking paths — and orders is the app that knows what
355
+ * a Product is. The polymorphic pair stops being something a human types: `EntityID` is this app's
356
+ * Products entity and `RecordID` is the record already on screen.
357
+ *
358
+ * ── WHY THE ENGINE AND NOT A QUERY ──
359
+ *
360
+ * `AccountingEngineBase` caches roles, accounts and links client-side and is the same primitive the
361
+ * booking pipeline resolves through (`ResolveLinkedAccount`). Reading from it means this panel cannot
362
+ * disagree with what an order will actually book — a re-implemented query could, and the first time it
363
+ * did, the screen would be lying about money.
364
+ *
365
+ * ── WHAT IS DELIBERATELY NOT HERE ──
366
+ *
367
+ * No fallback chain. A product with no direct link still books, through category and company defaults,
368
+ * and `GlResolutionPreviewComponent` in accounting-ng already renders that chain. Showing a resolved
369
+ * account here without saying it came from a category would read as "this product is linked" when it is
370
+ * not, which is the same confusion #112 caused one screen over.
371
+ */
372
+ export class BizAppsProductGLLinksComponent {
373
+ cdr;
374
+ Product;
375
+ EditMode = false;
376
+ FormContext;
377
+ Navigate = new EventEmitter();
378
+ Rows = [];
379
+ Loading = true;
380
+ /** Set when the links cannot be read at all. Shown instead of an empty list, which would lie. */
381
+ LoadError = null;
382
+ constructor(cdr) {
383
+ this.cdr = cdr;
384
+ }
385
+ async ngOnInit() {
386
+ /**
387
+ * FALSE, explicitly. The first mount is the one place a cached answer is what you want — the
388
+ * engine has usually just been configured by whatever rendered the form, and forcing a reload
389
+ * here would make opening any product form pay for ten entity loads.
390
+ *
391
+ * Every other caller wants the opposite, which is why `Refresh` defaults to true.
392
+ */
393
+ await this.Refresh(false);
394
+ }
395
+ /**
396
+ * Re-reads the links.
397
+ *
398
+ * ── WHY A WRITE MUST PASS `true` ──
399
+ *
400
+ * `load()` prefers the engine's cached links and only falls back to a view read when the cache
401
+ * holds none for this product. That is right for a plain refresh and WRONG straight after a write:
402
+ * the cache is a snapshot taken at `Config()` time, so a product that already had one link would
403
+ * keep showing exactly that one — the row just added, or the retirement just applied, would be
404
+ * invisible until something else reloaded the engine.
405
+ *
406
+ * It survived the first browser test only because that test started from a product with NO links,
407
+ * where the cache is empty and the fallback runs anyway. Adding a second link is the case that
408
+ * would have shown nothing.
409
+ *
410
+ * @param reloadEngine - force the accounting engine to re-read before rendering. Every write path
411
+ * defaults TRUE. `ngOnInit` passes false, because a first mount is the one
412
+ * place a cached answer is the right one; the refresh button and every write
413
+ * take the default, because both are asking the panel to go and look.
414
+ */
415
+ /**
416
+ * Set in `ngOnDestroy`, checked before every `detectChanges`.
417
+ *
418
+ * Every write ends with `finally { Saving = false; cdr.detectChanges(); }` AFTER an awaited save
419
+ * and engine reload. Navigate away mid-save and those resolve against a torn-down view, so
420
+ * `detectChanges` throws `ViewDestroyedError` — and because it throws from `finally`, the
421
+ * preceding `catch` cannot absorb it and it escapes as an unhandled rejection. This file's own
422
+ * comment elsewhere notes that a console error is treated as a broken screen by the Playwright
423
+ * keystone, so it would fail a suite rather than merely litter the console.
424
+ */
425
+ destroyed = false;
426
+ ngOnDestroy() {
427
+ this.destroyed = true;
428
+ }
429
+ /** `detectChanges`, unless the view is gone. */
430
+ tick() {
431
+ if (!this.destroyed) {
432
+ this.cdr.detectChanges();
433
+ }
434
+ }
435
+ /**
436
+ * `reloadEngine` DEFAULTS TRUE, and the default is the whole point.
437
+ *
438
+ * It used to default false, so the refresh icon re-ran `load()` against a cache nobody had
439
+ * invalidated — and `load()`'s own `Config(false, …)` is a no-op once the engine is loaded. A
440
+ * colleague retiring a link from accounting's own screen was therefore invisible here: the button
441
+ * rendered the identical stale row, still chipped "In force", for the rest of the session.
442
+ *
443
+ * A user who presses refresh is asking the panel to go and look. Writes already passed `true`
444
+ * explicitly; making that the default means the only path that can serve a cached answer is the
445
+ * initial load, which is the one place a cache is what you want.
446
+ */
447
+ async Refresh(reloadEngine = true) {
448
+ this.Loading = true;
449
+ this.LoadError = null;
450
+ this.tick();
451
+ try {
452
+ if (reloadEngine) {
453
+ await AccountingEngineBase.Instance.Config(true, new Metadata().CurrentUser);
454
+ }
455
+ this.Rows = await this.load();
456
+ }
457
+ catch (err) {
458
+ this.Rows = [];
459
+ this.LoadError = err instanceof Error ? err.message : String(err);
460
+ }
461
+ finally {
462
+ this.Loading = false;
463
+ this.tick();
464
+ }
465
+ }
466
+ /**
467
+ * What the status chip says.
468
+ *
469
+ * The three states are NOT the two the `Status` column has. A link can be stored `Active` and still
470
+ * not be the one that books — superseded by a later link on the same role — and rendering the raw
471
+ * column would put the word "Active" on both rows. Saying SUPERSEDED is the whole reason this panel
472
+ * resolves through accounting's picker rather than reading a flag.
473
+ */
474
+ /**
475
+ * Why a row is not the one in force.
476
+ *
477
+ * `Active === false` has three causes and this used to print "Superseded" for all of them. A link
478
+ * added to take over next quarter therefore rendered as "Superseded" — it supersedes something, it
479
+ * is not superseded — beside a Remove button saying it had not started. Two controls in one row
480
+ * telling opposite stories is how a user concludes the save was rejected and adds another link.
481
+ */
482
+ StatusLabel(row) {
483
+ if (row.Active) {
484
+ return 'In force';
485
+ }
486
+ if (row.Status !== 'Active') {
487
+ // Disabled or Pending — the row's own status is the honest answer.
488
+ return row.Status;
489
+ }
490
+ // Active, but not the winner. Which of the two remaining reasons applies is a date question.
491
+ return this.HasApplied(row) ? 'Superseded' : 'Not started';
492
+ }
493
+ /**
494
+ * What Remove actually does to THIS row, said accurately.
495
+ *
496
+ * ── THE CONTROL USED TO ASSERT SOMETHING FALSE ────────────────────────────────────────────────
497
+ *
498
+ * Its title read "It has not started yet, so nothing has booked through it." That is true of a link
499
+ * dated in the future and FALSE of a link dated today, because `pickActiveLinkIndex` skips only
500
+ * when `t < started` — a link starting at today's UTC midnight is already the one resolution
501
+ * picks. `OpenDraft` defaults the start to today, so every link this panel creates spends its
502
+ * first UTC day in exactly that state: chip reading "In force", button promising nothing had
503
+ * booked, and `Delete()` behind it.
504
+ *
505
+ * The button stays, because it has to. `CK_GLAccountLink_Window` enforces `EndedAt > StartedAt`,
506
+ * so a link created today CANNOT be retired — Remove is the only way to undo a mistake made today.
507
+ * What was wrong was the sentence, not the affordance.
508
+ */
509
+ RemoveWarning(row) {
510
+ return row.Active
511
+ ? 'Remove this link permanently. It is IN FORCE today, so orders booked today may already '
512
+ + 'have resolved through it. It cannot be retired instead, because a link cannot end on '
513
+ + 'the day it starts.'
514
+ : 'Remove this link permanently. It has not started yet, so nothing has booked through it.';
515
+ }
516
+ /** Opens accounting's Account Links screen, which is where a link is created or retired. */
517
+ OpenLink(row) {
518
+ this.Navigate.emit({
519
+ Kind: 'record',
520
+ EntityName: LINK_ENTITY,
521
+ PrimaryKey: CompositeKey.FromID(row.ID),
522
+ });
523
+ }
524
+ // ── Writing: the half accounting has no UI for at all ─────────────────────────────────────────
525
+ /** Roles and accounts to choose from, filled by {@link Refresh} off the same engine the reader uses. */
526
+ Roles = [];
527
+ Accounts = [];
528
+ /** The add form. Null when closed — opening it is a deliberate act, not the default state. */
529
+ Draft = null;
530
+ /** Set when a write is refused. The SERVER's message, verbatim — see {@link AddLink}. */
531
+ WriteError = null;
532
+ Saving = false;
533
+ /** A product must exist before anything can point at it: `RecordID` is its primary key. */
534
+ get CanWrite() {
535
+ /**
536
+ * `EditMode` IS READ HERE, and its absence was a real defect rather than a tidy-up.
537
+ *
538
+ * The parent passes it in and this component accepted it and never looked at it, so Add, Edit,
539
+ * Retire and Remove stayed live while the form was in read-only mode — the sibling pricing
540
+ * widget gates every one of its edit affordances on the same input. An unread `@Input` also
541
+ * reads, to anyone scanning the class, as though the gating were already handled.
542
+ */
543
+ return this.EditMode && !!this.Product?.ID && this.Product.IsSaved;
544
+ }
545
+ OpenDraft() {
546
+ this.WriteError = null;
547
+ this.Draft = { RoleID: '', AccountID: '', StartedAt: this.today() };
548
+ }
549
+ CancelDraft() {
550
+ this.Draft = null;
551
+ this.WriteError = null;
552
+ }
553
+ /**
554
+ * The link being edited, or null. `Applied` decides how much of it may change — see {@link OpenEdit}.
555
+ */
556
+ Editing = null;
557
+ /**
558
+ * The options the EDIT picker shows: the company-scoped list, plus this link's CURRENT account when
559
+ * that account is not in it.
560
+ *
561
+ * ── WHY THE SCOPED LIST IS NOT ENOUGH ON ITS OWN ──────────────────────────────────────────────
562
+ *
563
+ * Scoping accounts to the product's company (PR #125 review) is right for CHOOSING, but a link that
564
+ * already points somewhere else still has to render. Such links exist: nothing stopped the panel
565
+ * creating them before this change, and nothing stops another tool creating one now. Showing only
566
+ * the scoped list would leave the select with no matching option — blank — on a link that plainly
567
+ * has an account.
568
+ *
569
+ * The template a few lines above already states the rule this would break: hiding a value "would
570
+ * read as 'this link has no account', which is the confusion #112 was". A filtered picker on an
571
+ * existing record must never hide that record's current value.
572
+ *
573
+ * The current account is labelled so the reader can tell it apart from the offerable ones, and the
574
+ * ordinary edit rules still apply — an applied link cannot change its account at all, and a
575
+ * not-yet-applied one can be moved onto a valid account, which is the repair path.
576
+ */
577
+ get EditAccounts() {
578
+ return this.EditAccountOptions;
579
+ }
580
+ /**
581
+ * Frozen when the edit opens, NOT derived from the live `ngModel` value.
582
+ *
583
+ * Deriving it was wrong twice. It allocated a fresh array and object on every change-detection
584
+ * pass — the hazard this codebase already documents with its frozen-empty constant elsewhere — and,
585
+ * worse, the option deleted itself: the moment a user picked a valid account to repair a
586
+ * cross-company link, `current` matched the scoped list and the original option vanished, so they
587
+ * could neither compare the two nor change their mind without cancelling the whole edit.
588
+ */
589
+ EditAccountOptions = [];
590
+ /**
591
+ * The role options for one edit, with the link's current role re-admitted when the engine has no
592
+ * such role.
593
+ *
594
+ * Accounts got this treatment and roles did not, which left the identical hole one field over. A
595
+ * link whose role is missing from the engine cache — deleted, or filtered out by row-level
596
+ * permissions — renders `(unknown role)` in the table, and the select then matched no option and
597
+ * rendered BLANK. Carrying `RoleID` on the row fixed the duplicate-name case only; it cannot put a
598
+ * matching `<option>` in a list that does not contain one.
599
+ *
600
+ * Disabled for the same reason as the account equivalent: visible so the picker never reads as
601
+ * "this link has no role", unselectable so a save cannot write an orphan id back.
602
+ */
603
+ get EditRoles() {
604
+ return this.EditRoleOptions;
605
+ }
606
+ EditRoleOptions = [];
607
+ roleOptionsForEditing(row) {
608
+ const current = idKey(row.RoleID);
609
+ const offered = this.Roles.map((r) => ({ ID: r.ID, Label: r.Name }));
610
+ if (!current || offered.some((r) => idKey(r.ID) === current))
611
+ return offered;
612
+ return [...offered, { ID: current, Label: `${row.RoleName} — no longer available`, Disabled: true }];
613
+ }
614
+ /** The options for one edit, with the link's current account re-admitted when it is not offerable. */
615
+ optionsForEditing(row) {
616
+ const current = idKey(row.AccountID);
617
+ if (!current || this.Accounts.some((a) => idKey(a.ID) === current))
618
+ return this.Accounts;
619
+ /**
620
+ * Re-admitted so the picker cannot render blank on a link that plainly has an account — the
621
+ * template states that rule a few lines above the select, and it is the #112 confusion.
622
+ *
623
+ * DISABLED, though, and appended rather than prepended. Re-admitting it as a selectable option
624
+ * at the top of the list would let the repair path re-create the very link the scoping exists to
625
+ * prevent, and nothing downstream would stop it: accounting's tie guard only refuses ties WITHIN
626
+ * a company. The user may see what the link points at; they may not choose it again.
627
+ *
628
+ * The label names the company, because on this one option the company is the whole point.
629
+ */
630
+ const label = `${row.AccountCode} ${row.AccountName}`.trim() || current;
631
+ return [...this.Accounts, { ID: current, Label: `${label} — not available for this product's company`, Disabled: true }];
632
+ }
633
+ /**
634
+ * Opens an edit for one link.
635
+ *
636
+ * ── WHAT MAY CHANGE DEPENDS ON WHETHER IT HAS BOOKED ──
637
+ *
638
+ * #113 asks for "adding/editing/deleting" and editing has to exist. But a link is not an ordinary
639
+ * record: while it is in force it is the reason journal entries name the account they name. Letting
640
+ * someone re-point the account or move the start date on such a link would silently rewrite the
641
+ * explanation for money that has already moved, and nothing else records what the mapping used to
642
+ * be.
643
+ *
644
+ * So the split follows {@link HasApplied}, the same boundary Remove uses:
645
+ *
646
+ * · NOT applied yet — nothing has booked through it, so role, account and start date are all
647
+ * editable. This is the "I picked the wrong account a minute ago" case.
648
+ * · ALREADY applied — only the END date may change. That is a correction to when the mapping
649
+ * stopped, which is a statement about the future of a window rather than a rewrite of its past.
650
+ * To change where an applied product books, add a link with a later start; superseding is the
651
+ * accounting-correct edit and `pickActiveLinkIndex` already understands it.
652
+ */
653
+ OpenEdit(row) {
654
+ this.WriteError = null;
655
+ this.Draft = null;
656
+ this.EditAccountOptions = this.optionsForEditing(row);
657
+ this.EditRoleOptions = this.roleOptionsForEditing(row);
658
+ this.Editing = {
659
+ ID: row.ID,
660
+ // Normalised HERE too, not only in load(): this is the boundary that feeds the select,
661
+ // and a row assembled anywhere else must not be able to render it blank.
662
+ RoleID: idKey(row.RoleID),
663
+ AccountID: idKey(row.AccountID),
664
+ StartedAt: this.asDateInput(row.StartedAt),
665
+ EndedAt: this.asDateInput(row.EndedAt),
666
+ Applied: this.HasApplied(row),
667
+ };
668
+ }
669
+ CancelEdit() {
670
+ this.Editing = null;
671
+ this.EditAccountOptions = [];
672
+ this.EditRoleOptions = [];
673
+ this.WriteError = null;
674
+ }
675
+ /**
676
+ * Writes the edit.
677
+ *
678
+ * Only the fields the tier allows are assigned — the guard is here as well as in the template, so a
679
+ * stale render cannot rewrite an applied link's account by being clicked after the boundary moved.
680
+ * An empty end date clears it, which is how a retire applied by mistake is undone.
681
+ */
682
+ /**
683
+ * Has this link applied AS OF NOW — read from the LIVE row, never from the edit snapshot.
684
+ *
685
+ * `Editing.Applied` is captured when the form opens and the template reads the same captured
686
+ * value. That is fine for deciding what to RENDER, and wrong for deciding what to WRITE: a form
687
+ * opened at 23:59 on a link starting tomorrow is still open at 00:01, when the link has come into
688
+ * force. Trusting the snapshot there let a save rewrite the role, account and start date of a link
689
+ * that was by then explaining journal entries.
690
+ *
691
+ * A row that has vanished counts as APPLIED. The safe direction is refusing an edit that might
692
+ * have been allowed, not permitting one that rewrites history because the row could not be found.
693
+ */
694
+ appliedNowFor(id) {
695
+ const live = this.Rows.find((r) => idKey(r.ID) === idKey(id));
696
+ return live ? this.HasApplied(live) : true;
697
+ }
698
+ async SaveEdit() {
699
+ const edit = this.Editing;
700
+ if (!edit) {
701
+ return;
702
+ }
703
+ this.Saving = true;
704
+ this.WriteError = null;
705
+ this.tick();
706
+ try {
707
+ const md = new Metadata();
708
+ const link = await md.GetEntityObject(LINK_ENTITY);
709
+ if (!(await link.InnerLoad(CompositeKey.FromID(edit.ID)))) {
710
+ throw new Error(`link ${edit.ID} could not be re-read`);
711
+ }
712
+ /**
713
+ * RE-DERIVED, not read off the snapshot.
714
+ *
715
+ * This used to test `edit.Applied`, captured once in `OpenEdit`. The comment claimed it was a
716
+ * second line of defence behind the template's `[disabled]`, but both read the SAME frozen
717
+ * boolean, so it could not disagree with the template and defended nothing. Open the form at
718
+ * 23:58 on a link starting today, save at 00:02, and the link had come into force while the
719
+ * snapshot still said otherwise — so role, account and start were rewritten on a link that
720
+ * was by then explaining real journal entries.
721
+ *
722
+ * `RemoveLink` already re-evaluated live; the asymmetry was the bug.
723
+ */
724
+ /**
725
+ * The stored row, not the form. The form's `StartedAt` is what the user is proposing; the
726
+ * question here is whether the link AS STORED has come into force since the form opened.
727
+ * If the row has disappeared underneath us the snapshot is the safer answer — treat it as
728
+ * applied and write nothing but the end date.
729
+ */
730
+ const appliedNow = this.appliedNowFor(edit.ID);
731
+ if (!appliedNow) {
732
+ link.Set('GLAccountRoleID', edit.RoleID);
733
+ link.Set('GLAccountID', edit.AccountID);
734
+ link.Set('StartedAt', edit.StartedAt ? new Date(`${edit.StartedAt}T00:00:00.000Z`) : null);
735
+ }
736
+ link.Set('EndedAt', edit.EndedAt ? new Date(`${edit.EndedAt}T00:00:00.000Z`) : null);
737
+ if (!(await link.Save())) {
738
+ this.WriteError = this.readableError(link) ?? 'the link could not be updated';
739
+ return;
740
+ }
741
+ this.Editing = null;
742
+ await this.Refresh(true);
743
+ }
744
+ catch (err) {
745
+ this.WriteError = err instanceof Error ? err.message : String(err);
746
+ }
747
+ finally {
748
+ this.Saving = false;
749
+ this.tick();
750
+ }
751
+ }
752
+ /**
753
+ * `yyyy-MM-dd` for a date input, in UTC.
754
+ *
755
+ * The same conversion the sales workspace isolates in `deal-workspace.dates.ts`: an
756
+ * `<input type="date">` reads and writes strings while the field holds a Date, and handing an input
757
+ * a Date renders it BLANK with no error. UTC because the window is stored and displayed in UTC.
758
+ */
759
+ asDateInput(value) {
760
+ if (!value) {
761
+ return '';
762
+ }
763
+ const d = new Date(value);
764
+ if (Number.isNaN(d.getTime())) {
765
+ return '';
766
+ }
767
+ return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`;
768
+ }
769
+ /**
770
+ * Creates the link.
771
+ *
772
+ * The polymorphic pair is NOT typed by a human, and that is the entire point of doing this here:
773
+ * `EntityID` is orders' Products entity and `RecordID` is the record already on screen. On
774
+ * accounting's own screen a person would have to paste the product's UUID into a text box, which is
775
+ * what #113 is reporting.
776
+ *
777
+ * ── THE REFUSAL IS SURFACED, NOT RE-IMPLEMENTED ──
778
+ *
779
+ * `GLAccountLinkEntityServer` refuses two ACTIVE links for the same (record, role) whose accounts
780
+ * share a company AND share a `StartedAt`, because the tie-break is a strict `>` and resolution
781
+ * would otherwise pick arbitrarily between two accounts that both balance. This does not restate
782
+ * that rule client-side: a copy would drift from it, and the copy would be the one that silently
783
+ * disagreed. It saves, and shows the server's own message when the server says no — which is also
784
+ * how the human learns that superseding means a LATER start date rather than a duplicate.
785
+ */
786
+ async AddLink() {
787
+ const draft = this.Draft;
788
+ /**
789
+ * The START DATE IS REQUIRED, and leaving it optional produced a link that did nothing.
790
+ *
791
+ * `<input type="date">` is clearable, and neither this guard nor the template's disabled-state
792
+ * checked it. A null `StartedAt` then LOSES `pickActiveLinkIndex`'s tie-break — it compares
793
+ * `(started ?? -Infinity)` — so the new link was written, accepted by the server (a different
794
+ * start means the duplicate rule does not fire), and silently ranked below any dated link.
795
+ * The row appeared as history next to the one it was meant to replace, with no error to explain
796
+ * it, and `HasApplied(null)` returns true so the panel offered no way to remove it either.
797
+ */
798
+ if (!draft || !draft.RoleID || !draft.AccountID || !draft.StartedAt || !this.Product?.ID) {
799
+ return;
800
+ }
801
+ this.Saving = true;
802
+ this.WriteError = null;
803
+ this.tick();
804
+ try {
805
+ const md = new Metadata();
806
+ const productEntity = md.Entities.find((e) => e.Name === PRODUCT_ENTITY);
807
+ if (!productEntity) {
808
+ throw new Error(`${PRODUCT_ENTITY} is not registered in metadata.`);
809
+ }
810
+ const link = await md.GetEntityObject(LINK_ENTITY);
811
+ link.NewRecord();
812
+ link.Set('GLAccountRoleID', draft.RoleID);
813
+ link.Set('GLAccountID', draft.AccountID);
814
+ link.Set('EntityID', productEntity.ID);
815
+ link.Set('RecordID', this.Product.ID);
816
+ link.Set('Status', 'Active');
817
+ link.Set('StartedAt', draft.StartedAt ? new Date(`${draft.StartedAt}T00:00:00.000Z`) : null);
818
+ if (!(await link.Save())) {
819
+ this.WriteError = this.readableError(link) ?? 'the link could not be saved';
820
+ return;
821
+ }
822
+ this.Draft = null;
823
+ await this.Refresh(true);
824
+ }
825
+ catch (err) {
826
+ this.WriteError = err instanceof Error ? err.message : String(err);
827
+ }
828
+ finally {
829
+ this.Saving = false;
830
+ this.tick();
831
+ }
832
+ }
833
+ /**
834
+ * Whether a link can have explained any money yet.
835
+ *
836
+ * `pickActiveLinkIndex` treats a window as applying when `StartedAt <= now <= EndedAt`, so a link
837
+ * that starts TODAY OR LATER has not yet been the answer to any resolution. That is the boundary
838
+ * between the two destructive-looking actions below, and it is deliberately conservative: dated in
839
+ * the past means assume it booked something.
840
+ */
841
+ HasApplied(row) {
842
+ if (!row.StartedAt) {
843
+ return true; // a null start means "always has" — the widest possible claim
844
+ }
845
+ /**
846
+ * PARSED, NOT STRING-SLICED. `StartedAt` reaches this row as a `Date` from the engine cache and
847
+ * as an ISO string from the view fallback, and the first version sliced `String(value)` — which
848
+ * for a Date yields "Wed Jan 01", compares greater than "2026-08-28", and reported a link from
849
+ * 2020 as never having applied. It offered Remove on a link that had been booking for six
850
+ * years. The unit test missed it by passing a string, which is the shape the bug did not have.
851
+ */
852
+ const started = new Date(row.StartedAt);
853
+ if (Number.isNaN(started.getTime())) {
854
+ return true; // unreadable date — assume it applied rather than offer to delete it
855
+ }
856
+ return started.getTime() < new Date(`${this.today()}T00:00:00.000Z`).getTime();
857
+ }
858
+ /**
859
+ * Ends a link's window, which is how accounting retires a mapping that has been in force.
860
+ *
861
+ * ── WHY THIS IS NOT A DELETE ──
862
+ *
863
+ * A GL link explains where money went. Deleting one that has already booked would remove the
864
+ * explanation for journal entries that still exist, and nothing else records which mapping was in
865
+ * force at the time. Ending the window keeps the history and stops the link applying.
866
+ *
867
+ * ── AND WHAT "TODAY" ACTUALLY MEANS HERE ──
868
+ *
869
+ * `EndedAt` is stored at MIDNIGHT UTC, and `pickActiveLinkIndex` excludes a link once `now > ended`.
870
+ * Midnight has already passed by the time anyone clicks, so retiring stops the link IMMEDIATELY
871
+ * rather than at the end of the day. Verified in the browser: a link dated 2020-01-01 retired on
872
+ * 2026-08-28 rendered as Superseded on the spot, not as still in force.
873
+ *
874
+ * This comment previously claimed the opposite — "applies for the whole of today and stops
875
+ * tomorrow" — and the button's tooltip said so too. The behaviour is the defensible one; the
876
+ * description was wrong, and it is the description that changed.
877
+ */
878
+ /**
879
+ * Closes an edit form that is bound to `id`.
880
+ *
881
+ * Retire and Remove used to leave it open, and the template only hides a row's EDIT button while
882
+ * that row is being edited — Retire and Remove stayed live. So the two orderings both corrupted:
883
+ * Retire then Save wrote `EndedAt = null` from the stale form and silently UN-RETIRED the link;
884
+ * Remove then Save threw "could not be re-read" on a record that no longer existed, which reads as
885
+ * a broken screen rather than "that record is gone".
886
+ */
887
+ closeEditFor(id) {
888
+ if (this.Editing && idKey(this.Editing.ID) === idKey(id)) {
889
+ this.Editing = null;
890
+ this.EditAccountOptions = [];
891
+ this.EditRoleOptions = [];
892
+ }
893
+ }
894
+ async RetireLink(row) {
895
+ this.Saving = true;
896
+ this.WriteError = null;
897
+ this.tick();
898
+ try {
899
+ const md = new Metadata();
900
+ const link = await md.GetEntityObject(LINK_ENTITY);
901
+ if (!(await link.InnerLoad(CompositeKey.FromID(row.ID)))) {
902
+ throw new Error(`link ${row.ID} could not be re-read`);
903
+ }
904
+ link.Set('EndedAt', new Date(`${this.today()}T00:00:00.000Z`));
905
+ if (!(await link.Save())) {
906
+ this.WriteError = this.readableError(link) ?? 'the link could not be retired';
907
+ return;
908
+ }
909
+ // The row this form was bound to has just changed underneath it; see closeEditFor.
910
+ this.closeEditFor(row.ID);
911
+ await this.Refresh(true);
912
+ }
913
+ catch (err) {
914
+ this.WriteError = err instanceof Error ? err.message : String(err);
915
+ }
916
+ finally {
917
+ this.Saving = false;
918
+ this.tick();
919
+ }
920
+ }
921
+ /**
922
+ * Deletes a link that cannot have explained anything yet.
923
+ *
924
+ * ── WHY THIS EXISTS DESPITE THE ARGUMENT AGAINST DELETING ──
925
+ *
926
+ * Retiring is right for a mapping that has been in force. It is USELESS for one created by
927
+ * mistake a minute ago: the server refuses `EndedAt` on or before `StartedAt` ("The end date must
928
+ * be after the start date"), so a link mistyped today could be neither retired nor removed. That
929
+ * was a real hole — found by clicking Retire on a link the same test had just created.
930
+ *
931
+ * The boundary is {@link HasApplied}: a link starting today or later has never been the answer to
932
+ * a resolution, so deleting it destroys no explanation of anything. One that started earlier keeps
933
+ * the retire path and cannot be deleted from here at all.
934
+ */
935
+ async RemoveLink(row) {
936
+ if (this.HasApplied(row)) {
937
+ return; // the template does not offer this, and neither does the method
938
+ }
939
+ this.Saving = true;
940
+ this.WriteError = null;
941
+ this.tick();
942
+ try {
943
+ const md = new Metadata();
944
+ const link = await md.GetEntityObject(LINK_ENTITY);
945
+ if (!(await link.InnerLoad(CompositeKey.FromID(row.ID)))) {
946
+ throw new Error(`link ${row.ID} could not be re-read`);
947
+ }
948
+ if (!(await link.Delete())) {
949
+ this.WriteError = this.readableError(link) ?? 'the link could not be removed';
950
+ return;
951
+ }
952
+ // The row this form was bound to has just changed underneath it; see closeEditFor.
953
+ this.closeEditFor(row.ID);
954
+ await this.Refresh(true);
955
+ }
956
+ catch (err) {
957
+ this.WriteError = err instanceof Error ? err.message : String(err);
958
+ }
959
+ finally {
960
+ this.Saving = false;
961
+ this.tick();
962
+ }
963
+ }
964
+ /**
965
+ * The sentence a human should read out of a refused save.
966
+ *
967
+ * `CompleteMessage` can be a JSON blob of validation results — the retire failure arrived as
968
+ * `{"Source":"EndedAt","Message":"The end date must be after the start date.",...}`, and showing
969
+ * that verbatim buries the one sentence that matters inside punctuation. This pulls the `Message`
970
+ * out when it is there and falls back to the raw text when it is not, so nothing is ever swallowed.
971
+ */
972
+ readableError(entity) {
973
+ const raw = entity.LatestResult?.CompleteMessage;
974
+ if (!raw) {
975
+ return null;
976
+ }
977
+ try {
978
+ const parsed = JSON.parse(raw);
979
+ const messages = (Array.isArray(parsed) ? parsed : [parsed])
980
+ .map((p) => p?.Message)
981
+ .filter((m) => !!m);
982
+ if (messages.length) {
983
+ return messages.join(' ');
984
+ }
985
+ }
986
+ catch {
987
+ /* not JSON — the raw text is already the message */
988
+ }
989
+ return raw;
990
+ }
991
+ /** Today in UTC as `yyyy-MM-dd` — the same zone the window is stored and rendered in. */
992
+ today() {
993
+ const n = new Date();
994
+ return `${n.getUTCFullYear()}-${String(n.getUTCMonth() + 1).padStart(2, '0')}-${String(n.getUTCDate()).padStart(2, '0')}`;
995
+ }
996
+ async load() {
997
+ const productID = this.Product?.ID;
998
+ if (!productID) {
999
+ return [];
1000
+ }
1001
+ const md = new Metadata();
1002
+ /**
1003
+ * ORDERS MAY BE INSTALLED WITHOUT ACCOUNTING, and that is a supported state rather than an
1004
+ * error. Asking `RunView` for an unregistered entity does not return `Success: false` — it logs
1005
+ * `Entity ... not found in metadata`, and a console error is treated as a broken screen by the
1006
+ * Playwright keystone. So the entity is checked in metadata FIRST and its absence simply means
1007
+ * no panel, the same guard sales' product picker uses for this app.
1008
+ */
1009
+ const linkEntity = md.Entities.find((e) => e.Name === LINK_ENTITY);
1010
+ if (!linkEntity) {
1011
+ return [];
1012
+ }
1013
+ const productEntity = md.Entities.find((e) => e.Name === PRODUCT_ENTITY);
1014
+ if (!productEntity) {
1015
+ throw new Error(`${PRODUCT_ENTITY} is not registered in metadata, so its links cannot be identified.`);
1016
+ }
1017
+ // `new Metadata().CurrentUser` is how this repo reaches the user client-side; `ProviderToUse`
1018
+ // is a component-level property and does not exist on Metadata.
1019
+ await AccountingEngineBase.Instance.Config(false, md.CurrentUser);
1020
+ const engine = AccountingEngineBase.Instance;
1021
+ /**
1022
+ * The pickers come from the SAME engine cache the reader uses, so a role or account offered here
1023
+ * is one resolution can actually see.
1024
+ *
1025
+ * ── ACCOUNTS ARE SCOPED TO THIS PRODUCT'S COMPANY, AND THE REASON IS NOT COSMETIC ──────────
1026
+ *
1027
+ * This used to offer every GL account in the system, on the reasoning that a link may
1028
+ * legitimately point at another company's account because the resolver disambiguates. Reading
1029
+ * the booking path settles it the other way.
1030
+ *
1031
+ * `GLAccountResolver.Resolve` passes the ORDER LINE's company down as `expectedCompanyID`
1032
+ * ("the resolved account must belong to it"), and `AccountingBridge` uses it to filter
1033
+ * candidates by the ACCOUNT's company before `pickActiveLinkIndex` ever runs. So a link to
1034
+ * another company's account is not a link that books to the wrong company — it is a link the
1035
+ * booking path SKIPS, falling through to the category, the product type, and finally the
1036
+ * company default.
1037
+ *
1038
+ * That is the worse outcome of the two. A wrong posting is at least visible in the ledger;
1039
+ * dead configuration looks exactly like configuration that works, and stays wrong until
1040
+ * someone reconciles. Offering only accounts that can actually be reached means the panel
1041
+ * cannot create one.
1042
+ *
1043
+ * ONE CORRECTION TO THE ABOVE, because an earlier version of this comment stated only half of
1044
+ * it: skipping is not always silent. When the walk finds nothing at the category, the product
1045
+ * type or the company default either, `GLAccountResolver.Resolve` THROWS and the order confirm
1046
+ * hard-fails naming the role. So a dead link produces either a silent fallback or a loud
1047
+ * refusal, depending on what else is configured. The scoping is right in both cases; only the
1048
+ * "looks like it works" half of the argument is specific to the first.
1049
+ *
1050
+ * The label still carries code, name and company. Codes repeat across companies, and after
1051
+ * scoping they no longer collide here — but the label is also what a reader checks the row
1052
+ * against, so it keeps saying which company it belongs to.
1053
+ */
1054
+ this.Roles = engine.GLAccountRoles
1055
+ .map((r) => ({ ID: idKey(r.ID), Name: String(r.Name ?? '(unnamed role)') }))
1056
+ .sort((a, b) => a.Name.localeCompare(b.Name));
1057
+ /**
1058
+ * `CompanyID` and `IsActive` are read WITHOUT a cast, deliberately.
1059
+ *
1060
+ * They used to be cast through `as unknown`, which was both unnecessary — they are real typed
1061
+ * accessors on the entity — and precisely backwards: the two fields that decide whether an
1062
+ * account is offered were the two the compiler could not check, while `Code` and `Name`, which
1063
+ * only decide the label, were checked. A rename would have emptied the picker silently instead
1064
+ * of failing the build.
1065
+ *
1066
+ * `Company` keeps its cast because it genuinely is a virtual field, absent from the entity type.
1067
+ */
1068
+ this.Accounts = engine.GLAccounts
1069
+ .filter((a) => AccountIsOfferable(a.CompanyID, this.Product?.CompanyID) && a.IsActive !== false)
1070
+ .map((a) => {
1071
+ const company = a.Company;
1072
+ return {
1073
+ ID: idKey(a.ID),
1074
+ Label: `${a.Code ?? '(no code)'} ${a.Name ?? ''}${company ? ` — ${company}` : ''}`.trim(),
1075
+ };
1076
+ })
1077
+ .sort((a, b) => a.Label.localeCompare(b.Label));
1078
+ /**
1079
+ * READ THROUGH THE ENGINE, fall back to a view only if the cache has nothing. The engine is the
1080
+ * booking pipeline's own source; the view is here so a stale or unconfigured cache shows the
1081
+ * truth rather than an empty list that reads as "no accounts assigned".
1082
+ */
1083
+ const cached = engine.GLAccountLinks.filter((l) => String(l.EntityID).toLowerCase() === String(productEntity.ID).toLowerCase()
1084
+ && String(l.RecordID).toLowerCase() === String(productID).toLowerCase());
1085
+ const rows = cached.length ? cached : await this.readDirect(productEntity.ID, productID);
1086
+ const inForce = this.inForceIDs(rows);
1087
+ return rows
1088
+ .map((l) => {
1089
+ const account = engine.GLAccountByID(String(l.GLAccountID));
1090
+ const role = engine.GLAccountRoles.find((r) => String(r.ID).toLowerCase() === String(l.GLAccountRoleID).toLowerCase());
1091
+ return {
1092
+ ID: String(l.ID),
1093
+ // Normalised, because these are matched against option values with `Object.is`.
1094
+ RoleID: idKey(l.GLAccountRoleID),
1095
+ AccountID: idKey(l.GLAccountID),
1096
+ RoleName: role?.Name ?? '(unknown role)',
1097
+ AccountCode: account?.Code ?? '(unknown)',
1098
+ AccountName: account?.Name ?? '',
1099
+ Status: String(l.Status ?? ''),
1100
+ StartedAt: l.StartedAt ?? null,
1101
+ EndedAt: l.EndedAt ?? null,
1102
+ Active: inForce.has(String(l.ID).toLowerCase()),
1103
+ };
1104
+ })
1105
+ .sort((a, b) => a.RoleName.localeCompare(b.RoleName) || a.AccountCode.localeCompare(b.AccountCode));
1106
+ }
1107
+ /** The view read, used only when the engine cache holds nothing for this product. */
1108
+ async readDirect(entityID, recordID) {
1109
+ /**
1110
+ * Refused rather than sanitised. Both values are UUIDs by construction — one is an entity id
1111
+ * from metadata, the other the product's primary key — so anything that is not UUID-shaped is a
1112
+ * programming error, and returning it as an empty result would hide that behind a panel reading
1113
+ * "no GL accounts are linked".
1114
+ */
1115
+ if (!UUID_PATTERN.test(entityID) || !UUID_PATTERN.test(recordID)) {
1116
+ throw new Error('the GL account links could not be read: the product could not be identified');
1117
+ }
1118
+ const rv = new RunView();
1119
+ const res = await rv.RunView({
1120
+ EntityName: LINK_ENTITY,
1121
+ ExtraFilter: `EntityID = '${entityID}' AND RecordID = '${recordID}'`,
1122
+ ResultType: 'simple',
1123
+ Fields: ['ID', 'GLAccountID', 'GLAccountRoleID', 'Status', 'StartedAt', 'EndedAt'],
1124
+ });
1125
+ if (!res?.Success) {
1126
+ throw new Error(res?.ErrorMessage ?? 'the GL account links could not be read');
1127
+ }
1128
+ // The one honest cast in this file: a view row arrives untyped, and this is the boundary where
1129
+ // it becomes the shape the panel reads. Every field is then narrowed with String()/?? below.
1130
+ return (res.Results ?? []);
1131
+ }
1132
+ /**
1133
+ * The ids of the links actually IN FORCE today — one per role at most, decided by accounting.
1134
+ *
1135
+ * ── WHY THIS IS NOT A WINDOW TEST ──
1136
+ *
1137
+ * The obvious implementation, and the one this replaced, asks of each link "is it Active and does
1138
+ * today fall inside its window?" and marks every link that passes. That is wrong whenever a role
1139
+ * has more than one qualifying link — a mapping being superseded while the old row is still open,
1140
+ * which is exactly what an accountant does when they re-point a product mid-year. Both rows pass
1141
+ * the window test, so the panel would show TWO active accounts for one role while an order books
1142
+ * through exactly one of them. A screen that disagrees with the booking about where money lands is
1143
+ * worse than no screen.
1144
+ *
1145
+ * `pickActiveLinkIndex` is accounting's own picker — the pure function behind
1146
+ * `ResolveLinkedAccount`, exported by that package for precisely this kind of reuse. It applies the
1147
+ * window AND the tie-break this panel has no business restating: latest `StartedAt` wins, and a
1148
+ * null `StartedAt` loses to any dated one. Calling it means "active" here can only ever mean what
1149
+ * the booking pipeline means.
1150
+ *
1151
+ * Grouped by ROLE because that is the unit the picker resolves: a product legitimately has a
1152
+ * revenue link and a deferred-revenue link at once, and they do not compete.
1153
+ */
1154
+ inForceIDs(rows) {
1155
+ const byRole = new Map();
1156
+ for (const link of rows) {
1157
+ const role = String(link.GLAccountRoleID ?? '').toLowerCase();
1158
+ const group = byRole.get(role);
1159
+ if (group) {
1160
+ group.push(link);
1161
+ }
1162
+ else {
1163
+ byRole.set(role, [link]);
1164
+ }
1165
+ }
1166
+ // One instant for every role, so a slow load cannot resolve two roles against two "nows".
1167
+ const asOf = new Date();
1168
+ const winners = new Set();
1169
+ for (const group of byRole.values()) {
1170
+ const index = pickActiveLinkIndex(
1171
+ // The cast is the boundary between a database string and accounting's closed vocabulary
1172
+ // (`Active | Disabled | Pending`). It is safe in the only direction that matters: a value
1173
+ // outside that set simply is not `'Active'`, so the picker skips it rather than
1174
+ // mis-classifying it as in force.
1175
+ group.map((l) => ({
1176
+ Status: String(l.Status ?? ''),
1177
+ StartedAt: l.StartedAt ?? null,
1178
+ EndedAt: l.EndedAt ?? null,
1179
+ })), asOf);
1180
+ if (index >= 0) {
1181
+ winners.add(String(group[index].ID).toLowerCase());
1182
+ }
1183
+ }
1184
+ return winners;
1185
+ }
1186
+ static ɵfac = function BizAppsProductGLLinksComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || BizAppsProductGLLinksComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
1187
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: BizAppsProductGLLinksComponent, selectors: [["bizapps-product-gl-links"]], inputs: { Product: "Product", EditMode: "EditMode", FormContext: "FormContext" }, outputs: { Navigate: "Navigate" }, standalone: false, decls: 16, vars: 5, consts: [[1, "mjo-gl-links"], [1, "mjo-gl-links__head"], [1, "mjo-gl-links__title"], [1, "fa-solid", "fa-link"], [1, "mjo-gl-links__actions"], ["type", "button", "title", "Link a GL account to this product", 1, "mjo-gl-links__refresh"], ["type", "button", "title", "Re-read this product's GL account links", 1, "mjo-gl-links__refresh", 3, "click"], [1, "fa-solid", "fa-rotate"], [1, "mjo-gl-links__error"], [1, "mjo-gl-links__draft"], [1, "mjo-gl-links__note"], [1, "mjo-gl-links__scroll"], ["type", "button", "title", "Link a GL account to this product", 1, "mjo-gl-links__refresh", 3, "click"], [1, "fa-solid", "fa-plus"], [1, "fa-solid", "fa-triangle-exclamation"], ["aria-label", "Role", 3, "ngModelChange", "ngModel", "disabled"], ["value", ""], [3, "value"], ["aria-label", "GL account", 3, "ngModelChange", "ngModel", "disabled"], ["type", "date", "aria-label", "In force from", 3, "ngModelChange", "ngModel", "disabled"], ["type", "button", 1, "mjo-gl-links__btn", 3, "click", "disabled"], [3, "value", "disabled"], [1, "mjo-gl-links__lbl"], ["type", "date", "aria-label", "In force to", 3, "ngModelChange", "ngModel", "disabled"], [1, "mjo-gl-links__table"], [1, "mjo-gl-links__sr"], [3, "mjo-gl-links__row--inactive"], ["type", "button", 1, "mjo-gl-links__link", 3, "click"], [1, "mjo-gl-links__name"], [1, "mjo-gl-links__chip"], ["type", "button", "title", "Edit this link", 1, "mjo-gl-links__btn", 3, "disabled"], ["type", "button", "title", "End this link's window as of today. It stops applying immediately, and the record stays on the product as history.", 1, "mjo-gl-links__btn", 3, "disabled"], ["type", "button", 1, "mjo-gl-links__btn", 3, "disabled", "title"], ["type", "button", "title", "Edit this link", 1, "mjo-gl-links__btn", 3, "click", "disabled"], ["type", "button", "title", "End this link's window as of today. It stops applying immediately, and the record stays on the product as history.", 1, "mjo-gl-links__btn", 3, "click", "disabled"], ["type", "button", 1, "mjo-gl-links__btn", 3, "click", "disabled", "title"]], template: function BizAppsProductGLLinksComponent_Template(rf, ctx) { if (rf & 1) {
1188
+ i0.ɵɵelementStart(0, "div", 0)(1, "div", 1)(2, "span", 2);
1189
+ i0.ɵɵelement(3, "i", 3);
1190
+ i0.ɵɵtext(4, " GL Accounts ");
1191
+ i0.ɵɵelementEnd();
1192
+ i0.ɵɵelementStart(5, "span", 4);
1193
+ i0.ɵɵconditionalCreate(6, BizAppsProductGLLinksComponent_Conditional_6_Template, 2, 0, "button", 5);
1194
+ i0.ɵɵelementStart(7, "button", 6);
1195
+ i0.ɵɵlistener("click", function BizAppsProductGLLinksComponent_Template_button_click_7_listener() { return ctx.Refresh(); });
1196
+ i0.ɵɵelement(8, "i", 7);
1197
+ i0.ɵɵelementEnd()()();
1198
+ i0.ɵɵconditionalCreate(9, BizAppsProductGLLinksComponent_Conditional_9_Template, 3, 1, "p", 8);
1199
+ i0.ɵɵconditionalCreate(10, BizAppsProductGLLinksComponent_Conditional_10_Template, 17, 10, "div", 9);
1200
+ i0.ɵɵconditionalCreate(11, BizAppsProductGLLinksComponent_Conditional_11_Template, 18, 12, "div", 9);
1201
+ i0.ɵɵconditionalCreate(12, BizAppsProductGLLinksComponent_Conditional_12_Template, 2, 0, "p", 10)(13, BizAppsProductGLLinksComponent_Conditional_13_Template, 3, 1, "p", 8)(14, BizAppsProductGLLinksComponent_Conditional_14_Template, 2, 0, "p", 10)(15, BizAppsProductGLLinksComponent_Conditional_15_Template, 20, 0, "div", 11);
1202
+ i0.ɵɵelementEnd();
1203
+ } if (rf & 2) {
1204
+ i0.ɵɵadvance(6);
1205
+ i0.ɵɵconditional(ctx.CanWrite && !ctx.Draft ? 6 : -1);
1206
+ i0.ɵɵadvance(3);
1207
+ i0.ɵɵconditional(ctx.WriteError ? 9 : -1);
1208
+ i0.ɵɵadvance();
1209
+ i0.ɵɵconditional(ctx.Draft ? 10 : -1);
1210
+ i0.ɵɵadvance();
1211
+ i0.ɵɵconditional(ctx.Editing ? 11 : -1);
1212
+ i0.ɵɵadvance();
1213
+ i0.ɵɵconditional(ctx.Loading ? 12 : ctx.LoadError ? 13 : !ctx.Rows.length ? 14 : 15);
1214
+ } }, dependencies: [i1.NgSelectOption, i1.ɵNgSelectMultipleOption, i1.DefaultValueAccessor, i1.SelectControlValueAccessor, i1.NgControlStatus, i1.NgModel, i2.DatePipe], styles: ["\n\n\n\n\n\n\n.mjo-gl-links[_ngcontent-%COMP%] {\n margin-top: 12px;\n}\n\n.mjo-gl-links__head[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n margin-bottom: 6px;\n}\n\n.mjo-gl-links__title[_ngcontent-%COMP%] {\n font-size: 12px;\n font-weight: 600;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n opacity: 0.75;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.mjo-gl-links__refresh[_ngcontent-%COMP%] {\n background: none;\n border: none;\n cursor: pointer;\n padding: 2px 6px;\n opacity: 0.6;\n color: inherit;\n}\n\n.mjo-gl-links__refresh[_ngcontent-%COMP%]:hover {\n opacity: 1;\n}\n\n.mjo-gl-links__refresh[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid currentColor;\n outline-offset: 2px;\n border-radius: 3px;\n}\n\n.mjo-gl-links__note[_ngcontent-%COMP%] {\n font-size: 13px;\n opacity: 0.7;\n margin: 4px 0;\n max-width: 70ch;\n}\n\n.mjo-gl-links__error[_ngcontent-%COMP%] {\n font-size: 13px;\n margin: 4px 0;\n display: flex;\n align-items: flex-start;\n gap: 6px;\n color: #b3452c;\n}\n\n\n\n.mjo-gl-links__scroll[_ngcontent-%COMP%] {\n overflow-x: auto;\n}\n\n.mjo-gl-links__table[_ngcontent-%COMP%] {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n font-variant-numeric: tabular-nums;\n min-width: 460px;\n}\n\n.mjo-gl-links__table[_ngcontent-%COMP%] th[_ngcontent-%COMP%] {\n text-align: left;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n opacity: 0.6;\n padding: 4px 10px 4px 0;\n border-bottom: 1px solid currentColor;\n border-bottom-color: color-mix(in srgb, currentColor 20%, transparent);\n white-space: nowrap;\n}\n\n.mjo-gl-links__table[_ngcontent-%COMP%] td[_ngcontent-%COMP%] {\n padding: 6px 10px 6px 0;\n border-bottom: 1px solid currentColor;\n border-bottom-color: color-mix(in srgb, currentColor 10%, transparent);\n vertical-align: top;\n}\n\n\n\n.mjo-gl-links__row--inactive[_ngcontent-%COMP%] {\n opacity: 0.55;\n}\n\n.mjo-gl-links__link[_ngcontent-%COMP%] {\n background: none;\n border: none;\n padding: 0;\n cursor: pointer;\n font: inherit;\n color: inherit;\n text-align: left;\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n.mjo-gl-links__link[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid currentColor;\n outline-offset: 2px;\n border-radius: 2px;\n}\n\n.mjo-gl-links__name[_ngcontent-%COMP%] {\n opacity: 0.7;\n}\n\n.mjo-gl-links__chip[_ngcontent-%COMP%] {\n display: inline-block;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.04em;\n padding: 1px 7px;\n border-radius: 2px;\n border: 1px solid currentColor;\n border-color: color-mix(in srgb, currentColor 30%, transparent);\n white-space: nowrap;\n}\n\n.mjo-gl-links__chip--on[_ngcontent-%COMP%] {\n color: #1f6f5c;\n border-color: color-mix(in srgb, #1f6f5c 45%, transparent);\n}\n\n\n\n\n.mjo-gl-links__actions[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n}\n\n\n\n\n\n\n.mjo-gl-links__draft[_ngcontent-%COMP%] {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n margin: 6px 0 10px;\n padding: 8px;\n border: 1px solid currentColor;\n border-color: color-mix(in srgb, currentColor 18%, transparent);\n border-radius: 4px;\n}\n\n.mjo-gl-links__draft[_ngcontent-%COMP%] select[_ngcontent-%COMP%], \n.mjo-gl-links__draft[_ngcontent-%COMP%] input[_ngcontent-%COMP%] {\n font: inherit;\n font-size: 13px;\n padding: 3px 5px;\n border: 1px solid currentColor;\n border-color: color-mix(in srgb, currentColor 28%, transparent);\n border-radius: 3px;\n background: transparent;\n color: inherit;\n max-width: 100%;\n}\n\n.mjo-gl-links__btn[_ngcontent-%COMP%] {\n font: inherit;\n font-size: 12px;\n padding: 3px 10px;\n border: 1px solid currentColor;\n border-color: color-mix(in srgb, currentColor 30%, transparent);\n border-radius: 3px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n}\n\n.mjo-gl-links__btn[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: color-mix(in srgb, currentColor 8%, transparent);\n}\n\n.mjo-gl-links__btn[_ngcontent-%COMP%]:disabled {\n opacity: 0.45;\n cursor: default;\n}\n\n.mjo-gl-links__btn[_ngcontent-%COMP%]:focus-visible, \n.mjo-gl-links__draft[_ngcontent-%COMP%] select[_ngcontent-%COMP%]:focus-visible, \n.mjo-gl-links__draft[_ngcontent-%COMP%] input[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid currentColor;\n outline-offset: 2px;\n}\n\n\n\n.mjo-gl-links__sr[_ngcontent-%COMP%] {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n\n\n.mjo-gl-links__lbl[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 12px;\n opacity: 0.8;\n}\n\n.mjo-gl-links__draft[_ngcontent-%COMP%] .mjo-gl-links__note[_ngcontent-%COMP%] {\n flex-basis: 100%;\n margin: 2px 0 0;\n}\n\n.mjo-gl-links__draft[_ngcontent-%COMP%] select[_ngcontent-%COMP%]:disabled, \n.mjo-gl-links__draft[_ngcontent-%COMP%] input[_ngcontent-%COMP%]:disabled {\n opacity: 0.55;\n}"] });
1215
+ }
1216
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(BizAppsProductGLLinksComponent, [{
1217
+ type: Component,
1218
+ args: [{ standalone: false, selector: 'bizapps-product-gl-links', template: "<div class=\"mjo-gl-links\">\n <div class=\"mjo-gl-links__head\">\n <span class=\"mjo-gl-links__title\">\n <i class=\"fa-solid fa-link\"></i>\n GL Accounts\n </span>\n <span class=\"mjo-gl-links__actions\">\n <!--\n ADD is offered only on a SAVED product. A link stores the product's primary key in\n RecordID, so there is nothing to point at until the record exists \u2014 the same reason\n sales blocks Add line on an unsaved deal rather than failing at save time.\n -->\n @if (CanWrite && !Draft) {\n <button\n type=\"button\"\n class=\"mjo-gl-links__refresh\"\n title=\"Link a GL account to this product\"\n (click)=\"OpenDraft()\">\n <i class=\"fa-solid fa-plus\"></i>\n </button>\n }\n <button\n type=\"button\"\n class=\"mjo-gl-links__refresh\"\n title=\"Re-read this product's GL account links\"\n (click)=\"Refresh()\">\n <i class=\"fa-solid fa-rotate\"></i>\n </button>\n </span>\n </div>\n\n <!--\n A REFUSED WRITE IS SHOWN VERBATIM. GLAccountLinkEntityServer explains why it refused \u2014 usually\n that an Active link already exists for this role, same company, same start date \u2014 and that\n sentence is what teaches the reader to supersede with a LATER date instead. Replacing it with a\n friendlier summary would remove the only part that says what to do next.\n -->\n @if (WriteError) {\n <p class=\"mjo-gl-links__error\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i>\n {{ WriteError }}\n </p>\n }\n\n @if (Draft) {\n <div class=\"mjo-gl-links__draft\">\n <select [(ngModel)]=\"Draft.RoleID\" [disabled]=\"Saving\" aria-label=\"Role\">\n <option value=\"\">\u2014 role \u2014</option>\n @for (r of Roles; track r.ID) {\n <option [value]=\"r.ID\">{{ r.Name }}</option>\n }\n </select>\n @if (Accounts.length === 0) {\n <!--\n An empty picker and \"no accounts exist\" look identical to a reader and mean opposite\n things \u2014 the same distinction the load-error message below is careful about. Scoping\n to the product's company made this reachable: a company whose chart has not been\n seeded yields nothing, and without this the form renders a permanently disabled Link\n button with no explanation.\n -->\n <span class=\"mjo-gl-links__note\">\n No GL accounts exist for this product's company yet, so there is nothing to link to.\n They are created in Accounting.\n </span>\n }\n <select [(ngModel)]=\"Draft.AccountID\" [disabled]=\"Saving\" aria-label=\"GL account\">\n <option value=\"\">\u2014 account \u2014</option>\n @for (a of Accounts; track a.ID) {\n <option [value]=\"a.ID\">{{ a.Label }}</option>\n }\n </select>\n <!-- Starts today by default; a LATER date is how an existing mapping is superseded. -->\n <input type=\"date\" [(ngModel)]=\"Draft.StartedAt\" [disabled]=\"Saving\" aria-label=\"In force from\" />\n <button type=\"button\" class=\"mjo-gl-links__btn\" [disabled]=\"Saving || !Draft.RoleID || !Draft.AccountID\"\n (click)=\"AddLink()\">{{ Saving ? 'Linking\u2026' : 'Link' }}</button>\n <button type=\"button\" class=\"mjo-gl-links__btn\" [disabled]=\"Saving\" (click)=\"CancelDraft()\">Cancel</button>\n </div>\n }\n\n @if (Editing) {\n <div class=\"mjo-gl-links__draft\">\n <!--\n On a link that has ALREADY applied, role / account / start are disabled rather than\n hidden: seeing what the mapping is, and that it cannot be changed here, is the point.\n Hiding them would read as \"this link has no account\", which is the confusion #112 was.\n -->\n <select [(ngModel)]=\"Editing.RoleID\" [disabled]=\"Saving || Editing.Applied\" aria-label=\"Role\">\n @for (r of EditRoles; track r.ID) {\n <option [value]=\"r.ID\" [disabled]=\"r.Disabled === true\">{{ r.Label }}</option>\n }\n </select>\n <select [(ngModel)]=\"Editing.AccountID\" [disabled]=\"Saving || Editing.Applied\" aria-label=\"GL account\">\n @for (a of EditAccounts; track a.ID) {\n <!--\n A disabled option is the link's CURRENT account when this product's company\n cannot use it. Shown so the picker never reads as \"this link has no account\";\n unselectable so the repair path cannot write the unreachable link back.\n -->\n <option [value]=\"a.ID\" [disabled]=\"a.Disabled === true\">{{ a.Label }}</option>\n }\n </select>\n <label class=\"mjo-gl-links__lbl\">from\n <input type=\"date\" [(ngModel)]=\"Editing.StartedAt\"\n [disabled]=\"Saving || Editing.Applied\" aria-label=\"In force from\" />\n </label>\n <!-- Always editable, and clearable: emptying it undoes a retire applied by mistake. -->\n <label class=\"mjo-gl-links__lbl\">to\n <input type=\"date\" [(ngModel)]=\"Editing.EndedAt\" [disabled]=\"Saving\" aria-label=\"In force to\" />\n </label>\n <button type=\"button\" class=\"mjo-gl-links__btn\" [disabled]=\"Saving\"\n (click)=\"SaveEdit()\">{{ Saving ? 'Saving\u2026' : 'Save' }}</button>\n <button type=\"button\" class=\"mjo-gl-links__btn\" [disabled]=\"Saving\" (click)=\"CancelEdit()\">Cancel</button>\n @if (Editing.Applied) {\n <span class=\"mjo-gl-links__note\">\n This link has been in force, so only its end date can change here. To book a\n different account, add a link with a later start date.\n </span>\n }\n </div>\n }\n\n @if (Loading) {\n <p class=\"mjo-gl-links__note\">Loading\u2026</p>\n } @else if (LoadError) {\n <!--\n An error is shown INSTEAD of the empty state, never alongside it. \"No accounts assigned\" and\n \"the accounts could not be read\" look identical to a reader and mean opposite things \u2014 one\n is a setup task, the other is a broken screen.\n -->\n <p class=\"mjo-gl-links__error\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i>\n The GL account links could not be read: {{ LoadError }}\n </p>\n } @else if (!Rows.length) {\n <p class=\"mjo-gl-links__note\">\n No GL accounts are linked directly to this product. It may still book through its category\n or the company default \u2014 open Accounting \u2192 Account Links to see the full chain.\n </p>\n } @else {\n <div class=\"mjo-gl-links__scroll\">\n <table class=\"mjo-gl-links__table\">\n <thead>\n <tr>\n <th>Role</th>\n <th>Account</th>\n <th>Status</th>\n <!--\n RENDERED IN UTC, EXPLICITLY. Angular's date pipe formats in the browser's LOCAL\n zone by default, and StartedAt/EndedAt are datetimeoffset stored as UTC midnight \u2014\n so west of Greenwich a link starting 2026-01-01 renders as 2025-12-31. Observed\n exactly that on this panel before the timezone argument was added. Sales'\n deal-workspace.dates.ts records the same hazard: \"a stored 20 November renders\n as the 19th\".\n -->\n <th>From</th>\n <th>To</th>\n <th><span class=\"mjo-gl-links__sr\">Row actions</span></th>\n </tr>\n </thead>\n <tbody>\n @for (row of Rows; track row.ID) {\n <tr [class.mjo-gl-links__row--inactive]=\"!row.Active\">\n <td>{{ row.RoleName }}</td>\n <td>\n <button type=\"button\" class=\"mjo-gl-links__link\" (click)=\"OpenLink(row)\">\n {{ row.AccountCode }}<span class=\"mjo-gl-links__name\"> {{ row.AccountName }}</span>\n </button>\n </td>\n <td>\n <span\n class=\"mjo-gl-links__chip\"\n [class.mjo-gl-links__chip--on]=\"row.Active\">\n {{ StatusLabel(row) }}\n </span>\n </td>\n <td>{{ row.StartedAt ? (row.StartedAt | date: 'yyyy-MM-dd' : 'UTC') : '\u2014' }}</td>\n <td>{{ row.EndedAt ? (row.EndedAt | date: 'yyyy-MM-dd' : 'UTC') : '\u2014' }}</td>\n <td>\n <!--\n Retire, not delete \u2014 the link explains where money went, and ending\n its window stops it applying while keeping the record. Offered only\n on the link actually IN FORCE: retiring one already superseded or\n ended changes nothing a reader can see, so the control would be a\n button that appears to do nothing.\n -->\n <!--\n EDIT is offered on every link, but it does not mean the same thing\n on both sides of HasApplied \u2014 see OpenEdit. On a link that has\n booked, only the end date opens; the rest is history.\n -->\n @if (CanWrite && Editing?.ID !== row.ID) {\n <button type=\"button\" class=\"mjo-gl-links__btn\" [disabled]=\"Saving\"\n title=\"Edit this link\"\n (click)=\"OpenEdit(row)\">Edit</button>\n }\n @if (CanWrite && row.Active && HasApplied(row)) {\n <button type=\"button\" class=\"mjo-gl-links__btn\" [disabled]=\"Saving\"\n title=\"End this link's window as of today. It stops applying immediately, and the record stays on the product as history.\"\n (click)=\"RetireLink(row)\">Retire</button>\n }\n <!--\n Remove exists because retiring is impossible for a link that starts\n today: CK_GLAccountLink_Window enforces EndedAt > StartedAt, so a\n mistake made today has no other undo.\n\n The WARNING is conditional, because the two cases are not the same. A\n future-dated link has explained nothing. A link dated TODAY is already\n in force \u2014 pickActiveLinkIndex includes t === started \u2014 so orders\n booked today may have resolved through it. The button used to claim\n otherwise on both.\n -->\n @if (CanWrite && !HasApplied(row)) {\n <button type=\"button\" class=\"mjo-gl-links__btn\" [disabled]=\"Saving\"\n [title]=\"RemoveWarning(row)\"\n (click)=\"RemoveLink(row)\">Remove</button>\n }\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n</div>\n", styles: ["/*\n * Deliberately minimal, and deliberately not a new visual language. This sits inside the Accounting\n * panel beside `mj-form-field` rows, so it borrows the host's type and colour rather than introducing\n * its own \u2014 a table that looks like a different product is the tell of a bolted-on feature.\n */\n\n.mjo-gl-links {\n margin-top: 12px;\n}\n\n.mjo-gl-links__head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n margin-bottom: 6px;\n}\n\n.mjo-gl-links__title {\n font-size: 12px;\n font-weight: 600;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n opacity: 0.75;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.mjo-gl-links__refresh {\n background: none;\n border: none;\n cursor: pointer;\n padding: 2px 6px;\n opacity: 0.6;\n color: inherit;\n}\n\n.mjo-gl-links__refresh:hover {\n opacity: 1;\n}\n\n.mjo-gl-links__refresh:focus-visible {\n outline: 2px solid currentColor;\n outline-offset: 2px;\n border-radius: 3px;\n}\n\n.mjo-gl-links__note {\n font-size: 13px;\n opacity: 0.7;\n margin: 4px 0;\n max-width: 70ch;\n}\n\n.mjo-gl-links__error {\n font-size: 13px;\n margin: 4px 0;\n display: flex;\n align-items: flex-start;\n gap: 6px;\n color: #b3452c;\n}\n\n/* Wide content scrolls inside its own container so the form never scrolls sideways. */\n.mjo-gl-links__scroll {\n overflow-x: auto;\n}\n\n.mjo-gl-links__table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n font-variant-numeric: tabular-nums;\n min-width: 460px;\n}\n\n.mjo-gl-links__table th {\n text-align: left;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n opacity: 0.6;\n padding: 4px 10px 4px 0;\n border-bottom: 1px solid currentColor;\n border-bottom-color: color-mix(in srgb, currentColor 20%, transparent);\n white-space: nowrap;\n}\n\n.mjo-gl-links__table td {\n padding: 6px 10px 6px 0;\n border-bottom: 1px solid currentColor;\n border-bottom-color: color-mix(in srgb, currentColor 10%, transparent);\n vertical-align: top;\n}\n\n/* A retired or not-yet-started link stays visible but reads as history, not as current mapping. */\n.mjo-gl-links__row--inactive {\n opacity: 0.55;\n}\n\n.mjo-gl-links__link {\n background: none;\n border: none;\n padding: 0;\n cursor: pointer;\n font: inherit;\n color: inherit;\n text-align: left;\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n.mjo-gl-links__link:focus-visible {\n outline: 2px solid currentColor;\n outline-offset: 2px;\n border-radius: 2px;\n}\n\n.mjo-gl-links__name {\n opacity: 0.7;\n}\n\n.mjo-gl-links__chip {\n display: inline-block;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.04em;\n padding: 1px 7px;\n border-radius: 2px;\n border: 1px solid currentColor;\n border-color: color-mix(in srgb, currentColor 30%, transparent);\n white-space: nowrap;\n}\n\n.mjo-gl-links__chip--on {\n color: #1f6f5c;\n border-color: color-mix(in srgb, #1f6f5c 45%, transparent);\n}\n\n/* \u2500\u2500 Writing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mjo-gl-links__actions {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n}\n\n/*\n * The add row wraps rather than scrolling: it sits inside a form panel whose width is not ours to\n * assume, and a horizontally-scrolling control strip hides the confirm button on a narrow pane.\n */\n.mjo-gl-links__draft {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n margin: 6px 0 10px;\n padding: 8px;\n border: 1px solid currentColor;\n border-color: color-mix(in srgb, currentColor 18%, transparent);\n border-radius: 4px;\n}\n\n.mjo-gl-links__draft select,\n.mjo-gl-links__draft input {\n font: inherit;\n font-size: 13px;\n padding: 3px 5px;\n border: 1px solid currentColor;\n border-color: color-mix(in srgb, currentColor 28%, transparent);\n border-radius: 3px;\n background: transparent;\n color: inherit;\n max-width: 100%;\n}\n\n.mjo-gl-links__btn {\n font: inherit;\n font-size: 12px;\n padding: 3px 10px;\n border: 1px solid currentColor;\n border-color: color-mix(in srgb, currentColor 30%, transparent);\n border-radius: 3px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n}\n\n.mjo-gl-links__btn:hover:not(:disabled) {\n background: color-mix(in srgb, currentColor 8%, transparent);\n}\n\n.mjo-gl-links__btn:disabled {\n opacity: 0.45;\n cursor: default;\n}\n\n.mjo-gl-links__btn:focus-visible,\n.mjo-gl-links__draft select:focus-visible,\n.mjo-gl-links__draft input:focus-visible {\n outline: 2px solid currentColor;\n outline-offset: 2px;\n}\n\n/* Screen-reader-only, for the actions column header. */\n.mjo-gl-links__sr {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n/* Small inline label so the two date inputs in the edit row say which end they are. */\n.mjo-gl-links__lbl {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 12px;\n opacity: 0.8;\n}\n\n.mjo-gl-links__draft .mjo-gl-links__note {\n flex-basis: 100%;\n margin: 2px 0 0;\n}\n\n.mjo-gl-links__draft select:disabled,\n.mjo-gl-links__draft input:disabled {\n opacity: 0.55;\n}\n"] }]
1219
+ }], () => [{ type: i0.ChangeDetectorRef }], { Product: [{
1220
+ type: Input
1221
+ }], EditMode: [{
1222
+ type: Input
1223
+ }], FormContext: [{
1224
+ type: Input
1225
+ }], Navigate: [{
1226
+ type: Output
1227
+ }] }); })();
1228
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(BizAppsProductGLLinksComponent, { className: "BizAppsProductGLLinksComponent", filePath: "lib/custom/Product/widgets/product-gl-links.component.ts", lineNumber: 161 }); })();
1229
+ //# sourceMappingURL=product-gl-links.component.js.map