newportsite 1.1.3

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.
Files changed (131) hide show
  1. package/newportsite-1.1.3.tgz +0 -0
  2. package/ng-package.json +7 -0
  3. package/obfuscate.js +70 -0
  4. package/package.json +15 -0
  5. package/src/lib/app.component.ts +47 -0
  6. package/src/lib/app.routing.ts +38 -0
  7. package/src/lib/auth/alert.component.html +5 -0
  8. package/src/lib/auth/alert.component.ts +24 -0
  9. package/src/lib/auth/auth.component.html +1 -0
  10. package/src/lib/auth/auth.component.ts +10 -0
  11. package/src/lib/auth/auth.routes.ts +16 -0
  12. package/src/lib/auth/index.ts +4 -0
  13. package/src/lib/auth/login.component.html +87 -0
  14. package/src/lib/auth/login.component.ts +158 -0
  15. package/src/lib/auth/models/index.ts +1 -0
  16. package/src/lib/auth/models/user.ts +25 -0
  17. package/src/lib/auth/register.component.html +157 -0
  18. package/src/lib/auth/register.component.ts +219 -0
  19. package/src/lib/auth/services/alert.service.ts +47 -0
  20. package/src/lib/auth/services/auth.service.ts +28 -0
  21. package/src/lib/auth/services/index.ts +3 -0
  22. package/src/lib/auth/services/user.service.spec.ts +112 -0
  23. package/src/lib/auth/services/user.service.ts +47 -0
  24. package/src/lib/common/card.component.html +72 -0
  25. package/src/lib/common/card.component.ts +102 -0
  26. package/src/lib/common/commands.component.html +8 -0
  27. package/src/lib/common/commands.component.ts +42 -0
  28. package/src/lib/common/context.component.html +9 -0
  29. package/src/lib/common/context.component.ts +38 -0
  30. package/src/lib/common/grid.component.html +20 -0
  31. package/src/lib/common/grid.component.ts +747 -0
  32. package/src/lib/common/index.ts +9 -0
  33. package/src/lib/common/loader.component.html +5 -0
  34. package/src/lib/common/loader.component.ts +27 -0
  35. package/src/lib/common/lookup.component.html +29 -0
  36. package/src/lib/common/lookup.component.ts +115 -0
  37. package/src/lib/common/messagebox.component.html +39 -0
  38. package/src/lib/common/messagebox.component.ts +74 -0
  39. package/src/lib/common/theme-toggle.component.ts +139 -0
  40. package/src/lib/config.ts +62 -0
  41. package/src/lib/containers/default-layout/default-layout.component.html +191 -0
  42. package/src/lib/containers/default-layout/default-layout.component.ts +158 -0
  43. package/src/lib/containers/default-layout/index.ts +1 -0
  44. package/src/lib/containers/index.ts +1 -0
  45. package/src/lib/directives/component.draggable.ts +80 -0
  46. package/src/lib/directives/index.ts +2 -0
  47. package/src/lib/directives/input.directive.spec.ts +158 -0
  48. package/src/lib/directives/input.directive.ts +210 -0
  49. package/src/lib/home/dashboard/dashboard.component.html +38 -0
  50. package/src/lib/home/dashboard/dashboard.component.ts +50 -0
  51. package/src/lib/home/dashboard/index.ts +1 -0
  52. package/src/lib/home/index.component.html +1 -0
  53. package/src/lib/home/index.component.ts +10 -0
  54. package/src/lib/home/index.routes.ts +29 -0
  55. package/src/lib/home/index.ts +1 -0
  56. package/src/lib/home/info/index.ts +1 -0
  57. package/src/lib/home/info/info.component.css +476 -0
  58. package/src/lib/home/info/info.component.html +174 -0
  59. package/src/lib/home/info/info.component.ts +287 -0
  60. package/src/lib/home/model/article.component.html +10 -0
  61. package/src/lib/home/model/article.component.ts +50 -0
  62. package/src/lib/home/model/barchart.component.html +8 -0
  63. package/src/lib/home/model/barchart.component.ts +59 -0
  64. package/src/lib/home/model/index.ts +7 -0
  65. package/src/lib/home/model/itemdetail.component.html +25 -0
  66. package/src/lib/home/model/itemdetail.component.ts +93 -0
  67. package/src/lib/home/model/itemtab.component.html +25 -0
  68. package/src/lib/home/model/itemtab.component.ts +105 -0
  69. package/src/lib/home/model/model.component.html +121 -0
  70. package/src/lib/home/model/model.component.ts +510 -0
  71. package/src/lib/home/model/modeltoolbar.component.html +111 -0
  72. package/src/lib/home/model/modeltoolbar.component.ts +157 -0
  73. package/src/lib/home/model/navigation.component.html +86 -0
  74. package/src/lib/home/model/navigation.component.ts +247 -0
  75. package/src/lib/home/model/services/index.ts +1 -0
  76. package/src/lib/home/model/services/model.service.spec.ts +423 -0
  77. package/src/lib/home/model/services/model.service.ts +319 -0
  78. package/src/lib/home/modelsearch/index.ts +1 -0
  79. package/src/lib/home/modelsearch/modelsearch.component.html +124 -0
  80. package/src/lib/home/modelsearch/modelsearch.component.ts +453 -0
  81. package/src/lib/interfaces/data.interface.ts +131 -0
  82. package/src/lib/interfaces/index.ts +2 -0
  83. package/src/lib/interfaces/item.interface.ts +438 -0
  84. package/src/lib/players/lookup/lookup.directive.ts +6 -0
  85. package/src/lib/players/lookup/lookup.item.component.ts +37 -0
  86. package/src/lib/players/lookup/lookup.item.ts +9 -0
  87. package/src/lib/players/lookup/lookup.player.component.ts +59 -0
  88. package/src/lib/players/lookup/lookup.selector.component.ts +41 -0
  89. package/src/lib/players/model/model.directive.ts +6 -0
  90. package/src/lib/players/model/model.item.component.spec.ts +311 -0
  91. package/src/lib/players/model/model.item.component.ts +3457 -0
  92. package/src/lib/players/model/model.item.ts +9 -0
  93. package/src/lib/players/model/model.player.component.ts +109 -0
  94. package/src/lib/players/model/model.selector.component.ts +59 -0
  95. package/src/lib/scheduler/scheduler.component.html +13 -0
  96. package/src/lib/scheduler/scheduler.component.scss +6 -0
  97. package/src/lib/scheduler/scheduler.component.ts +296 -0
  98. package/src/lib/scheduler/scheduler.routes.ts +15 -0
  99. package/src/lib/scheduler/schedulerdialog.component.html +72 -0
  100. package/src/lib/scheduler/schedulerdialog.component.ts +208 -0
  101. package/src/lib/scheduler/services/scheduler.service.ts +133 -0
  102. package/src/lib/services/auth-state.service.ts +129 -0
  103. package/src/lib/services/auth.interceptor.spec.ts +144 -0
  104. package/src/lib/services/auth.interceptor.ts +44 -0
  105. package/src/lib/services/cache.service.spec.ts +143 -0
  106. package/src/lib/services/cache.service.ts +71 -0
  107. package/src/lib/services/global-error-handler.spec.ts +39 -0
  108. package/src/lib/services/global-error-handler.ts +28 -0
  109. package/src/lib/services/global.service.spec.ts +801 -0
  110. package/src/lib/services/global.service.ts +724 -0
  111. package/src/lib/services/message.service.ts +556 -0
  112. package/src/lib/services/theme.service.ts +96 -0
  113. package/src/lib/template/authtemplate.component.html +6 -0
  114. package/src/lib/template/authtemplate.component.ts +13 -0
  115. package/src/lib/template/basetemplate.component.html +7 -0
  116. package/src/lib/template/basetemplate.component.ts +13 -0
  117. package/src/lib/template/index.ts +3 -0
  118. package/src/lib/template/modeltemplate.component.html +7 -0
  119. package/src/lib/template/modeltemplate.component.ts +21 -0
  120. package/src/lib/utils/piva.spec.ts +56 -0
  121. package/src/lib/utils/piva.ts +29 -0
  122. package/src/lib/validators/email.validator.spec.ts +57 -0
  123. package/src/lib/validators/email.validator.ts +17 -0
  124. package/src/lib/validators/equalPasswords.validator.spec.ts +54 -0
  125. package/src/lib/validators/equalPasswords.validator.ts +17 -0
  126. package/src/lib/validators/index.ts +2 -0
  127. package/src/lib/version.ts +1 -0
  128. package/src/public-api.ts +64 -0
  129. package/src/typings.d.ts +2 -0
  130. package/tsconfig.lib.json +18 -0
  131. package/tsconfig.lib.prod.json +9 -0
@@ -0,0 +1,3457 @@
1
+ import {
2
+ ChangeDetectorRef,
3
+ Component,
4
+ viewChild,
5
+ ElementRef,
6
+ HostListener,
7
+ OnDestroy,
8
+ AfterViewInit,
9
+ DestroyRef,
10
+ inject,
11
+ signal,
12
+ } from '@angular/core';
13
+
14
+ import {
15
+ ItemInterface,
16
+ Member,
17
+ ItemComponentInterface,
18
+ FieldType,
19
+ TransportInterface,
20
+ ItemLinkInterface,
21
+ FieldState,
22
+ FieldAttr,
23
+ ItemState,
24
+ ItemFieldChangedInterface,
25
+ DialogType,
26
+ ItemCommandInterface,
27
+ PadType,
28
+ ManagmentInterface,
29
+ EntitiesData,
30
+ Entities,
31
+ Entity,
32
+ EntityData,
33
+ ModuleData,
34
+ LabelAction,
35
+ Functions,
36
+ MemberData,
37
+ LookupResultEvent,
38
+ DialogResultEvent,
39
+ FixedField,
40
+ FieldFontSize,
41
+ FieldPaddingTop,
42
+ } from '../../interfaces/index';
43
+
44
+ import { GlobalService, Languages } from '../../services/global.service';
45
+
46
+ import { AppMessageService } from '../../services/message.service';
47
+
48
+ import { NEWPORT_CONFIG } from '../../config';
49
+
50
+ import { ManagmentService } from '../../home/model/services/index';
51
+
52
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
53
+ import { firstValueFrom } from 'rxjs';
54
+
55
+ import CodiceFiscale from 'codice-fiscale-js';
56
+
57
+ import { PartitaIVA } from '../../utils/piva';
58
+ import { v4 as uuidv4 } from 'uuid';
59
+
60
+ import { saveAs } from 'file-saver';
61
+
62
+ import { HttpEventType } from '@angular/common/http';
63
+
64
+ import { CommandsComponent } from '../../common/commands.component';
65
+ import { NgClass } from '@angular/common';
66
+ import { InputDirective } from '../../directives/input.directive';
67
+ import { BarChartComponent } from '../../home/model/barchart.component';
68
+ import { TooltipDirective } from 'ngx-bootstrap/tooltip';
69
+
70
+ @Component({
71
+ selector: 'app-model-item',
72
+ template: `
73
+ <div class="position-absolute" #innercontainer>
74
+ <div>
75
+ <commands
76
+ [(show)]="showcommands"
77
+ [(text)]="textcommands"
78
+ [(title)]="texttitle"
79
+ (commandsResult)="commandsResult($event)"
80
+ [(width)]="commandsWidth"
81
+ [(height)]="commandsHeight"
82
+ [(top)]="commandsTop"
83
+ [(left)]="commandsLeft"></commands>
84
+
85
+ <img
86
+ [class.field-loading]="!loaded"
87
+ [class.field-cache]="model?.cache"
88
+ [class.pe-none]="showlookup || showdialog || model?.cache"
89
+ id="bkgwhite"
90
+ [hidden]="entity.svg === null || entity.svg.length === 0"
91
+ [src]="svg"
92
+ [style.width]="entity.width"
93
+ [style.height]="entity.height"
94
+ />
95
+
96
+ @for (
97
+ field of entity.members;
98
+ track trackByField($index, field);
99
+ let i = $index
100
+ ) {
101
+ @switch (field.type) {
102
+ @case (fieldType.Label) {
103
+ <div
104
+ [hidden]="field.graphic !== 'label'"
105
+ [ngClass]="labelClasses(field)"
106
+ [class.field-loading]="!loaded"
107
+ [class.field-cache]="model?.cache"
108
+ [class.pe-none]="showlookup || showdialog || model?.cache"
109
+ nvg
110
+ [id]="field.name"
111
+ [style.line-height]="field.height"
112
+ [style.left]="field.x"
113
+ [style.top]="field.y"
114
+ [style.width]="field.width"
115
+ class="position-absolute text-capitalize field-z0"
116
+ [attr.data-type]="field.type"
117
+ [attr.data-state]="field.state">
118
+ {{ defaultLanguage() ? field.caption : field.captiongb }}
119
+ </div>
120
+ }
121
+ @case (fieldType.Image) {
122
+ <input
123
+ type="image"
124
+ [ngClass]="{
125
+ 'has-hidden': field?.state === fieldState.Hidden,
126
+ }"
127
+ [class.field-loading]="!loaded"
128
+ [class.field-cache]="model?.cache"
129
+ [class.pe-none]="showlookup || showdialog || model?.cache"
130
+ [id]="field.name"
131
+ [style.line-height]="field.height"
132
+ [style.left]="field.x"
133
+ [style.top]="field.y"
134
+ [style.width]="field.width"
135
+ class="position-absolute field-z0"
136
+ [attr.data-type]="field.type"
137
+ [attr.data-state]="field.state"
138
+ [src]="field.value.toLowerCase()"
139
+ disabled="true" />
140
+ }
141
+ @case (fieldType.Video) {
142
+ <video
143
+ playsinline
144
+ autoplay
145
+ controls
146
+ #video
147
+ [ngClass]="{
148
+ 'has-hidden': field?.state === fieldState.Hidden,
149
+ }"
150
+ [class.field-loading]="!loaded"
151
+ [class.field-cache]="model?.cache"
152
+ [class.pe-none]="showlookup || showdialog || model?.cache"
153
+ [id]="field.name"
154
+ [style.line-height]="field.height"
155
+ [style.left]="field.x"
156
+ [style.top]="field.y"
157
+ [style.width]="field.width"
158
+ class="position-absolute field-z0"
159
+ [attr.data-type]="field.type"
160
+ [attr.data-state]="field.state">
161
+ <source [src]="field.value.toLowerCase()" type="video/mp4" />
162
+ </video>
163
+ }
164
+ @case (fieldType.Map) {
165
+ <input
166
+ type="image"
167
+ [ngClass]="{
168
+ 'has-hidden': field?.state === fieldState.Hidden,
169
+ }"
170
+ [class.field-loading]="!loaded"
171
+ [class.field-cache]="model?.cache"
172
+ [class.pe-none]="showlookup || showdialog || model?.cache"
173
+ [id]="field.name"
174
+ [style.line-height]="field.height"
175
+ [style.left]="field.x"
176
+ [style.top]="field.y"
177
+ [style.width]="field.width"
178
+ class="position-absolute field-z0"
179
+ [attr.data-type]="field.type"
180
+ [attr.data-state]="field.state"
181
+ [src]="field.value.toLowerCase()"
182
+ disabled="true" />
183
+ }
184
+ @case (fieldType.Button) {
185
+ <input
186
+ type="button"
187
+ (click)="buttonClick(field.name, field.action)"
188
+ [ngClass]="buttonClasses(field)"
189
+ [class.field-loading]="!loaded"
190
+ [class.field-cache]="model?.cache"
191
+ [class.pe-none]="showlookup || showdialog || model?.cache"
192
+ nvg
193
+ [id]="field.name"
194
+ [style.line-height]="field.height"
195
+ [style.left]="field.x"
196
+ [style.top]="field.y"
197
+ [style.width]="field.width"
198
+ class="position-absolute text-capitalize p-0 m-0 field-z0"
199
+ [attr.data-type]="field.type"
200
+ [attr.data-state]="field.state"
201
+ value="{{
202
+ defaultLanguage() ? field.caption : field.captiongb
203
+ }}" />
204
+ }
205
+ @case (fieldType.Combo) {
206
+ <select
207
+ [ngClass]="fieldClasses(field)"
208
+ [class.field-loading]="!loaded"
209
+ [class.field-cache]="model?.cache"
210
+ [class.pe-none]="showlookup || showdialog || model?.cache"
211
+ nvg
212
+ [id]="field.name"
213
+ [style.left]="field.x"
214
+ [style.top]="field.y"
215
+ [style.width]="field.width"
216
+ [style.height]="field.height"
217
+ class="position-absolute"
218
+ [class.field-border-svg]="entity.svg.length > 0"
219
+ [class.field-border-left]="
220
+ field.state !== 0 || entity.svg.length === 0
221
+ "
222
+ [tabindex]="field.tabindex"
223
+ [attr.data-type]="field.type"
224
+ [attr.data-state]="field.state"
225
+ [attr.data-mandatory]="field.mandatory">
226
+ @for (
227
+ c of field.combo.split(',');
228
+ track trackByCombo($index, c)
229
+ ) {
230
+ <option
231
+ selected="{{
232
+ c === field.value.toLocaleLowerCase() ? 'selected' : ''
233
+ }}">
234
+ {{ c.toUpperCase() }}
235
+ </option>
236
+ }
237
+ </select>
238
+ }
239
+ @case (fieldType.Chart) {
240
+ <barchart
241
+ [dataChart]="
242
+ gV(field.id.replace('barchart', '')).toLocaleLowerCase()
243
+ "
244
+ [ngClass]="{
245
+ 'has-hidden': field?.state === fieldState.Hidden,
246
+ }"
247
+ [class.field-loading]="!loaded"
248
+ [id]="field.name"
249
+ [style.line-height]="field.height"
250
+ [style.left]="field.x"
251
+ [style.top]="field.y"
252
+ [style.width]="field.width"
253
+ class="position-absolute field-z0"
254
+ [attr.data-type]="field.type"
255
+ [attr.data-state]="field.state"></barchart>
256
+ }
257
+ @case (fieldType.Date) {
258
+ <input
259
+ type="text"
260
+ autocomplete="off"
261
+ [ngClass]="fieldClasses(field)"
262
+ [class.field-loading]="!loaded"
263
+ [class.field-cache]="model?.cache"
264
+ [class.pe-none]="showlookup || showdialog || model?.cache"
265
+ [style.paddingTop]="
266
+ entity.svg.length === 0
267
+ ? fieldPaddingTop.none
268
+ : gsv.getInt(field.height) > fieldPaddingTop.tallThreshold
269
+ ? fieldPaddingTop.tall
270
+ : fieldPaddingTop.short
271
+ "
272
+ nvg
273
+ [id]="field.name"
274
+ [style.left]="field.x"
275
+ [style.top]="field.y"
276
+ [style.width]="field.width"
277
+ [style.height]="field.height"
278
+ class="position-absolute"
279
+ [class.field-border-svg]="entity.svg.length > 0"
280
+ [class.field-border-left]="
281
+ field.state !== 0 || entity.svg.length === 0
282
+ "
283
+ [tabindex]="field.tabindex"
284
+ [attr.data-type]="field.type"
285
+ [attr.data-state]="field.state"
286
+ tooltip="{{ field.title }}"
287
+ [containerClass]="tooltipClass(field)"
288
+ [value]="field.value"
289
+ [disabled]="
290
+ !field.enabled &&
291
+ field?.state !== fieldState.Error &&
292
+ field?.state !== fieldState.Warning
293
+ "
294
+ [hidden]="field.state === fieldState.Hidden || !field.visible"
295
+ [attr.data-initialvalue]="field.initialvalue"
296
+ [attr.data-match]="field.match"
297
+ [attr.data-replace]="field.replace"
298
+ [attr.data-tag]="field.tag"
299
+ [attr.data-autoformatter]="field.autoformatter"
300
+ [attr.data-state]="field.state"
301
+ [attr.data-min]="field.min"
302
+ [attr.data-max]="field.max"
303
+ [attr.data-mandatory]="field.mandatory"
304
+ [attr.data-viewlist]="field.viewlist"
305
+ [attr.data-lookup]="field.lookup" />
306
+ }
307
+ @case (fieldType.Number) {
308
+ <input
309
+ type="text"
310
+ autocomplete="off"
311
+ placement="right"
312
+ container="innercontainer"
313
+ adaptivePosition="true"
314
+ [ngClass]="fieldClasses(field)"
315
+ [class.field-loading]="!loaded"
316
+ [class.field-cache]="model?.cache"
317
+ [class.pe-none]="showlookup || showdialog || model?.cache"
318
+ [style.fontSize]="
319
+ field.value.length * fieldFontSize.compactPx > parseWidth(field.width) &&
320
+ !field.autoformatter
321
+ ? fieldFontSize.compact
322
+ : fieldFontSize.default
323
+ "
324
+ [style.paddingTop]="
325
+ entity.svg.length === 0
326
+ ? fieldPaddingTop.none
327
+ : gsv.getInt(field.height) > fieldPaddingTop.tallThreshold
328
+ ? fieldPaddingTop.tall
329
+ : fieldPaddingTop.short
330
+ "
331
+ nvg
332
+ [id]="field.name"
333
+ [style.left]="field.x"
334
+ [style.top]="field.y"
335
+ [style.width]="field.width"
336
+ [style.height]="field.height"
337
+ class="position-absolute"
338
+ [class.field-border-svg]="entity.svg.length > 0"
339
+ [class.field-border-left]="
340
+ field.state !== 0 || entity.svg.length === 0
341
+ "
342
+ [tabindex]="field.tabindex"
343
+ [attr.data-type]="field.type"
344
+ [value]="field.value === 0 ? '' : field.value"
345
+ tooltip="{{ field.title }}"
346
+ [containerClass]="tooltipClass(field)"
347
+ [disabled]="
348
+ !field.enabled &&
349
+ field?.state !== fieldState.Error &&
350
+ field?.state !== fieldState.Warning
351
+ "
352
+ [hidden]="field.state === fieldState.Hidden || !field.visible"
353
+ [attr.data-initialvalue]="field.initialvalue"
354
+ [attr.data-length]="field.precision"
355
+ [attr.data-decimal]="field.scale"
356
+ [attr.data-sign]="field.sign"
357
+ [attr.data-thousandseparator]="field.thousandseparator"
358
+ [attr.data-match]="field.match"
359
+ [attr.data-replace]="field.replace"
360
+ [attr.data-tag]="field.tag"
361
+ [attr.data-state]="field.state"
362
+ [attr.data-min]="field.min"
363
+ [attr.data-max]="field.max"
364
+ [attr.data-mandatory]="field.mandatory"
365
+ [attr.data-viewlist]="field.viewlist"
366
+ [attr.data-lookup]="field.lookup" />
367
+ }
368
+ @case (fieldType.TextualCheck) {
369
+ @if (field.graphic === 'text') {
370
+ <div>
371
+ <input
372
+ type="text"
373
+ autocomplete="off"
374
+ placement="right"
375
+ container="innercontainer"
376
+ adaptivePosition="true"
377
+ [ngClass]="fieldClasses(field)"
378
+ [class.field-loading]="!loaded"
379
+ [class.field-cache]="model?.cache"
380
+ [class.pe-none]="showlookup || showdialog || model?.cache"
381
+ [style.paddingTop]="
382
+ entity.svg.length === 0
383
+ ? fieldPaddingTop.none
384
+ : gsv.getInt(field.height) > fieldPaddingTop.tallThreshold
385
+ ? fieldPaddingTop.tall
386
+ : fieldPaddingTop.short
387
+ "
388
+ nvg
389
+ [id]="field.name"
390
+ [style.left]="field.x"
391
+ [style.top]="field.y"
392
+ [style.width]="field.width"
393
+ [style.height]="field.height"
394
+ class="position-absolute"
395
+ [class.field-border-svg]="entity.svg.length > 0"
396
+ [class.field-border-left]="
397
+ field.state !== 0 || entity.svg.length === 0
398
+ "
399
+ [tabindex]="field.tabindex"
400
+ [attr.data-type]="field.type"
401
+ [value]="field.value"
402
+ tooltip="{{ field.title }}"
403
+ [containerClass]="tooltipClass(field)"
404
+ [disabled]="
405
+ !field.enabled &&
406
+ field?.state !== fieldState.Error &&
407
+ field?.state !== fieldState.Warning
408
+ "
409
+ [hidden]="
410
+ field.state === fieldState.Hidden || !field.visible
411
+ "
412
+ [attr.data-allowedchars]="field.allowedchars"
413
+ [attr.data-autoformatter]="field.autoformatter"
414
+ [attr.data-padleftchar]="field.padleftchar"
415
+ [attr.data-padrightchar]="field.padrightchar"
416
+ [attr.data-initialvalue]="field.initialvalue"
417
+ [attr.data-state]="field.state"
418
+ [attr.data-mandatory]="field.mandatory"
419
+ [attr.data-viewlist]="field.viewlist"
420
+ [attr.data-lookup]="field.lookup" />
421
+ </div>
422
+ }
423
+ @if (field.graphic === 'check') {
424
+ <div>
425
+ <input
426
+ type="checkbox"
427
+ autocomplete="off"
428
+ placement="right"
429
+ container="innercontainer"
430
+ adaptivePosition="true"
431
+ [ngClass]="fieldClasses(field)"
432
+ [class.field-loading]="!loaded"
433
+ [class.field-cache]="model?.cache"
434
+ [class.pe-none]="showlookup || showdialog || model?.cache"
435
+ [class.field-appearance-auto]="field.tag === 'X'"
436
+ [class.field-appearance-none]="field.tag !== 'X'"
437
+ nvg
438
+ [id]="field.name"
439
+ [style.left]="field.x"
440
+ [style.top]="field.y"
441
+ [style.width]="field.width"
442
+ [style.height]="field.height"
443
+ class="position-absolute"
444
+ [class.field-border-svg]="entity.svg.length > 0"
445
+ [class.field-border-left]="
446
+ field.state !== 0 || entity.svg.length === 0
447
+ "
448
+ [tabindex]="field.tabindex"
449
+ [attr.data-type]="field.type"
450
+ [value]="field.value"
451
+ [checked]="field.value === 'X' ? 'checked' : ''"
452
+ tooltip="{{ field.title }}"
453
+ [containerClass]="tooltipClass(field)"
454
+ [disabled]="
455
+ (!field.enabled &&
456
+ field?.state !== fieldState.Error &&
457
+ field?.state !== fieldState.Warning) ||
458
+ field?.state === fieldState.ReadOnly
459
+ "
460
+ [hidden]="
461
+ field.state === fieldState.Hidden || !field.visible
462
+ "
463
+ [attr.data-allowedchars]="field.allowedchars"
464
+ [attr.data-autoformatter]="field.autoformatter"
465
+ [attr.data-padleftchar]="field.padleftchar"
466
+ [attr.data-padrightchar]="field.padrightchar"
467
+ [attr.data-initialvalue]="field.initialvalue"
468
+ [attr.data-state]="field.state"
469
+ [attr.data-mandatory]="field.mandatory"
470
+ [attr.data-viewlist]="field.viewlist"
471
+ [attr.data-lookup]="field.lookup" />
472
+ </div>
473
+ }
474
+ @if (field.graphic === 'radio') {
475
+ <div>
476
+ <input
477
+ type="radio"
478
+ autocomplete="off"
479
+ placement="right"
480
+ container="innercontainer"
481
+ adaptivePosition="true"
482
+ [ngClass]="fieldClasses(field)"
483
+ [class.field-loading]="!loaded"
484
+ [class.field-cache]="model?.cache"
485
+ [class.pe-none]="showlookup || showdialog || model?.cache"
486
+ nvg
487
+ [id]="field.name"
488
+ [style.left]="field.x"
489
+ [style.top]="field.y"
490
+ [style.width]="field.width"
491
+ [style.height]="field.height"
492
+ class="position-absolute"
493
+ [class.field-border-svg]="entity.svg.length > 0"
494
+ [class.field-border-left]="
495
+ field.state !== 0 || entity.svg.length === 0
496
+ "
497
+ [tabindex]="field.tabindex"
498
+ [attr.data-type]="field.type"
499
+ [value]="field.value"
500
+ [checked]="field.value === 'X' ? 'checked' : ''"
501
+ tooltip="{{ field.title }}"
502
+ [containerClass]="tooltipClass(field)"
503
+ [disabled]="
504
+ (!field.enabled &&
505
+ field?.state !== fieldState.Error &&
506
+ field?.state !== fieldState.Warning) ||
507
+ field?.state === fieldState.ReadOnly
508
+ "
509
+ [hidden]="
510
+ field.state === fieldState.Hidden || !field.visible
511
+ "
512
+ [attr.data-allowedchars]="field.allowedchars"
513
+ [attr.data-autoformatter]="field.autoformatter"
514
+ [attr.data-padleftchar]="field.padleftchar"
515
+ [attr.data-padrightchar]="field.padrightchar"
516
+ [attr.data-initialvalue]="field.initialvalue"
517
+ [attr.data-state]="field.state"
518
+ [attr.data-mandatory]="field.mandatory"
519
+ [attr.data-viewlist]="field.viewlist"
520
+ [attr.data-lookup]="field.lookup" />
521
+ </div>
522
+ }
523
+ }
524
+ @case (fieldType.Text) {
525
+ <input
526
+ type="text"
527
+ autocomplete="off"
528
+ placement="right"
529
+ container="innercontainer"
530
+ adaptivePosition="true"
531
+ [ngClass]="fieldClasses(field)"
532
+ [class.field-loading]="!loaded"
533
+ [class.field-cache]="model?.cache"
534
+ [class.pe-none]="showlookup || showdialog || model?.cache"
535
+ [style.fontSize]="
536
+ field.value.length * fieldFontSize.compactPx > parseWidth(field.width) &&
537
+ !field.autoformatter
538
+ ? fieldFontSize.compact
539
+ : fieldFontSize.default
540
+ "
541
+ [style.paddingTop]="
542
+ entity.svg.length === 0
543
+ ? fieldPaddingTop.none
544
+ : gsv.getInt(field.height) > fieldPaddingTop.tallThreshold
545
+ ? fieldPaddingTop.tall
546
+ : fieldPaddingTop.short
547
+ "
548
+ nvg
549
+ [id]="field.name"
550
+ [style.left]="field.x"
551
+ [style.top]="field.y"
552
+ [style.width]="field.width"
553
+ [style.height]="field.height"
554
+ class="position-absolute"
555
+ [class.field-border-svg]="entity.svg.length > 0"
556
+ [class.field-border-left]="
557
+ field.state !== 0 || entity.svg.length === 0
558
+ "
559
+ [tabindex]="field.tabindex"
560
+ [attr.data-type]="field.type"
561
+ [value]="field.value"
562
+ tooltip="{{ field.title }}"
563
+ [containerClass]="tooltipClass(field)"
564
+ [disabled]="
565
+ !field.enabled &&
566
+ field?.state !== fieldState.Error &&
567
+ field?.state !== fieldState.Warning
568
+ "
569
+ [hidden]="field.state === fieldState.Hidden || !field.visible"
570
+ [attr.data-initialvalue]="field.initialvalue"
571
+ [attr.data-length]="field.length"
572
+ [attr.data-match]="field.match"
573
+ [attr.data-replace]="field.replace"
574
+ [attr.data-tag]="field.tag"
575
+ [attr.data-allowedchars]="field.allowedchars"
576
+ [attr.data-autoformatter]="field.autoformatter"
577
+ [attr.data-padleftchar]="field.padleftchar"
578
+ [attr.data-padrightchar]="field.padrightchar"
579
+ [attr.data-state]="field.state"
580
+ [attr.data-mandatory]="field.mandatory"
581
+ [attr.data-viewlist]="field.viewlist"
582
+ [attr.data-lookup]="field.lookup" />
583
+ }
584
+ }
585
+ }
586
+ </div>
587
+ </div>
588
+ `,
589
+ imports: [
590
+ CommandsComponent,
591
+ InputDirective,
592
+ NgClass,
593
+ BarChartComponent,
594
+ TooltipDirective,
595
+ ],
596
+ })
597
+ export class ModelItemComponent
598
+ implements AfterViewInit, ItemComponentInterface, OnDestroy
599
+ {
600
+ gsv = inject(GlobalService);
601
+ msg = inject(AppMessageService);
602
+ msv = inject(ManagmentService);
603
+ private config = inject(NEWPORT_CONFIG);
604
+ private cdr = inject(ChangeDetectorRef);
605
+
606
+ public model!: ItemInterface;
607
+ public managment!: ManagmentInterface;
608
+ public insertMode!: boolean;
609
+ public fieldType = FieldType;
610
+ public fieldState = FieldState;
611
+ public fieldFontSize = FieldFontSize;
612
+ public fieldPaddingTop = FieldPaddingTop;
613
+ public logic!: any;
614
+ public titledialog = '';
615
+ public textdialog = '';
616
+ public showconfirm = false;
617
+ public dialogHeight = 0;
618
+ public dialogWidth = 0;
619
+ public dialogTop = 0;
620
+ public dialogLeft = 0;
621
+ public dialogType: DialogType = 0;
622
+ public showdialog = false;
623
+ private _dialogTarget = '';
624
+ public titlelookup = '';
625
+ public textlookup = '';
626
+ public lookupHeight = 0;
627
+ public lookupWidth = 0;
628
+ public lookupTop = 0;
629
+ public lookupLeft = 0;
630
+ public lookup = '';
631
+ public lookupfilter = '';
632
+ public showlookup = false;
633
+ //
634
+ // Commands
635
+ public showcommands = false;
636
+ public textcommands = '';
637
+ public texttitle = '';
638
+ public commandsHeight = 0;
639
+ public commandsWidth = 0;
640
+ public commandsTop = 0;
641
+ public commandsLeft = 0;
642
+ //
643
+ public currentFieldId: string = '';
644
+ private destroyRef = inject(DestroyRef);
645
+
646
+ public viewList!: boolean;
647
+ public lookupField!: Member;
648
+
649
+ public bplus = true;
650
+ public bminus = true;
651
+ public bfirst = true;
652
+ public bprevious = true;
653
+ public bnext = true;
654
+ public blast = true;
655
+ public bdetail = true;
656
+ public bintodetail = true;
657
+ public bdownload = true;
658
+ public bupload = true;
659
+ public bprint = true;
660
+ public btable = true;
661
+ public save = true;
662
+ public refreshgrid = true;
663
+ public svg: string = '';
664
+ public data!: EntitiesData;
665
+ public loaded = false;
666
+ public entity!: Entity;
667
+ private _memberMap: Map<string, Member> | null = null;
668
+ private _readonlySet = new Set<string>();
669
+ private _cachedKeyArray: number[] | null = null;
670
+ private _cachedKeyString = '';
671
+ public lastKey!: number;
672
+ public fieldsId = '';
673
+ public closing = false;
674
+ public resetData = true;
675
+ public root!: Entities;
676
+
677
+ readonly container = viewChild.required<ElementRef>('innercontainer');
678
+ private readonly video = viewChild<ElementRef<HTMLVideoElement>>('video');
679
+ constructor() {
680
+ this.managment = this.gsv.getManagmentInterface();
681
+
682
+ this.gsv
683
+ .getFieldChanged()
684
+ .pipe(takeUntilDestroyed(this.destroyRef))
685
+ .subscribe(async currentFieldChanged => {
686
+ if (currentFieldChanged.function === 'setForced') {
687
+ if (this.gS(currentFieldChanged.id) === FieldState.Forced) {
688
+ this.sS(currentFieldChanged.id, FieldState.None);
689
+ } else {
690
+ this.sS(currentFieldChanged.id, FieldState.Forced);
691
+ }
692
+ }
693
+ await this.fieldChanged({
694
+ id: currentFieldChanged.id,
695
+ value: currentFieldChanged.value,
696
+ function: currentFieldChanged.function,
697
+ });
698
+ });
699
+
700
+ this.gsv
701
+ .getCurrentField()
702
+ .pipe(takeUntilDestroyed(this.destroyRef))
703
+ .subscribe(currentFieldChanged => {
704
+ this.logic.function(
705
+ Functions.lostfocus,
706
+ this.getIndex(),
707
+ this.getKeyArray(),
708
+ this.currentFieldId
709
+ );
710
+ this.currentField(currentFieldChanged);
711
+ this.setFocus(currentFieldChanged.id);
712
+ });
713
+ }
714
+
715
+ /** Lifecycle: fetches entity data on first render. */
716
+ async ngAfterViewInit() {
717
+ await this.getData(this.model.id);
718
+ }
719
+
720
+ /** Lifecycle: saves cached data and calls close() on component teardown. */
721
+ public async ngOnDestroy() {
722
+ if (this.data && this.data.entities) {
723
+ this.svg = null!;
724
+ this.gsv.setData(this.fieldsId, this.data);
725
+ if (
726
+ this.gsv.getProject().type !== 'D' &&
727
+ !this.closing &&
728
+ !this.model.preview
729
+ ) {
730
+ await this.close();
731
+ }
732
+ }
733
+ }
734
+
735
+ /**
736
+ * Persists any unsaved changes via putDataPromise (save) before the component is
737
+ * removed from the view, and optionally resets the in-memory data cache.
738
+ */
739
+ public async close() {
740
+ const idSchemaJson = this.managment.appId + '.' + this.managment.year;
741
+ this.closing = true;
742
+ // const root: Entities = this.gsv.getSchemaJson(idSchemaJson);
743
+ if (this.data && this.data.entities) {
744
+ this.data.entities.forEach(entity => {
745
+ this.fieldsId =
746
+ this.managment.appId +
747
+ '.' +
748
+ entity.name.toUpperCase() +
749
+ '.' +
750
+ this.managment.year;
751
+ const schemaEntity = this.root.entities.find(
752
+ e => e.name == entity.name.toLocaleLowerCase()
753
+ );
754
+ if (schemaEntity && !schemaEntity.cache && this.resetData) {
755
+ this.gsv.removeData(this.fieldsId);
756
+ }
757
+ });
758
+ this.gsv.setExtendedDescription('');
759
+ const json = this.getJsonData();
760
+ if (this.model.cache || !json) {
761
+ return;
762
+ } else {
763
+ return await this.throwSave(json).then(e => {
764
+ if (this.resetData) {
765
+ this.data.entities = [];
766
+ this.data = null!;
767
+ this.entity = null!;
768
+ this._memberMap = null;
769
+ this.root = null!;
770
+ this.gsv.setData(this.fieldsId, null!);
771
+ }
772
+ });
773
+ }
774
+ } else {
775
+ return;
776
+ }
777
+ }
778
+
779
+ /** Sends a PUT request to persist the given JSON payload; shows a dialog on failure. */
780
+ public async throwSave(json: string) {
781
+ if (!json || json === '') return;
782
+ this.managment.data = json;
783
+ this.managment.functionId = 'save';
784
+ await this.msv.putDataPromise(this.managment).then(
785
+ (data: TransportInterface) => {
786
+ this.save = true;
787
+ this.sendCommands();
788
+ },
789
+ error => {
790
+ this.showMessage(
791
+ this.msg.get('app.saveerror') || this.msg.get('app.unabletosave')
792
+ );
793
+ }
794
+ );
795
+ }
796
+
797
+ @HostListener('document:keydown.escape', ['$event']) onKeydownHandlerEscape(
798
+ event: KeyboardEvent
799
+ ) {
800
+ if (this.gsv.getModalActive()) {
801
+ return;
802
+ }
803
+ event.stopImmediatePropagation();
804
+ this.sendCommand({ id: 'exit', state: true });
805
+ }
806
+
807
+ @HostListener('window:keydown', ['$event']) onKeydownHandler(
808
+ event: KeyboardEvent
809
+ ) {
810
+ if (event.shiftKey && event.key === 'N') {
811
+ event.preventDefault();
812
+ event.stopImmediatePropagation();
813
+ this.sendCommand({ id: 'plus', state: true });
814
+ }
815
+ }
816
+
817
+ /** Returns true if the active locale is Italian. Used in template bindings. */
818
+ public defaultLanguage(): boolean {
819
+ return this.gsv?.getLng() === Languages.IT;
820
+ }
821
+
822
+ /** Returns the EntityData for the given entity name and optional entity index. */
823
+ public getDataEntity(name: string, entity: number = 0): EntityData {
824
+ return this.data?.entities.find(e => e.name === name)!;
825
+ }
826
+
827
+ /**
828
+ * Navigates to the MemberData for a field within the currently active module,
829
+ * using the same modulefiltered logic as commandData/fieldBusiness.
830
+ * Returns undefined if the entity or module cannot be resolved.
831
+ */
832
+ private getModuleMemberData(
833
+ fieldId: string,
834
+ entityName?: string
835
+ ): MemberData | undefined {
836
+ const entName = entityName ?? this.managment.entId.toLocaleLowerCase();
837
+ const entity = this.getDataEntity(entName);
838
+ if (!entity?.modules?.length) return undefined;
839
+ const prgLength = entity.modules[0].prg.length - 1;
840
+ const modulefiltered =
841
+ prgLength > 0
842
+ ? entity.modules.filter(
843
+ e => e.prg[0] === this.getKeyArray()[0] && e.state === 1
844
+ )
845
+ : entity.modules;
846
+ const index = entity.modules.indexOf(modulefiltered[this.lastKey]);
847
+ if (index < 0) return undefined;
848
+ return entity.modules[index].members.find(m => m.name === fieldId);
849
+ }
850
+
851
+ /** Returns the first ModuleData matching key and keyElement within the given EntityData. */
852
+ public getDataModule(
853
+ data: EntityData,
854
+ key: number,
855
+ keyElement: number,
856
+ module: number = 0
857
+ ): ModuleData {
858
+ return data.modules.filter(e => e.prg[keyElement] === key && e.state === 1)[
859
+ module
860
+ ];
861
+ }
862
+
863
+ /** Returns a nested ModuleData by entity name, key, and keyElement indices (utility for logic classes). */
864
+ public getDataEntityModule(
865
+ name: string,
866
+ key: number,
867
+ keyElement: number,
868
+ entity: number = 0,
869
+ module: number = 0
870
+ ): ModuleData {
871
+ let data = this.data.entities.filter(e => e.name === name)[entity];
872
+ return data.modules.filter(e => e.prg[keyElement] === key && e.state === 1)[
873
+ module
874
+ ];
875
+ }
876
+
877
+ /** Propagates the global UI scale factor to GlobalService. */
878
+ public setScale(scale: number) {
879
+ this.gsv.setScale(scale);
880
+ }
881
+
882
+ /** Handles a row selected in the lookup popup: maps output fields and resets the panel. */
883
+ public lookupResult(rsp: LookupResultEvent) {
884
+ // Parse lookup definition before clearing this.lookup below.
885
+ let output: string[] = [];
886
+ let input: string[] = [];
887
+ if (rsp.value !== null) {
888
+ const lookupdata = this.lookup.split('#');
889
+ output = lookupdata[4].split(',');
890
+ input = lookupdata[3].split(',');
891
+ for (let i = 0; i < input.length; i++) {
892
+ if (rsp.value[0][input[i]] === undefined) {
893
+ rsp.value[0][input[i]] = '';
894
+ }
895
+ }
896
+ }
897
+
898
+ // Dismiss the modal FIRST so that pe-none is lifted immediately.
899
+ // Also required: showlookup=false lets the lookup block inside fieldChanged
900
+ // run when the user next blurs a field (it checks !this.showlookup).
901
+ this.titlelookup = '';
902
+ this.textlookup = '';
903
+ this.lookupHeight = 0;
904
+ this.lookupWidth = 0;
905
+ this.lookupTop = 0;
906
+ this.lookupLeft = 0;
907
+ this.lookup = '';
908
+ this.lookupfilter = '';
909
+ this.textcommands = '...';
910
+ this.showcommands = true;
911
+ this.bdetail = false;
912
+ this.showlookup = false;
913
+ this.sendCommands();
914
+
915
+ if (
916
+ rsp.value !== null &&
917
+ this.lookupField &&
918
+ this.lookupField.state !== FieldState.ReadOnly
919
+ ) {
920
+ for (let i = 0; i < input.length; i++) {
921
+ // Skip only Forced (server-immutable) fields.
922
+ // ReadOnly = lookupoutput; a lookup selection must be able to fill these.
923
+ if (this.gS(output[i]) !== FieldState.Forced) {
924
+ this.sV(output[i], rsp.value[0][input[i]]);
925
+ // 'lookupCheck' is in the exclusion list of the lookup block inside
926
+ // fieldChanged, so this call is entirely synchronous (no HTTP fetch,
927
+ // no recursive lookupCheck). Do NOT await — there is nothing to await,
928
+ // and awaiting would schedule microtasks that interleave with the
929
+ // still-pending blur-triggered fieldChanged subscribers.
930
+ this.fieldChanged({
931
+ id: output[i],
932
+ value: rsp.value[0][input[i]],
933
+ function: 'lookupCheck',
934
+ });
935
+ }
936
+ }
937
+ // Run business logic for the primary output field so that computed
938
+ // fields (e.g. MMAG_RIG: imp/tot/rim calculations, Forced states on
939
+ // prz/codiva) are applied immediately after the lookup fills the fields.
940
+ // fieldBusiness calls updateState() internally, so we skip the separate call.
941
+ if (output.length > 0) {
942
+ this.fieldBusiness(output[0]);
943
+ } else {
944
+ // No output fields — still update navigation badge states.
945
+ this.updateState();
946
+ }
947
+ }
948
+
949
+ // For masterDetail entities the grid must be refreshed so that the newly
950
+ // populated lookup values (and any computed members set by fieldBusiness)
951
+ // are visible in the grid immediately after lookup selection.
952
+ if (this.entity?.masterdetail !== 0) {
953
+ this.refreshData();
954
+ }
955
+
956
+ if (this.lookupField) {
957
+ this.setFocus(this.lookupField.id);
958
+ }
959
+ }
960
+
961
+ /** Opens the lookup popup for the currently focused field, computing position and filter. */
962
+ public commandsResult(rsp: unknown) {
963
+ const field: Member = this.entity.members.find(
964
+ item => item.id === this.currentFieldId
965
+ )!;
966
+ if (!field) {
967
+ return;
968
+ }
969
+ this.titlelookup = field.lookup !== '' ? field.lookup.split('#')[6] : '';
970
+ this.textlookup = '';
971
+ this.lookupHeight = 500;
972
+ this.lookupWidth = 700;
973
+
974
+ const scrollTop =
975
+ this.container().nativeElement.parentElement.parentElement.parentElement
976
+ .parentElement.scrollTop;
977
+ const scrollLeft =
978
+ this.container().nativeElement.parentElement.parentElement.parentElement
979
+ .parentElement.scrollLeft;
980
+
981
+ if (
982
+ parseInt(field.y, 10) + parseInt(field.height, 10) + this.lookupHeight >
983
+ this.container().nativeElement.parentElement.parentElement.parentElement
984
+ .parentElement.clientHeight +
985
+ scrollTop &&
986
+ this.model.masterDetail == 0
987
+ ) {
988
+ this.lookupTop =
989
+ parseInt(field.y, 10) + parseInt(field.height, 10) - this.lookupHeight;
990
+ } else {
991
+ this.lookupTop = parseInt(field.y, 10) + parseInt(field.height, 10);
992
+ }
993
+
994
+ this.lookupField = field;
995
+ this.lookupfilter = field.lookup !== '' ? field.lookup.split('#')[5] : '';
996
+ if (this.lookupfilter !== '') {
997
+ const lkf: string[] = this.lookupfilter.split(',');
998
+ // tslint:disable-next-line:prefer-for-of
999
+ for (let i = 0; i < lkf.length; i++) {
1000
+ const lkfe: string[] = lkf[i].split(';');
1001
+ this.lookupfilter = this.lookupfilter.replace(
1002
+ lkfe[1],
1003
+ String(this.entity.members.find(item => item.id === lkfe[1].substring(1))?.value ?? '')
1004
+ );
1005
+ }
1006
+ }
1007
+
1008
+ this.lookupLeft = parseInt(field.x, 10);
1009
+
1010
+ this.lookupTop -= scrollTop - 2;
1011
+ this.lookupLeft -= scrollLeft;
1012
+
1013
+ let isCentered = false;
1014
+ if (this.lookupTop < 0) {
1015
+ const _scale = this.gsv.getScale();
1016
+ this.lookupTop = Math.max(
1017
+ 0,
1018
+ (window.innerHeight / _scale - this.lookupHeight) / 2
1019
+ );
1020
+ this.lookupLeft = Math.max(
1021
+ 0,
1022
+ (window.innerWidth / _scale - this.lookupWidth) / 2
1023
+ );
1024
+ isCentered = true;
1025
+ }
1026
+
1027
+ if (!isCentered) {
1028
+ if (
1029
+ this.lookupLeft + this.lookupWidth >
1030
+ this.container().nativeElement.scrollWidth
1031
+ ) {
1032
+ this.lookupLeft = this.lookupLeft - this.lookupWidth;
1033
+ }
1034
+
1035
+ if (this.lookupLeft < 0) {
1036
+ this.lookupLeft = 0;
1037
+ }
1038
+ }
1039
+
1040
+ this.lookup = field.lookup;
1041
+ this.textcommands = '';
1042
+ this.texttitle = '';
1043
+ this.showcommands = false;
1044
+ this.bdetail = true;
1045
+ this.showlookup = true;
1046
+ this.sendLookup();
1047
+ }
1048
+
1049
+ /** Resets all member fields to blank/default values (called when entering insert mode). */
1050
+ public setupFields(data: Member[]) {
1051
+ for (const input of data) {
1052
+ if (input.id !== undefined) {
1053
+ input.value = '';
1054
+ input.tag = '';
1055
+ input.initialvalue = '';
1056
+ // Use sS() so: DOM attribute, schema member AND backing data-module state
1057
+ // are all reset synchronously. executeCommand runs immediately after and
1058
+ // re-applies correct states via awaited fieldChanged calls, avoiding the
1059
+ // race condition that was caused by the old non-awaited fieldChanged here.
1060
+ this.sS(input.id, FieldState.None);
1061
+ }
1062
+ }
1063
+ }
1064
+
1065
+ /**
1066
+ * Sets the visual state of a field (setState). Also updates the underlying MemberData
1067
+ * in the data cache so that business logic and JSON serialisation remain in sync.
1068
+ */
1069
+ public sS(id: string, state: FieldState, message = '', dialog = false) {
1070
+ const field: Member = this.gF(id);
1071
+ if (field && !this.model.preview) {
1072
+ if (this.entity.members.length > 0) {
1073
+ if (
1074
+ this.gE(id) &&
1075
+ this.gE(id)!.getAttribute(FieldAttr.get('state')!) !== null
1076
+ ) {
1077
+ field.state = state;
1078
+ if (state === FieldState.ReadOnly) {
1079
+ this._readonlySet.add(id);
1080
+ } else if (state === FieldState.None) {
1081
+ this._readonlySet.delete(id);
1082
+ }
1083
+ this.gE(id)!.setAttribute(FieldAttr.get('state')!, state.toString());
1084
+ field.title = message;
1085
+ if (dialog && message !== '') {
1086
+ this.showMessage(message);
1087
+ } else {
1088
+ this.gsv.setDialog({ target: '', state: false });
1089
+ }
1090
+ if (this.data) {
1091
+ const member = this.getModuleMemberData(
1092
+ field.id,
1093
+ this.model.name.toLocaleLowerCase()
1094
+ );
1095
+ if (member) {
1096
+ member.state = state;
1097
+ }
1098
+ }
1099
+ }
1100
+ }
1101
+ }
1102
+ }
1103
+
1104
+ /** Returns the FieldType of the given field (getType). Defaults to Text if not found. */
1105
+ public gT(id: string): FieldType {
1106
+ return this.gF(id)?.type ?? FieldType.Text;
1107
+ }
1108
+
1109
+ /** Returns the current FieldState of the given field (getState). Defaults to None. */
1110
+ public gS(id: string): FieldState {
1111
+ return this.gF(id)?.state ?? FieldState.None;
1112
+ }
1113
+
1114
+ /**
1115
+ * Sets the display value of a field (setValue). Handles type-specific formatting
1116
+ * (dates, numbers, text) and keeps the backing MemberData and logic function in sync.
1117
+ */
1118
+ public sV(id: string, value: any) {
1119
+ const field: Member = this.gF(id);
1120
+ if (field !== undefined) {
1121
+ switch (this.gT(id)) {
1122
+ case FieldType.Date:
1123
+ field.value = value;
1124
+ if (value === '' || value === null) {
1125
+ field.value = '';
1126
+ field.tag = '';
1127
+ } else {
1128
+ value = value.replaceAll(' ', '');
1129
+ // If the value is in ISO format (YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss),
1130
+ // convert it to locale display format before further processing.
1131
+ const isoMatch =
1132
+ typeof value === 'string' &&
1133
+ value.match(/^(\d{4})-(\d{2})-(\d{2})/);
1134
+ if (isoMatch) {
1135
+ const fmt =
1136
+ this.gsv.getLng() === Languages.IT
1137
+ ? 'DD/MM/YYYY'
1138
+ : 'MM/DD/YYYY';
1139
+ value = this.formatDateFromISO(value.substr(0, 10), fmt);
1140
+ } else if (value.indexOf('/') === -1 && value.length > 0) {
1141
+ // User typed DDMMYYYY without separators — insert slashes.
1142
+ value =
1143
+ value.toString().substr(0, 2) +
1144
+ '/' +
1145
+ value.toString().substr(2, 2) +
1146
+ '/' +
1147
+ value.toString().substr(4);
1148
+ }
1149
+ const fmt =
1150
+ this.gsv.getLng() === Languages.IT ? 'DD/MM/YYYY' : 'MM/DD/YYYY';
1151
+ if (this.isValidDateString(value, fmt)) {
1152
+ field.value = value;
1153
+ } else {
1154
+ field.value = '';
1155
+ }
1156
+ field.tag = field.value;
1157
+ }
1158
+ break;
1159
+ case FieldType.Number:
1160
+ if (value === '' || value === '0') {
1161
+ field.value = '';
1162
+ field.tag = '';
1163
+ } else {
1164
+ const isIT = this.gsv.getLng() === Languages.IT;
1165
+ if (typeof value === 'string') {
1166
+ value = isIT
1167
+ ? value.replace(/\./g, '').replace(',', '.')
1168
+ : value.replace(/,/g, '');
1169
+ }
1170
+ const numVal =
1171
+ typeof value === 'number'
1172
+ ? value
1173
+ : parseFloat(String(value).replace(',', '.'));
1174
+ const fixed = numVal.toFixed(field.scale);
1175
+ field.tag = isIT ? fixed.replace('.', ',') : fixed;
1176
+ if (field.thousandseparator) {
1177
+ field.value = isIT
1178
+ ? numVal.toLocaleString('it-it', {
1179
+ minimumFractionDigits: field.scale,
1180
+ })
1181
+ : numVal.toLocaleString('en-us', {
1182
+ minimumFractionDigits: field.scale,
1183
+ });
1184
+ } else {
1185
+ field.value = field.tag;
1186
+ }
1187
+ }
1188
+ break;
1189
+ default:
1190
+ if (value !== null) {
1191
+ field.value = value.toString().toUpperCase();
1192
+ field.tag = value.toString().toUpperCase();
1193
+ } else {
1194
+ field.value = '';
1195
+ field.tag = '';
1196
+ }
1197
+ break;
1198
+ }
1199
+ if (this.data) {
1200
+ const member = this.getModuleMemberData(field.id);
1201
+ if (member) {
1202
+ if (
1203
+ member.initialvalue !== null &&
1204
+ member.initialvalue === member.value &&
1205
+ member.type === FieldType.Date &&
1206
+ typeof member.initialvalue === 'string' &&
1207
+ /^\d{4}-\d{2}-\d{2}/.test(member.initialvalue)
1208
+ ) {
1209
+ // Server returned an ISO date (with or without time); normalise
1210
+ // initialvalue to the display format so dirty-detection is correct.
1211
+ member.initialvalue = this.gV(id);
1212
+ }
1213
+ member.state = this.gS(id);
1214
+ member.value = this.gV(id);
1215
+ if (member.type === FieldType.Number && member.value === '') {
1216
+ member.value = 0;
1217
+ }
1218
+ this.logic.function(
1219
+ id + Functions.changed,
1220
+ this.lastKey,
1221
+ this.getKeyArray(),
1222
+ this.currentFieldId
1223
+ );
1224
+ if (id === FixedField.cfpiva || id === FixedField.modnum) {
1225
+ this.paddingFormattingRegEx(id);
1226
+ }
1227
+ }
1228
+ }
1229
+ }
1230
+ }
1231
+
1232
+ /** Converts an ISO date string to DD/MM/YYYY or MM/DD/YYYY depending on locale. */
1233
+ private formatDateFromISO(
1234
+ isoDate: string,
1235
+ format: 'DD/MM/YYYY' | 'MM/DD/YYYY'
1236
+ ): string {
1237
+ const d = new Date(isoDate);
1238
+ if (isNaN(d.getTime())) return '';
1239
+ const pad = (n: number) => String(n).padStart(2, '0');
1240
+ const day = pad(d.getUTCDate());
1241
+ const month = pad(d.getUTCMonth() + 1);
1242
+ const year = d.getUTCFullYear();
1243
+ return format === 'DD/MM/YYYY'
1244
+ ? `${day}/${month}/${year}`
1245
+ : `${month}/${day}/${year}`;
1246
+ }
1247
+
1248
+ /**
1249
+ * Converts a locale display date string (DD/MM/YYYY or MM/DD/YYYY) to ISO YYYY-MM-DD.
1250
+ * Returns an empty string if the value is empty or not a valid display date.
1251
+ */
1252
+ private displayDateToISO(value: string): string {
1253
+ if (!value || value.length !== 10) return value;
1254
+ const fmt =
1255
+ this.gsv.getLng() === Languages.IT ? 'DD/MM/YYYY' : 'MM/DD/YYYY';
1256
+ if (!this.isValidDateString(value, fmt)) return value;
1257
+ const parts = value.split('/');
1258
+ const [p1, p2, p3] = parts.map(Number);
1259
+ const [day, month, year] =
1260
+ fmt === 'DD/MM/YYYY' ? [p1, p2, p3] : [p2, p1, p3];
1261
+ const pad = (n: number) => String(n).padStart(2, '0');
1262
+ return `${year}-${pad(month)}-${pad(day)}`;
1263
+ }
1264
+
1265
+ /** Returns true if the given string is a valid date in the specified format. */
1266
+ private isValidDateString(
1267
+ value: string,
1268
+ format: 'DD/MM/YYYY' | 'MM/DD/YYYY'
1269
+ ): boolean {
1270
+ if (!value || value.length !== 10) return false;
1271
+ const parts = value.split('/');
1272
+ if (parts.length !== 3) return false;
1273
+ const [p1, p2, p3] = parts.map(Number);
1274
+ const [day, month, year] =
1275
+ format === 'DD/MM/YYYY' ? [p1, p2, p3] : [p2, p1, p3];
1276
+ if (isNaN(day) || isNaN(month) || isNaN(year)) return false;
1277
+ const d = new Date(year, month - 1, day);
1278
+ return (
1279
+ d.getFullYear() === year &&
1280
+ d.getMonth() === month - 1 &&
1281
+ d.getDate() === day
1282
+ );
1283
+ }
1284
+
1285
+ /** Returns the formatted string value (tag) of the given field (getValue). */
1286
+ public gV(id: string): any {
1287
+ const field: Member = this.gF(id);
1288
+ let retValue: any = '';
1289
+ if (field) {
1290
+ if (field.tag) {
1291
+ switch (this.gT(id)) {
1292
+ case FieldType.Number:
1293
+ retValue =
1294
+ String(field.value ?? '').trim().length === 0 ||
1295
+ String(field.value ?? '') === '0'
1296
+ ? 0
1297
+ : parseFloat(field.tag.toString().replace(',', '.'));
1298
+ break;
1299
+ case FieldType.Date:
1300
+ retValue = field.value;
1301
+ break;
1302
+ default:
1303
+ retValue = field.value;
1304
+ break;
1305
+ }
1306
+ }
1307
+ }
1308
+ return retValue === null ? '' : retValue;
1309
+ }
1310
+
1311
+ /** Returns the native DOM input element for the given field id (getElement). */
1312
+ public gE(id: string): HTMLInputElement {
1313
+ return document.getElementById(id) as HTMLInputElement;
1314
+ }
1315
+
1316
+ /** Returns the schema Member definition for the given field id (getField). */
1317
+ public gF(id: string): Member {
1318
+ if (this.entity !== null && this.entity.members !== undefined) {
1319
+ if (!this._memberMap) {
1320
+ this._memberMap = new Map(this.entity.members.map(m => [m.id, m]));
1321
+ }
1322
+ return this._memberMap.get(id)!;
1323
+ }
1324
+ return undefined!;
1325
+ }
1326
+
1327
+ /**
1328
+ * Validates whether the key field value is non-empty and unique within loaded modules.
1329
+ * On success marks the field ReadOnly and triggers commandData('create').
1330
+ */
1331
+ public checkKey(id: string) {
1332
+ if (this.gV(id) === '' || this.gV(id) === 0) {
1333
+ this.sS(id, FieldState.Fatal, this.getMessage('app.requiredkey'));
1334
+ this.showMessage(this.getMessage('app.requiredkey'));
1335
+ this.resetCommands();
1336
+ } else {
1337
+ this.sS(id, FieldState.None);
1338
+ const entity = this.getDataEntity(
1339
+ this.managment.entId.toLocaleLowerCase()
1340
+ );
1341
+ const prgLength = Math.max(0, entity.modules[0].prg.length - 1);
1342
+ let modulefiltered: ModuleData[];
1343
+ if (prgLength > 0) {
1344
+ modulefiltered = entity.modules.filter(
1345
+ e => e.prg[0] === this.getKeyArray()[0] && e.state === 1
1346
+ );
1347
+ } else {
1348
+ modulefiltered = entity.modules.filter(e => e.state === 1);
1349
+ }
1350
+ let count = 0;
1351
+ for (let i = 0; i < modulefiltered.length; i++) {
1352
+ const v = modulefiltered[i].members.find(
1353
+ e => e.name === id && e.value === this.gV(id)
1354
+ );
1355
+ if (v) {
1356
+ count++;
1357
+ }
1358
+ if (count > 1) {
1359
+ this.sS(id, FieldState.Fatal, this.getMessage('app.existskey'));
1360
+ this.showMessage(this.getMessage('app.existskey'));
1361
+ break;
1362
+ }
1363
+ }
1364
+ }
1365
+ if (this.gS(id) !== FieldState.Fatal) {
1366
+ this.sS(id, FieldState.ReadOnly);
1367
+ if (this.insertMode) {
1368
+ this.sendCommands();
1369
+ }
1370
+ }
1371
+ return false;
1372
+ }
1373
+
1374
+ /** Performs a server-side constraint check before deleting a module record. */
1375
+ public async checkConstraint(): Promise<boolean> {
1376
+ let retData: boolean = true;
1377
+ this.managment.functionId = 'checkconstraint';
1378
+ await this.msv.putDataPromise(this.managment).then(
1379
+ (data: TransportInterface) => {
1380
+ retData = data.booleanResult;
1381
+ },
1382
+ error => {}
1383
+ );
1384
+ return retData;
1385
+ }
1386
+
1387
+ /** Returns the localised message string for the given message key. */
1388
+ public getMessage(id: string): string {
1389
+ return this.msg.get(id);
1390
+ }
1391
+
1392
+ /** Displays the snackbar/dialog message overlay; optionally shows a confirm button. */
1393
+ public showMessage(
1394
+ message: string,
1395
+ confirm?: boolean,
1396
+ dialogType?: DialogType
1397
+ ) {
1398
+ const field: Member = this.entity.members.find(
1399
+ item => item.id === this.currentFieldId
1400
+ )!;
1401
+ this._dialogTarget = this.currentFieldId;
1402
+ this.gsv.setDialog({ target: this.currentFieldId, state: true });
1403
+ this.textdialog = message;
1404
+ this.dialogType = dialogType!;
1405
+ this.titledialog = this.msg.get('app.notify');
1406
+ this.dialogHeight = 200;
1407
+ this.dialogWidth = 400;
1408
+ const _scale = this.gsv.getScale();
1409
+ this.dialogTop = Math.max(0, (window.innerHeight / _scale - 200) / 2);
1410
+ this.dialogLeft = Math.max(0, (window.innerWidth / _scale - 400) / 2);
1411
+ this.showconfirm = confirm ?? false;
1412
+ this.showdialog = true;
1413
+ this.sendDialog();
1414
+ }
1415
+
1416
+ /**
1417
+ * Executes a navigation command (create, plus, minus, first, previous, next, last,
1418
+ * table, readed, print, …) on the entity module list and then calls executeCommand.
1419
+ */
1420
+ public async commandData(functionId: string): Promise<void> {
1421
+ const entity = this.getDataEntity(this.managment.entId.toLocaleLowerCase());
1422
+ const prgLength = Math.max(0, entity.modules[0].prg.length - 1);
1423
+
1424
+ if (functionId !== 'table') {
1425
+ const currentLinkItem: ItemLinkInterface = this.gsv.getCurrentLinkItem();
1426
+ if (
1427
+ currentLinkItem !== undefined &&
1428
+ currentLinkItem !== null &&
1429
+ this.entity.groupname !== '' &&
1430
+ this.entity.groupname === currentLinkItem.groupName
1431
+ ) {
1432
+ for (let i = 0; i < this.entity.prgdim; i++) {
1433
+ if (this.managment.keys !== '') {
1434
+ this.managment.keys += ';';
1435
+ }
1436
+ if (i === 0) {
1437
+ this.managment.keys = 'prg=' + currentLinkItem.prg[i];
1438
+ } else {
1439
+ this.managment.keys += 'prg=1';
1440
+ }
1441
+ }
1442
+ }
1443
+ }
1444
+
1445
+ let modulefiltered = this.getFilteredModules(entity);
1446
+
1447
+ switch (functionId) {
1448
+ case 'create':
1449
+ this.insertMode = false;
1450
+ this.btable = this.viewList || modulefiltered.length <= 1;
1451
+ this.bplus = !this.entity.allowadd;
1452
+ this.bminus = this.entity.allowdelete === 'none';
1453
+ if (!this.bplus) {
1454
+ this.updateNavButtons(modulefiltered.length);
1455
+ } else {
1456
+ this.updateNavButtons(0);
1457
+ }
1458
+ this.lastKey = modulefiltered.length - 1;
1459
+ this.setLastKey(
1460
+ this.getLastKeyFromModule(modulefiltered, prgLength) +
1461
+ modulefiltered.filter(m => m.state === 0).length
1462
+ );
1463
+ modulefiltered[modulefiltered.length - 1].prg = this.getKeyArray();
1464
+ modulefiltered[modulefiltered.length - 1].insert = true;
1465
+ break;
1466
+ case 'readed':
1467
+ if (!this.viewList || this.model.loaded) {
1468
+ this.insertMode =
1469
+ modulefiltered.length === 0 ||
1470
+ modulefiltered[this.lastKey]!.prg.length === 0 ||
1471
+ modulefiltered[this.lastKey]!.members[0]!.state ===
1472
+ FieldState.Fatal;
1473
+ this.btable =
1474
+ this.viewList ||
1475
+ this.insertMode ||
1476
+ modulefiltered.length === 0 ||
1477
+ this.model.masterDetail! > 0;
1478
+ this.bprint = !this.entity.printable;
1479
+ this.bplus = !this.entity.allowadd || this.insertMode;
1480
+ this.bminus = this.entity.allowdelete === 'none' || this.insertMode;
1481
+ if (modulefiltered.length === 0) {
1482
+ entity.modules.push(this.getEmptyModule());
1483
+ modulefiltered.push(entity.modules[entity.modules.length - 1]);
1484
+ this.lastKey = modulefiltered.length - 1;
1485
+ modulefiltered[modulefiltered.length - 1].prg = this.getKeyArray();
1486
+ } else {
1487
+ this.lastKey = Math.max(
1488
+ 0,
1489
+ modulefiltered.indexOf(
1490
+ modulefiltered.find(
1491
+ e => e.prg[prgLength] === this.getLastKey()
1492
+ )!
1493
+ )
1494
+ );
1495
+ }
1496
+ this.updateNavButtons(modulefiltered.length);
1497
+ if (modulefiltered[modulefiltered.length - 1].prg.length === 0) {
1498
+ modulefiltered[modulefiltered.length - 1].prg = this.getKeyArray();
1499
+ }
1500
+ this.btable = this.model.loaded && this.model.viewList ? false : true;
1501
+ await this.executeCommand(modulefiltered, functionId);
1502
+ } else {
1503
+ this.bplus = !this.entity.allowadd || this.insertMode;
1504
+ this.bprint = false;
1505
+ // Even in list view, run executeCommand so that updateState() fires
1506
+ // (nav-badge states) and lookupChecks validate the loaded data.
1507
+ if (modulefiltered.length === 0) {
1508
+ entity.modules.push(this.getEmptyModule());
1509
+ modulefiltered.push(entity.modules[entity.modules.length - 1]);
1510
+ this.lastKey = modulefiltered.length - 1;
1511
+ modulefiltered[modulefiltered.length - 1].prg = this.getKeyArray();
1512
+ } else {
1513
+ this.lastKey = 0;
1514
+ }
1515
+ await this.executeCommand(modulefiltered, functionId);
1516
+ }
1517
+ this.model.loaded = false;
1518
+ modulefiltered[modulefiltered.length - 1].insert = false;
1519
+ break;
1520
+ case 'plus':
1521
+ this.btable = true;
1522
+ this.updateNavButtons(0);
1523
+ this.bplus = true;
1524
+ this.bminus = true;
1525
+ this.insertMode = true;
1526
+ this.viewList = false;
1527
+ if (entity.modules[0].prg.length === 0) {
1528
+ entity.modules.splice(0, 1);
1529
+ }
1530
+ const newModule = this.getEmptyModule();
1531
+ if (prgLength > 0) {
1532
+ entity.modules.push(newModule);
1533
+ }
1534
+ modulefiltered.push(newModule);
1535
+ this.lastKey = modulefiltered.length - 1;
1536
+ this.setLastKey(
1537
+ this.getLastKeyFromModule(modulefiltered, prgLength) +
1538
+ modulefiltered.filter(m => m.state === 0).length
1539
+ );
1540
+ modulefiltered[modulefiltered.length - 1].prg = this.getKeyArray();
1541
+ modulefiltered[modulefiltered.length - 1].insert = true;
1542
+ this.setupFields(this.entity.members);
1543
+ await this.executeCommand(modulefiltered, functionId);
1544
+ break;
1545
+ case 'minus':
1546
+ modulefiltered[this.lastKey].state = 0;
1547
+ // Delete linkeds
1548
+ this.model.linkedItems?.forEach(linkedItemValue => {
1549
+ if (linkedItemValue !== '') {
1550
+ const linkedItem = this.data.entities.filter(
1551
+ e => e.name === linkedItemValue.toLocaleLowerCase()
1552
+ )[0];
1553
+ const linkedElements = linkedItem.modules.filter(
1554
+ e => e.prg[0] === this.getKeyArray()[0]
1555
+ );
1556
+ linkedElements.forEach(linkedElement => {
1557
+ linkedElement.state = 0;
1558
+ });
1559
+ }
1560
+ });
1561
+ if (
1562
+ (this.model.linkedItems ?? []).indexOf(
1563
+ this.model.name.toUpperCase()
1564
+ ) < 0
1565
+ ) {
1566
+ modulefiltered[this.lastKey]!.state = 0;
1567
+ }
1568
+ this.insertMode = true;
1569
+ modulefiltered = modulefiltered.filter(m => m.state === 1);
1570
+ const deleted = entity.modules.filter(
1571
+ e => e.prg[0] === this.getKeyArray()[0] && e.state === 0
1572
+ ).length;
1573
+ if (modulefiltered.length > 0) {
1574
+ this.lastKey = modulefiltered.length - 1;
1575
+ } else {
1576
+ await this.commandData('plus');
1577
+ }
1578
+ this.insertMode =
1579
+ modulefiltered.length === 0 ||
1580
+ modulefiltered[this.lastKey].prg.length === 0 ||
1581
+ modulefiltered[this.lastKey].members[0].state === FieldState.Fatal;
1582
+ this.btable = modulefiltered.length <= 1;
1583
+ this.bprint = !this.entity.printable;
1584
+ this.bplus = !this.entity.allowadd || this.insertMode;
1585
+ this.bminus = this.entity.allowdelete === 'none' || this.insertMode;
1586
+ this.updateNavButtons(modulefiltered.length);
1587
+ await this.executeCommand(modulefiltered, functionId);
1588
+ break;
1589
+ case 'table':
1590
+ this.lastKey = modulefiltered.indexOf(
1591
+ modulefiltered.find(e => e.prg[prgLength] === this.getLastKey())!
1592
+ );
1593
+ this.updateNavButtons(modulefiltered.length);
1594
+ this.bplus = !this.entity.allowadd || this.insertMode;
1595
+ this.bminus = this.entity.allowdelete === 'none' || this.insertMode;
1596
+ this.btable = false;
1597
+ this.bprint = this.viewList;
1598
+ await this.executeCommand(modulefiltered, functionId);
1599
+ break;
1600
+ case 'first':
1601
+ this.lastKey = 0;
1602
+ this.updateNavButtons(modulefiltered.length);
1603
+ await this.executeCommand(modulefiltered, functionId);
1604
+ break;
1605
+ case 'previous':
1606
+ this.lastKey--;
1607
+ this.updateNavButtons(modulefiltered.length);
1608
+ await this.executeCommand(modulefiltered, functionId);
1609
+ break;
1610
+ case 'next':
1611
+ this.lastKey++;
1612
+ this.updateNavButtons(modulefiltered.length);
1613
+ await this.executeCommand(modulefiltered, functionId);
1614
+ break;
1615
+ case 'last':
1616
+ this.lastKey = modulefiltered.length - 1;
1617
+ this.updateNavButtons(modulefiltered.length);
1618
+ await this.executeCommand(modulefiltered, functionId);
1619
+ break;
1620
+ default:
1621
+ this.resetData = false;
1622
+ await this.close().then(_ => {
1623
+ this.msv.putDataPromise(this.managment).then(
1624
+ (data: TransportInterface) => {
1625
+ this.insertMode = false;
1626
+ if (functionId === 'print') {
1627
+ this.gsv.setLoaderState(true);
1628
+ this.managment.fields = [];
1629
+ this.managment.functionId = 'print';
1630
+ this.msv.downloadFile(this.managment).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
1631
+ next: data => {
1632
+ switch (data.type) {
1633
+ case HttpEventType.DownloadProgress:
1634
+ break;
1635
+ case HttpEventType.Response:
1636
+ const blob = new Blob([data.body!], {
1637
+ type: data.body!.type,
1638
+ });
1639
+ saveAs(blob, uuidv4() + '.pdf');
1640
+ break;
1641
+ }
1642
+ },
1643
+ error: error => {},
1644
+ });
1645
+ this.gsv.setLoaderState(false);
1646
+ }
1647
+ this.setFocus(
1648
+ this.insertMode
1649
+ ? this.model.focusOnInsert
1650
+ : this.model.focusOnUpdate
1651
+ );
1652
+ this.managment.fields = [];
1653
+ this.managment.states = [];
1654
+ },
1655
+ error => {}
1656
+ );
1657
+ });
1658
+ break;
1659
+ }
1660
+
1661
+ this.gsv.setCurrentLinkItem({
1662
+ id: this.entity.name,
1663
+ groupName: this.entity.groupname,
1664
+ prg: this.getKeyArray(),
1665
+ });
1666
+ }
1667
+
1668
+ /**
1669
+ * Sets the four directional navigation buttons (first/prev/next/last) based on
1670
+ * the current `lastKey` position within the filtered module array.
1671
+ */
1672
+ private updateNavButtons(totalItems: number) {
1673
+ this.bfirst = true;
1674
+ this.bprevious = true;
1675
+ this.bnext = true;
1676
+ this.blast = true;
1677
+ if (totalItems > 1) {
1678
+ if (this.lastKey > 0) {
1679
+ this.bfirst = false;
1680
+ this.bprevious = false;
1681
+ }
1682
+ if (this.lastKey < totalItems - 1) {
1683
+ this.bnext = false;
1684
+ this.blast = false;
1685
+ }
1686
+ }
1687
+ }
1688
+
1689
+ /** Returns the filtered module list for the current entity, scoped to the active key. */
1690
+ private getFilteredModules(entity: EntityData): ModuleData[] {
1691
+ const prgLength = Math.max(0, entity.modules[0].prg.length - 1);
1692
+ if (prgLength > 0) {
1693
+ return entity.modules.filter(
1694
+ e => e.prg[0] === this.getKeyArray()[0] && e.state === 1
1695
+ );
1696
+ }
1697
+ return entity.modules;
1698
+ }
1699
+
1700
+ /** Pushes updated entity data to GlobalService and optionally triggers a grid refresh. */
1701
+ refreshData(force: boolean = false) {
1702
+ if (this.entity.masterdetail !== 0 || force) {
1703
+ this.gsv.setData(this.fieldsId, this.data);
1704
+ this.gsv.setData(
1705
+ this.managment.appId +
1706
+ '.' +
1707
+ this.managment.entId +
1708
+ '.' +
1709
+ this.managment.year,
1710
+ this.data
1711
+ );
1712
+ this.refreshgrid = !this.refreshgrid;
1713
+ this.gsv.emitGridRefresh();
1714
+ }
1715
+ this.sendCommands();
1716
+ }
1717
+
1718
+ /** Computes the next PRG key start index for a new module record. */
1719
+ getLastKeyFromModule(
1720
+ modulefiltered: ModuleData[],
1721
+ prgLength: number
1722
+ ): number {
1723
+ let startKey = 0;
1724
+ this.lastKey = modulefiltered.length - 1;
1725
+ if (this.lastKey > 0) {
1726
+ startKey = modulefiltered[this.lastKey - 1].prg[prgLength] + 1;
1727
+ } else {
1728
+ startKey = modulefiltered.length;
1729
+ }
1730
+ return startKey;
1731
+ }
1732
+
1733
+ /** Clones the first module record from the schema JSON to use as a blank template. */
1734
+ getEmptyModule(): ModuleData {
1735
+ const entity = this.gsv
1736
+ .getDataJson(this.getIdModel())
1737
+ .entities.filter(
1738
+ e => e.name === this.managment.entId.toLocaleLowerCase()
1739
+ )[0];
1740
+ return JSON.parse(JSON.stringify(entity.modules[0]));
1741
+ }
1742
+
1743
+ /** Returns the composite project key 'PROJECT.YEAR' used to look up schema/data caches. */
1744
+ getIdModel(): string {
1745
+ return this.gsv.getProject().project + '.' + this.gsv.getProject().year;
1746
+ }
1747
+
1748
+ /** Returns the composite entity cache key 'APP.ENT.YEAR'. */
1749
+ getEntIdModel(): string {
1750
+ return (
1751
+ this.managment.appId +
1752
+ '.' +
1753
+ this.managment.entId +
1754
+ '.' +
1755
+ this.managment.year
1756
+ );
1757
+ }
1758
+
1759
+ /**
1760
+ * Populates entity member fields from the given module, fires fieldChanged for each,
1761
+ * syncs initialvalue/initialstate after insert to avoid false-dirty detection, and
1762
+ * applies any pending logic refresh flags before moving focus.
1763
+ */
1764
+ async executeCommand(
1765
+ module: ModuleData[],
1766
+ functionId: string
1767
+ ): Promise<void> {
1768
+ this._readonlySet.clear();
1769
+ let entity = module[this.lastKey];
1770
+ for (let fld of entity.members) {
1771
+ const field: Member = this.entity.members.find(
1772
+ item => item.name === fld.name
1773
+ )!;
1774
+ if (field) {
1775
+ if (!field.visible) {
1776
+ this.sS(field.id, FieldState.Hidden);
1777
+ }
1778
+ switch (field.type) {
1779
+ case FieldType.Date:
1780
+ if (fld.value !== null && fld.value !== '' && typeof fld.value === 'string') {
1781
+ field.value =
1782
+ fld.value.length > 10
1783
+ ? this.formatDateFromISO(
1784
+ fld.value.substring(0, 10),
1785
+ this.gsv.getLng() === Languages.IT
1786
+ ? 'DD/MM/YYYY'
1787
+ : 'MM/DD/YYYY'
1788
+ )
1789
+ : fld.value;
1790
+ field.tag = new Date(fld.value);
1791
+ } else {
1792
+ field.value = null;
1793
+ field.tag = null;
1794
+ }
1795
+ break;
1796
+ case FieldType.Number:
1797
+ if (fld.value === 0) {
1798
+ field.value = '';
1799
+ field.tag = '';
1800
+ } else {
1801
+ field.value = fld.value;
1802
+ field.tag = parseFloat(String(fld.value));
1803
+ }
1804
+ break;
1805
+ case FieldType.Text:
1806
+ case FieldType.TextualCheck:
1807
+ case FieldType.Combo:
1808
+ field.value = fld.value;
1809
+ field.tag = fld.value;
1810
+ break;
1811
+ default:
1812
+ field.value = fld.value;
1813
+ field.tag = fld.value;
1814
+ break;
1815
+ }
1816
+ await this.fieldChanged({
1817
+ id: fld.name,
1818
+ value: String(field.value ?? ''),
1819
+ function: 'executeCommand',
1820
+ });
1821
+ }
1822
+ }
1823
+ if (!this.insertMode) {
1824
+ this.setLastKey(
1825
+ module[this.lastKey].prg[module[this.lastKey].prg.length - 1]
1826
+ );
1827
+ }
1828
+
1829
+ let fieldFocus =
1830
+ this.insertMode || !this.entity.members.find(_ => _.value !== '')
1831
+ ? this.entity.focusoninsert
1832
+ : this.entity.focusonupdate;
1833
+
1834
+ if (
1835
+ this.gsv.getCurrentFocus().id &&
1836
+ this.gsv.getCurrentFocus().id.length > 0
1837
+ ) {
1838
+ fieldFocus = this.gsv.getCurrentFocus().id;
1839
+ this.gsv.setCurrentFocus({ id: '', value: '' });
1840
+ }
1841
+
1842
+ this.logic.function(
1843
+ Functions.executedCommand + functionId,
1844
+ this.lastKey,
1845
+ this.getKeyArray(),
1846
+ this.currentFieldId
1847
+ );
1848
+
1849
+ // After all auto-fills/calculations on a new module, sync initialvalue so
1850
+ // that logic-auto-filled defaults are not treated as user changes. Without
1851
+ // this, close() → getJsonData() would detect value !== initialvalue and
1852
+ // send an empty record to the server even if the user never typed anything.
1853
+ // Exception: non-empty Date fields are intentionally excluded from the sync
1854
+ // so that mandatory today-filled dates are always serialised in the save
1855
+ // payload (member.initialvalue stays null → value !== initialvalue).
1856
+ if (module[this.lastKey]?.insert) {
1857
+ module[this.lastKey].members.forEach(m => {
1858
+ if (m.type === FieldType.Date && m.value !== null && m.value !== '') {
1859
+ return;
1860
+ }
1861
+ m.initialvalue = m.value;
1862
+ });
1863
+ }
1864
+
1865
+ // Apply any state/value changes queued via refresh flags (e.g. dynamicView
1866
+ // triggered by logic.function above). Sync initialstate so getJsonData()
1867
+ // does not mistake structural visibility changes for user edits.
1868
+ try {
1869
+ const _eDyn = this.getDataEntity(this.model.name.toLocaleLowerCase());
1870
+ const _idxDyn = this.getIndex();
1871
+ const _dynModule = _eDyn?.modules.filter(e => e.state === 1)[_idxDyn];
1872
+ const _isInsert = !!_dynModule?.insert;
1873
+ const _toRefresh = _dynModule?.members.filter(m => m.refresh) ?? [];
1874
+ _toRefresh.forEach(m => {
1875
+ m.initialstate = m.state;
1876
+ // Do not sync initialvalue for Date fields whose value is non-empty but
1877
+ // whose initialvalue is still null. This means the date was set by the
1878
+ // mandatory-today logic over a null server value and must remain dirty
1879
+ // so that getJsonData() includes it in the save payload.
1880
+ if (
1881
+ !(
1882
+ m.type === FieldType.Date &&
1883
+ m.value !== null &&
1884
+ m.value !== '' &&
1885
+ m.initialvalue === null
1886
+ )
1887
+ ) {
1888
+ m.initialvalue = m.value;
1889
+ }
1890
+ this.sS(m.name, m.state);
1891
+ this.sV(m.name, m.value);
1892
+ if (m.message) this.gF(m.name).title = m.message;
1893
+ m.refresh = false;
1894
+ });
1895
+ } catch (e) {
1896
+ console.error('executeCommand refresh-flags error:', e);
1897
+ }
1898
+
1899
+ // General logic.calc() pass: run calc for every field in the loaded module so
1900
+ // that state changes expressed in calc() (ReadOnly, Error, Warning, Hidden…)
1901
+ // are applied on initial load for ALL models — not only those that use the
1902
+ // logic.function(executedCommand*) / refresh-flag mechanism.
1903
+ try {
1904
+ const _calcIdx = this.getIndex();
1905
+ for (const fld of entity.members) {
1906
+ this.logic.calc(fld.name, _calcIdx, this.getKeyArray());
1907
+ }
1908
+ // Apply refresh flags accumulated by the calc pass above.
1909
+ const _eCalc = this.getDataEntity(this.model.name.toLocaleLowerCase());
1910
+ const _calcModule = _eCalc?.modules.filter(e => e.state === 1)[_calcIdx];
1911
+ if (_calcModule) {
1912
+ _calcModule.members
1913
+ .filter(m => m.refresh)
1914
+ .forEach(m => {
1915
+ m.initialstate = m.state;
1916
+ // Do not sync initialvalue for Date fields whose value is non-empty but
1917
+ // whose initialvalue is still null. This means the date was set by the
1918
+ // mandatory-today logic over a null server value (either new insert or
1919
+ // existing record that never had a date) and must remain dirty so that
1920
+ // getJsonData() includes it in the save payload.
1921
+ if (
1922
+ !(
1923
+ m.type === FieldType.Date &&
1924
+ m.value !== null &&
1925
+ m.value !== '' &&
1926
+ m.initialvalue === null
1927
+ )
1928
+ ) {
1929
+ m.initialvalue = m.value;
1930
+ }
1931
+ this.sS(m.name, m.state);
1932
+ this.sV(m.name, m.value);
1933
+ if (m.message) this.gF(m.name).title = m.message;
1934
+ m.refresh = false;
1935
+ });
1936
+ }
1937
+ } catch (e) {
1938
+ console.error('executeCommand calc-pass error:', e);
1939
+ }
1940
+
1941
+ // Re-apply business-logic states (Error/Warning) that were wiped by the
1942
+ // fieldChanged('executeCommand') loop above, which resets any Error state
1943
+ // to None before logic.calc has a chance to re-set it.
1944
+ if (functionId === 'create' && this.currentFieldId) {
1945
+ this.fieldBusiness(this.currentFieldId);
1946
+ }
1947
+
1948
+ this.save = true;
1949
+ this.updateState();
1950
+ this.refreshData(true);
1951
+ this.setFocus(fieldFocus);
1952
+ this.gsv.setLoaderState(false);
1953
+ }
1954
+
1955
+ /**
1956
+ * Serialises only changed (or deleted) members and modules to a JSON string
1957
+ * suitable for PUT /managment. Returns an empty string when there is nothing to save.
1958
+ */
1959
+ getJsonData(): string {
1960
+ if (this.data) {
1961
+ let hasData = false;
1962
+ let json = '{"entities":[';
1963
+ this.data.entities.forEach(entity => {
1964
+ let modules = 0;
1965
+ let hasModule = false;
1966
+ entity.modules.forEach((module, mi) => {
1967
+ let members = 0;
1968
+ const fatalMember = module.members.find(
1969
+ i => i.state === FieldState.Fatal
1970
+ );
1971
+ if (fatalMember) {
1972
+ // Only mark as deleted if the module has actual server data
1973
+ // (at least one member whose value differs from initialvalue).
1974
+ // Template modules created locally but never committed should not
1975
+ // generate @deleted markers that trigger spurious save requests.
1976
+ const hasServerData = module.members.some(
1977
+ m =>
1978
+ m.value !== m.initialvalue ||
1979
+ (m.state !== m.initialstate && m.state !== FieldState.Fatal)
1980
+ );
1981
+ if (hasServerData) {
1982
+ module.state = 0;
1983
+ } else {
1984
+ return;
1985
+ }
1986
+ }
1987
+ if (module.state === 0) {
1988
+ module.members = [];
1989
+ }
1990
+ if (module.members.length === 0) {
1991
+ if (modules === 0 && members == 0) {
1992
+ if (json !== '{"entities":[') {
1993
+ json += ',';
1994
+ }
1995
+ json += '{"name":"' + entity.name + '","modules":[';
1996
+ }
1997
+ if (modules > 0 && members === 0) {
1998
+ json += ',';
1999
+ }
2000
+ if (members === 0) {
2001
+ json += '{ "members": [';
2002
+ }
2003
+ if (members > 0) {
2004
+ json += ',';
2005
+ }
2006
+ json += '{"name":"@deleted"}';
2007
+ hasData = true;
2008
+ members++;
2009
+ }
2010
+ module.members.forEach(member => {
2011
+ if (
2012
+ (member?.value !== member?.initialvalue ||
2013
+ member?.initialstate !== member?.state) &&
2014
+ member.type !== FieldType.Label &&
2015
+ member.type !== FieldType.Button &&
2016
+ member.type !== FieldType.Video &&
2017
+ member.type !== FieldType.Image &&
2018
+ member.type !== FieldType.Map &&
2019
+ member.type !== FieldType.Chart
2020
+ ) {
2021
+ if (modules === 0 && members == 0) {
2022
+ if (json !== '{"entities":[') {
2023
+ json += ',';
2024
+ }
2025
+ json += '{"name":"' + entity.name + '","modules":[';
2026
+ }
2027
+ if (modules > 0 && members === 0) {
2028
+ json += ',';
2029
+ }
2030
+ if (members === 0) {
2031
+ json += '{ "members": [';
2032
+ }
2033
+ if (members > 0) {
2034
+ json += ',';
2035
+ }
2036
+ const _serializedValue =
2037
+ member.type === FieldType.Date && member.value
2038
+ ? this.displayDateToISO(member.value.toString())
2039
+ : member.value
2040
+ ? member.value.toString().toUpperCase()
2041
+ : '';
2042
+ json +=
2043
+ '{"name":"' +
2044
+ member.name +
2045
+ '", "value":' +
2046
+ '"' +
2047
+ _serializedValue +
2048
+ '"' +
2049
+ ',"state":' +
2050
+ member.state +
2051
+ ',"initialstate":' +
2052
+ member.initialstate +
2053
+ '}';
2054
+ member.initialvalue = member.value;
2055
+ member.initialstate = member.state;
2056
+ members++;
2057
+ hasData = true;
2058
+ }
2059
+ });
2060
+ if (module.prg.length === 0) {
2061
+ module.prg = this.getKeyArray();
2062
+ }
2063
+ if (members > 0) {
2064
+ json += '],"prg":[' + module.prg + ']}';
2065
+ modules++;
2066
+ hasModule = true;
2067
+ }
2068
+ });
2069
+ if (hasModule) {
2070
+ json += ']}';
2071
+ }
2072
+ });
2073
+ json += ']}';
2074
+ return hasData ? json : '';
2075
+ }
2076
+ return '';
2077
+ }
2078
+
2079
+ /** Parses the composite PRG key string into a numeric array (e.g. 'prg=1;prg=2' → [1,2]). */
2080
+ public getKeyArray(): number[] {
2081
+ if (
2082
+ this._cachedKeyArray === null ||
2083
+ this._cachedKeyString !== this.managment.keys
2084
+ ) {
2085
+ this._cachedKeyString = this.managment.keys;
2086
+ this._cachedKeyArray = this.managment.keys
2087
+ .split(';')
2088
+ .map(e => parseInt(e.replace('prg=', ''), 10));
2089
+ }
2090
+ return this._cachedKeyArray;
2091
+ }
2092
+
2093
+ /** Returns the last (innermost) PRG key from the composite key string. */
2094
+ public getLastKey(): number {
2095
+ const keys = this.managment.keys.split(';');
2096
+ return parseInt(keys[keys.length - 1].replace('prg=', ''));
2097
+ }
2098
+
2099
+ /** Replaces the last PRG segment in the composite key string with the given value. */
2100
+ public setLastKey(lastKey: number) {
2101
+ const keyElements = this.managment.keys.split(';');
2102
+ keyElements[keyElements.length - 1] = 'prg=' + lastKey;
2103
+ this.managment.keys = keyElements.join(';');
2104
+ }
2105
+
2106
+ /** Resets the dialog overlay state and restores focus after the user dismisses the dialog. */
2107
+ public dialogResult(rsp: DialogResultEvent) {
2108
+ this.textdialog = '';
2109
+ this.titledialog = '';
2110
+ this.showconfirm = false;
2111
+ this.showdialog = false;
2112
+ const target = this._dialogTarget;
2113
+ this._dialogTarget = '';
2114
+ if (target !== '') {
2115
+ if (this.gS(target) === FieldState.Fatal) {
2116
+ this.sV(target, '');
2117
+ this.sS(target, FieldState.None);
2118
+ }
2119
+ this.setFocus(target);
2120
+ } else {
2121
+ this.setFocus(
2122
+ this.insertMode ? this.entity.focusoninsert : this.entity.focusonupdate
2123
+ );
2124
+ }
2125
+ this.gsv.setDialog({ target: '', state: false });
2126
+ }
2127
+
2128
+ /**
2129
+ * Updates the active field ID and shows/hides the lookup trigger button
2130
+ * when the focused field has an associated lookup entity.
2131
+ */
2132
+ private currentField(currentField: Member) {
2133
+ if (
2134
+ currentField &&
2135
+ this.entity &&
2136
+ this.entity.members &&
2137
+ currentField.id !== this.currentFieldId
2138
+ ) {
2139
+ this.currentFieldId = currentField.id;
2140
+ const field: Member = this.entity.members.find(
2141
+ item => item.id === currentField.id
2142
+ )!;
2143
+ this.textcommands = '';
2144
+ this.texttitle = '';
2145
+ this.showcommands = false;
2146
+ this.bdetail = true;
2147
+ this.bintodetail = true;
2148
+ if (field && field.lookup !== '') {
2149
+ this.lookupField = currentField;
2150
+ this.commandsHeight = parseInt(field.height, 10);
2151
+ this.commandsWidth = 12;
2152
+ this.commandsTop = parseInt(field.y, 10);
2153
+ this.commandsLeft =
2154
+ parseInt(field.x, 10) + parseInt(field.width, 10) - 12;
2155
+ this.textcommands = '...';
2156
+ this.texttitle = field.lookup.split('#')[6];
2157
+ this.showcommands = true;
2158
+ this.bdetail = false;
2159
+ this.bintodetail = false;
2160
+ }
2161
+ field.value = field?.tag;
2162
+ if (field?.autoformatter) {
2163
+ const domEl = this.gE(field.id);
2164
+ if (domEl) domEl.value = String(field.tag ?? '');
2165
+ }
2166
+ this.sendCommands();
2167
+ }
2168
+ }
2169
+
2170
+ /**
2171
+ * Core field-change pipeline: normalises the input value (padding, regex, lookup),
2172
+ * persists it to MemberData, runs business logic, and schedules DOM updates.
2173
+ */
2174
+ private async fieldChanged(currentFieldChanged: ItemFieldChangedInterface) {
2175
+ if (
2176
+ currentFieldChanged.function === 'asyncRefresh' ||
2177
+ (this.showdialog && currentFieldChanged.function === 'sendChanged') ||
2178
+ (currentFieldChanged.function === 'sendChanged' &&
2179
+ this.gF(currentFieldChanged.id)?.lookupoutput &&
2180
+ !this.gF(currentFieldChanged.id)?.lookup &&
2181
+ this.gS(currentFieldChanged.id) === FieldState.ReadOnly)
2182
+ ) {
2183
+ if (currentFieldChanged.function === 'asyncRefresh') {
2184
+ this.cdr.detectChanges();
2185
+ }
2186
+ return;
2187
+ }
2188
+ if (this.entity) {
2189
+ const id = currentFieldChanged.id;
2190
+ const field: Member = this.entity?.members?.find(item => item.id === id)!;
2191
+ if (field) {
2192
+ if (
2193
+ (this.gS(currentFieldChanged.id) === FieldState.Error ||
2194
+ field.function !== 'none') &&
2195
+ this.gS(currentFieldChanged.id) !== FieldState.Forced &&
2196
+ this.gS(currentFieldChanged.id) !== FieldState.ReadOnly
2197
+ ) {
2198
+ this.sS(currentFieldChanged.id, FieldState.None);
2199
+ }
2200
+ if (this.gE(field.id)) {
2201
+ this.sV(id, currentFieldChanged.value);
2202
+ switch (field.type) {
2203
+ case FieldType.Text:
2204
+ if (field.generateuid && field.value === '') {
2205
+ this.sV(field.id, uuidv4());
2206
+ }
2207
+ break;
2208
+ case FieldType.Number:
2209
+ if (field.generateuid && field.value === '') {
2210
+ this.sV(
2211
+ field.id,
2212
+ parseInt(this.managment.keys.replace('prg=', ''), 0)
2213
+ );
2214
+ }
2215
+ break;
2216
+ }
2217
+ if (
2218
+ field.groupname !== '' &&
2219
+ field.value !== '' &&
2220
+ field.groupname !== undefined
2221
+ ) {
2222
+ for (let i = 0; i < this.entity.members.length; i++) {
2223
+ if (
2224
+ this.entity.members[i].groupname === field.groupname &&
2225
+ this.entity.members[i].id !== field.id
2226
+ ) {
2227
+ this.sV(this.entity.members[i].id, '');
2228
+ }
2229
+ }
2230
+ }
2231
+ // Controlli di livello generale
2232
+ switch (field.type) {
2233
+ case FieldType.Date:
2234
+ if (
2235
+ field.mandatory === true &&
2236
+ this.gV(id) === '' &&
2237
+ this.gS(id) !== FieldState.Forced
2238
+ ) {
2239
+ if (this.gS(currentFieldChanged.id) === FieldState.Error) {
2240
+ this.sS(currentFieldChanged.id, FieldState.None);
2241
+ }
2242
+ if (field.today) {
2243
+ const d = new Date();
2244
+ const dd = String(d.getDate()).padStart(2, '0');
2245
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
2246
+ const yyyy = d.getFullYear();
2247
+ const dataStr = this.gsv.getLng() === Languages.IT
2248
+ ? `${dd}/${mm}/${yyyy}`
2249
+ : `${mm}/${dd}/${yyyy}`;
2250
+ this.sV(id, dataStr);
2251
+ } else {
2252
+ this.sS(id, FieldState.Error, this.msg.get('app.required'));
2253
+ }
2254
+ } else if (this.gV(id).length < 10 && this.gV(id).length > 0) {
2255
+ this.sS(id, FieldState.Error, this.msg.get('app.dateerror'));
2256
+ } else if (
2257
+ this.gV(id) &&
2258
+ !this.isValidDateString(
2259
+ this.gV(id),
2260
+ this.gsv.getLng() === Languages.IT
2261
+ ? 'DD/MM/YYYY'
2262
+ : 'MM/DD/YYYY'
2263
+ )
2264
+ ) {
2265
+ this.sS(id, FieldState.Error, this.msg.get('app.dateerror'));
2266
+ }
2267
+ break;
2268
+ case FieldType.Text:
2269
+ if (
2270
+ field.mandatory === true &&
2271
+ this.gV(id) === '' &&
2272
+ this.gS(id) !== FieldState.Forced
2273
+ ) {
2274
+ this.sS(id, FieldState.Error, this.msg.get('app.required'));
2275
+ }
2276
+ if (
2277
+ field.function === 'cf' &&
2278
+ this.gS(id) !== FieldState.Forced
2279
+ ) {
2280
+ if (field.value !== '') {
2281
+ // Strip autoformatter spaces before structural validation so
2282
+ // that a spaced display value (e.g. "R S S M ...") validates
2283
+ // correctly against the raw 16-char CF string.
2284
+ const cfRaw = String(field.value ?? '').replace(/\s/g, '');
2285
+ try {
2286
+ const cf = new CodiceFiscale(cfRaw);
2287
+ } catch (err) {
2288
+ // BUG-FIX: was `if (this.gS(id))` — FieldState.None === 0 is
2289
+ // falsy, so the error was never set after the reset-to-None
2290
+ // that always runs at the top of fieldChanged. Only skip if
2291
+ // the field is already in a Fatal state (key field).
2292
+ if (this.gS(id) !== FieldState.Fatal) {
2293
+ this.sS(id, FieldState.Error, this.msg.get('app.cferr'));
2294
+ }
2295
+ }
2296
+ }
2297
+ }
2298
+ if (
2299
+ field.function === 'piva' &&
2300
+ this.gS(id) !== FieldState.Forced
2301
+ ) {
2302
+ // Strip autoformatter spaces before validation.
2303
+ const pivaRaw = String(field.value ?? '').replace(/\s/g, '');
2304
+ const piva: PartitaIVA = new PartitaIVA(pivaRaw);
2305
+ if (piva.Check().length !== 0) {
2306
+ this.sS(id, FieldState.Error, this.msg.get('app.pivaerr'));
2307
+ }
2308
+ }
2309
+ if (
2310
+ field.function === 'cfpiva' &&
2311
+ this.gS(id) !== FieldState.Forced
2312
+ ) {
2313
+ if (field.value !== '') {
2314
+ // Strip autoformatter spaces so the raw value is validated.
2315
+ // A 16-char CF with spacing:1 becomes 31 chars in the DOM;
2316
+ // without stripping, it would incorrectly fall into the PIVA
2317
+ // branch and get flagged as an error.
2318
+ const cfpivaRaw = String(field.value ?? '').replace(/\s/g, '');
2319
+ let error = false;
2320
+ try {
2321
+ if (cfpivaRaw.length === 16) {
2322
+ const cf = new CodiceFiscale(cfpivaRaw);
2323
+ error = false;
2324
+ } else {
2325
+ const piva: PartitaIVA = new PartitaIVA(cfpivaRaw);
2326
+ const cfpiva = piva.Check().length !== 0;
2327
+ if (cfpiva) {
2328
+ error = true;
2329
+ }
2330
+ }
2331
+ } catch (err) {
2332
+ error = true;
2333
+ }
2334
+ if (error) {
2335
+ this.sS(
2336
+ id,
2337
+ FieldState.Error,
2338
+ this.msg.get('app.cfpivaerr')
2339
+ );
2340
+ } else {
2341
+ this.sS(id, FieldState.None, '');
2342
+ }
2343
+ }
2344
+ }
2345
+ if (
2346
+ field.function === 'mail' &&
2347
+ this.gS(id) !== FieldState.Forced
2348
+ ) {
2349
+ if (field.value != '') {
2350
+ // Strip spaces (autoformatter artefacts) before regex check.
2351
+ const mailRaw = String(field.value ?? '').replace(/\s/g, '');
2352
+ if (!mailRaw.match('[^s@]+@[^s@]+.[^s@]+$')) {
2353
+ this.sS(id, FieldState.Error, this.msg.get('app.emailerr'));
2354
+ }
2355
+ }
2356
+ }
2357
+ break;
2358
+ case FieldType.Number:
2359
+ if (
2360
+ field.mandatory === true &&
2361
+ this.gV(id) === 0 &&
2362
+ this.gS(id) !== FieldState.Forced
2363
+ ) {
2364
+ this.sS(id, FieldState.Error, this.msg.get('app.required'));
2365
+ }
2366
+ if (field.min !== '' && this.gS(id) !== FieldState.Forced) {
2367
+ const minV = parseFloat(String(field.min));
2368
+ if (minV !== 0) {
2369
+ if (this.gV(id) < minV) {
2370
+ this.sS(id, FieldState.Error, this.msg.get('app.rangemin'));
2371
+ }
2372
+ }
2373
+ }
2374
+ if (field.max !== '' && this.gS(id) !== FieldState.Forced) {
2375
+ const maxV = parseFloat(String(field.max));
2376
+ if (maxV !== 0) {
2377
+ if (this.gV(id) > maxV) {
2378
+ this.sS(id, FieldState.Error, this.msg.get('app.rangemax'));
2379
+ }
2380
+ }
2381
+ }
2382
+ break;
2383
+ }
2384
+ if (
2385
+ field.lookup !== '' &&
2386
+ !this.showlookup &&
2387
+ currentFieldChanged.function !== 'noBusiness' &&
2388
+ currentFieldChanged.function !== 'lookupCheck'
2389
+ ) {
2390
+ const lkmanagment: ManagmentInterface =
2391
+ this.gsv.getManagmentInterface();
2392
+ const lkinfo: string[] = field.lookup.split('#');
2393
+ const input: string[] = lkinfo[3].split(',');
2394
+ const output: string[] = lkinfo[4].split(',');
2395
+ const extern: string[] = lkinfo[5].split(',');
2396
+ lkmanagment.fields = [];
2397
+ lkmanagment.functionId = 'read';
2398
+ lkmanagment.appId = lkinfo[0].toUpperCase();
2399
+ lkmanagment.year = parseInt(lkinfo[1], 10);
2400
+ lkmanagment.entId = lkinfo[2].toUpperCase();
2401
+ lkmanagment.keys = 'prg=0';
2402
+ const lkFieldsId =
2403
+ lkmanagment.appId +
2404
+ '.' +
2405
+ lkmanagment.entId +
2406
+ '.' +
2407
+ lkmanagment.year;
2408
+ const lkdata = this.gsv.getData(lkFieldsId);
2409
+ let resolvedData = lkdata;
2410
+ // During executeCommand (initial data load from server), skip the
2411
+ // expensive HTTP fetch — values are already valid. If lookup data is
2412
+ // cached we still run lookupCheck to set ReadOnly states; if not
2413
+ // cached we just apply ReadOnly to lookupoutput fields directly.
2414
+ if (currentFieldChanged.function === 'executeCommand' && !lkdata) {
2415
+ for (let i = 1; i < output.length; i++) {
2416
+ if (this.gF(output[i])?.lookupoutput) {
2417
+ this.sS(
2418
+ output[i],
2419
+ this.gS(output[i]) !== this.fieldState.Hidden
2420
+ ? this.fieldState.ReadOnly
2421
+ : this.fieldState.Hidden
2422
+ );
2423
+ }
2424
+ }
2425
+ } else {
2426
+ if (
2427
+ !lkdata ||
2428
+ (this.gsv.getSchemaJson(
2429
+ lkmanagment.appId + '.' + lkmanagment.year
2430
+ ) !== undefined &&
2431
+ !this.gsv
2432
+ .getSchemaJson(lkmanagment.appId + '.' + lkmanagment.year)
2433
+ .entities.find(
2434
+ e => e.name == lkmanagment.entId.toLocaleLowerCase()
2435
+ )!.cache)
2436
+ ) {
2437
+ if (currentFieldChanged.function !== 'executeCommand') {
2438
+ const data = await firstValueFrom(
2439
+ this.msv.getData(lkmanagment)
2440
+ );
2441
+ const dataFile = JSON.parse(data.dataFile as string);
2442
+ this.gsv.setData(lkFieldsId, dataFile);
2443
+ resolvedData = dataFile;
2444
+ }
2445
+ }
2446
+ if (resolvedData) {
2447
+ const module = resolvedData!.entities.filter(
2448
+ (e: any) => e.name === lkmanagment.entId.toLocaleLowerCase()
2449
+ )[0];
2450
+ await this.lookupCheck(
2451
+ module.modules,
2452
+ field,
2453
+ input,
2454
+ output,
2455
+ extern,
2456
+ currentFieldChanged.function
2457
+ );
2458
+ } else if (currentFieldChanged.function === 'executeCommand') {
2459
+ // No cached data and fetch was skipped — still mark lookupoutput fields ReadOnly.
2460
+ for (let i = 1; i < output.length; i++) {
2461
+ if (this.gF(output[i])?.lookupoutput) {
2462
+ this.sS(
2463
+ output[i],
2464
+ this.gS(output[i]) !== this.fieldState.Hidden
2465
+ ? this.fieldState.ReadOnly
2466
+ : this.fieldState.Hidden
2467
+ );
2468
+ }
2469
+ }
2470
+ }
2471
+ this.cdr.detectChanges();
2472
+ }
2473
+ }
2474
+ }
2475
+ this.paddingFormattingRegEx(id);
2476
+ if (
2477
+ this.gF(id).tabindex == 0 &&
2478
+ this.model.rootModel &&
2479
+ currentFieldChanged.function === 'sendChanged' &&
2480
+ !this.viewList
2481
+ ) {
2482
+ this.checkKey(id);
2483
+ }
2484
+ if (currentFieldChanged.function === 'sendChanged') {
2485
+ if (this.insertMode && this.gS(id) !== FieldState.Fatal) {
2486
+ if (!this.insertMode) {
2487
+ this.commandData('create');
2488
+ this.sendCommands();
2489
+ }
2490
+ }
2491
+ const preBusinessState = this.gS(id);
2492
+ const preBusinessMsg = this.gF(id)?.title ?? '';
2493
+ this.fieldBusiness(id);
2494
+ // Preserve validation Error/Warning that dynamicView/calc may have
2495
+ // overridden back to None via refresh flags.
2496
+ if (
2497
+ (preBusinessState === FieldState.Error ||
2498
+ preBusinessState === FieldState.Warning) &&
2499
+ this.gS(id) === FieldState.None
2500
+ ) {
2501
+ this.sS(id, preBusinessState, preBusinessMsg);
2502
+ }
2503
+ }
2504
+ }
2505
+ }
2506
+ }
2507
+
2508
+ /** Padding, formatting, regex */
2509
+ private paddingFormattingRegEx(id: string) {
2510
+ this.PadSpacing(id);
2511
+ const regExpRet = this.regExpression(id);
2512
+ if (!regExpRet) {
2513
+ this.sS(id, FieldState.Error, this.msg.get('app.regexpnotmatch'));
2514
+ }
2515
+ }
2516
+
2517
+ /** Computes the flat module index accounting for multi-dimensional PRG arrays. */
2518
+ private getIndex(): number {
2519
+ let index = this.lastKey;
2520
+ const entity = this.getDataEntity(this.model.name.toLocaleLowerCase());
2521
+ if (entity && this.model.prgDim > 1) {
2522
+ index = Math.max(
2523
+ 0,
2524
+ entity.modules.indexOf(
2525
+ entity.modules.filter(e => e.prg[0] === this.getKeyArray()[0])[
2526
+ this.lastKey
2527
+ ]
2528
+ )
2529
+ );
2530
+ }
2531
+ return index;
2532
+ }
2533
+
2534
+ /**
2535
+ * Runs logic.calc for the current field, then applies any member refresh flags
2536
+ * (state/value/message) marked by the logic layer in the active module.
2537
+ */
2538
+ private fieldBusiness(id: string, calc: boolean = true) {
2539
+ let index = this.getIndex();
2540
+ if (calc) {
2541
+ this.logic.calc(id, index, this.getKeyArray());
2542
+ }
2543
+ const entity = this.getDataEntity(this.model.name.toLocaleLowerCase());
2544
+ if (!entity?.modules?.length) return;
2545
+ const modulefiltered = this.getFilteredModules(entity);
2546
+ const currentModule = modulefiltered[this.lastKey];
2547
+ if (!currentModule) return;
2548
+ const membersToRefresh = currentModule.members.filter(m => m.refresh);
2549
+ if (membersToRefresh?.length > 0) {
2550
+ membersToRefresh.forEach(member => {
2551
+ this.sS(member.name, member.state);
2552
+ this.sV(member.name, member.value);
2553
+ if (member.message && member.message !== '') {
2554
+ this.gF(member.name).title = member.message;
2555
+ }
2556
+ member.refresh = false;
2557
+ this.video()?.nativeElement?.load();
2558
+ this.dataChanged(member);
2559
+ });
2560
+ }
2561
+ this.dataChanged(currentModule.members.find(m => m.name === id)!);
2562
+ this.updateState();
2563
+ }
2564
+
2565
+ /**
2566
+ * Returns true if the given member has unsaved changes (value or state differ from
2567
+ * initial values), updating `this.save` as a side-effect.
2568
+ */
2569
+ private dataChanged(member: MemberData): boolean {
2570
+ if (this.loaded) {
2571
+ if (
2572
+ ((member?.initialvalue !== null &&
2573
+ member?.value !== member?.initialvalue) ||
2574
+ member?.initialstate !== member?.state) &&
2575
+ member.type !== FieldType.Label &&
2576
+ member.type !== FieldType.Button &&
2577
+ member.type !== FieldType.Video &&
2578
+ member.type !== FieldType.Image &&
2579
+ member.type !== FieldType.Map &&
2580
+ member.type !== FieldType.Chart
2581
+ ) {
2582
+ this.save = false;
2583
+ }
2584
+ }
2585
+ return !this.save;
2586
+ }
2587
+
2588
+ /**
2589
+ * Validates the current field value against the referenced lookup entity.
2590
+ * On match, propagates output field values; on mismatch, sets Error state.
2591
+ */
2592
+ private async lookupCheck(
2593
+ lkdata: ModuleData[],
2594
+ field: Member,
2595
+ input: string[],
2596
+ output: string[],
2597
+ extern: string[],
2598
+ functionId: string
2599
+ ) {
2600
+ for (let i = 1; i < input.length; i++) {
2601
+ if (
2602
+ this.gS(output[i]) !== FieldState.Forced ||
2603
+ this.gV(output[0]) === ''
2604
+ ) {
2605
+ this.sV(output[i], '');
2606
+ } else {
2607
+ if (this.gV(output[i])) {
2608
+ output[i] = '';
2609
+ input[i] = '';
2610
+ }
2611
+ }
2612
+ }
2613
+ if (field.value !== '' && lkdata) {
2614
+ let elem = lkdata.find(item =>
2615
+ item.members?.find(
2616
+ e => e.name === input[0] && e.value === this.gV(field.id)
2617
+ )
2618
+ );
2619
+ if (!elem) {
2620
+ this.sS(field.id, FieldState.Error, this.msg.get('app.itemerror'));
2621
+ for (let i = 0; i < input.length; i++) {
2622
+ if (i > 0) {
2623
+ this.sS(
2624
+ output[i],
2625
+ this.gF(output[i])?.lookupoutput
2626
+ ? FieldState.ReadOnly
2627
+ : this.gS(output[i]),
2628
+ ''
2629
+ );
2630
+ const fld = this.entity?.members?.find(
2631
+ item => item.id === output[i]
2632
+ );
2633
+ if (fld?.lookup !== '') {
2634
+ this.fieldChanged({
2635
+ id: fld?.id ?? '',
2636
+ value: String(fld?.value ?? ''),
2637
+ function: 'lookupCheck',
2638
+ });
2639
+ }
2640
+ }
2641
+ }
2642
+ } else {
2643
+ if (this.gS(field.id) === FieldState.Error) {
2644
+ this.sS(field.id, FieldState.None);
2645
+ }
2646
+ for (let i = 0; i < input.length; i++) {
2647
+ if (i === 0) {
2648
+ for (let x = 0; x < extern.length; x++) {
2649
+ const ext: string[] = extern[x].split(';');
2650
+ if (ext[0] !== '') {
2651
+ const extField = ext[1].substring(1).toString();
2652
+ if (!this.gF(extField).readonly) {
2653
+ this.sS(field.id, FieldState.None);
2654
+ }
2655
+ const fldValue = this.gV(extField);
2656
+ if (x < extern.length - 1) {
2657
+ lkdata = lkdata.filter(item =>
2658
+ item.members?.find(
2659
+ e => e.name === ext[0] && e.value === fldValue
2660
+ )
2661
+ );
2662
+ continue;
2663
+ }
2664
+ elem = lkdata.find(item =>
2665
+ item.members?.find(
2666
+ e => e.name === ext[0] && e.value === fldValue
2667
+ )
2668
+ );
2669
+ if (
2670
+ !elem ||
2671
+ elem.members
2672
+ ?.filter(e => e.name === input[0])[0]
2673
+ ?.value?.toString() !== String(field.value ?? '')
2674
+ ) {
2675
+ this.sS(
2676
+ field.id,
2677
+ FieldState.Error,
2678
+ this.msg.get('app.itemerror')
2679
+ );
2680
+ return;
2681
+ }
2682
+ }
2683
+ }
2684
+ } else {
2685
+ if (elem) {
2686
+ const member = elem.members?.find(e => e.name === input[i]);
2687
+ if (member?.value) {
2688
+ if (
2689
+ !this.gV(output[i]) ||
2690
+ (this.gV(output[i]) &&
2691
+ this.gS(output[i]) !== this.fieldState.Forced)
2692
+ ) {
2693
+ this.sV(output[i], member.value);
2694
+ if (functionId !== 'noBusiness') {
2695
+ this.fieldChanged({
2696
+ id: output[i],
2697
+ value: String(member.value ?? ''),
2698
+ function: '',
2699
+ });
2700
+ }
2701
+ }
2702
+ }
2703
+ this.sS(
2704
+ output[i],
2705
+ this.gF(output[i])?.lookupoutput
2706
+ ? this.gS(output[i]) !== this.fieldState.Hidden
2707
+ ? this.fieldState.ReadOnly
2708
+ : this.fieldState.Hidden
2709
+ : this.gS(output[i])
2710
+ );
2711
+ }
2712
+ }
2713
+ }
2714
+ }
2715
+ } else {
2716
+ for (let i = 0; i < input.length; i++) {
2717
+ if (i > 0) {
2718
+ // Output fields that serve as lookup outputs should always be ReadOnly:
2719
+ // the user cannot type them directly — they auto-fill from the lookup popup.
2720
+ if (this.gF(output[i])?.lookupoutput) {
2721
+ this.sS(
2722
+ output[i],
2723
+ this.gS(output[i]) !== this.fieldState.Hidden
2724
+ ? this.fieldState.ReadOnly
2725
+ : this.fieldState.Hidden
2726
+ );
2727
+ }
2728
+ const fld = this.entity?.members?.find(item => item.id === output[i]);
2729
+ if (fld?.lookup !== '') {
2730
+ this.fieldChanged({
2731
+ id: fld?.id ?? '',
2732
+ value: String(fld?.value ?? ''),
2733
+ function: 'lookupCheck',
2734
+ });
2735
+ }
2736
+ }
2737
+ }
2738
+ }
2739
+ }
2740
+
2741
+ /** Schedules a select() call on the given field's DOM input element on the next tick. */
2742
+ private setFocus(id: string) {
2743
+ if (this.model && !this.model.cache) {
2744
+ if (id && id !== '') {
2745
+ const tm = setTimeout(() => {
2746
+ const el = this.gE(id);
2747
+ if (el) el.select();
2748
+ clearTimeout(tm);
2749
+ }, 100);
2750
+ }
2751
+ }
2752
+ }
2753
+
2754
+ /** Resolves cached entity data or fetches from the API; calls backData when ready. */
2755
+ private async getData(id: string) {
2756
+ await new Promise<void>(r =>
2757
+ requestAnimationFrame(() => requestAnimationFrame(() => r()))
2758
+ );
2759
+ if (!this.model.rootModel) {
2760
+ this.fieldsId =
2761
+ this.managment.appId +
2762
+ '.' +
2763
+ this.root.entities[0].name.toUpperCase() +
2764
+ '.' +
2765
+ this.managment.year;
2766
+ }
2767
+ const data = this.gsv.getData(this.fieldsId);
2768
+ if (data && !this.model.preview && this.model.masterDetail === 0) {
2769
+ this.insertMode = false;
2770
+ await this.backDataFromCache(data);
2771
+ this.loaded = true;
2772
+ this.model.loaded = true;
2773
+ } else {
2774
+ await this.performGetData();
2775
+ }
2776
+ }
2777
+
2778
+ /** Performs a GET /managment read request and populates the component via backData. */
2779
+ async performGetData(): Promise<void> {
2780
+ this.managment.fields = [];
2781
+ this.managment.functionId = 'read';
2782
+ this.msv.getData(this.managment).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
2783
+ next: async (data: TransportInterface) => {
2784
+ this.insertMode = false;
2785
+ await this.backData(data);
2786
+ this.loaded = true;
2787
+ this.model.loaded = true;
2788
+ },
2789
+ error: err => {
2790
+ console.error('getData HTTP error:', err);
2791
+ },
2792
+ });
2793
+ }
2794
+
2795
+ /** Broadcasts the current dialog state to the model shell (model.component). */
2796
+ private sendDialog() {
2797
+ this.gsv.setDialogField({
2798
+ showdialog: this.showdialog,
2799
+ titledialog: this.titledialog,
2800
+ textdialog: this.textdialog,
2801
+ showconfirm: this.showconfirm,
2802
+ dialogHeight: this.dialogHeight,
2803
+ dialogWidth: this.dialogWidth,
2804
+ dialogTop: this.dialogTop,
2805
+ dialogLeft: this.dialogLeft,
2806
+ });
2807
+ }
2808
+
2809
+ /** Broadcasts the current lookup panel state to the model shell (model.component). */
2810
+ private sendLookup() {
2811
+ this.gsv.setLookupField({
2812
+ showlookup: this.showlookup,
2813
+ titlelookup: this.titlelookup,
2814
+ textlookup: this.textlookup,
2815
+ lookupHeight: this.lookupHeight,
2816
+ lookupWidth: this.lookupWidth,
2817
+ lookupTop: this.lookupTop,
2818
+ lookupLeft: this.lookupLeft,
2819
+ lookup: this.lookup,
2820
+ lookupfilter: this.lookupfilter,
2821
+ });
2822
+ }
2823
+
2824
+ /** Broadcasts the current toolbar command states to ModelToolbarComponent. */
2825
+ private sendCommands() {
2826
+ if (this.entity?.viewlist) {
2827
+ // In list-view-only entities, disable all directional navigation.
2828
+ this.updateNavButtons(0);
2829
+ }
2830
+ this.gsv.setItemCommands([
2831
+ { id: 'exit', state: false },
2832
+ { id: 'insertmode', state: this.insertMode },
2833
+ { id: 'plus', state: this.bplus },
2834
+ { id: 'minus', state: this.bminus },
2835
+ { id: 'first', state: this.bfirst },
2836
+ { id: 'previous', state: this.bprevious },
2837
+ { id: 'next', state: this.bnext },
2838
+ { id: 'last', state: this.blast },
2839
+ { id: 'detail', state: this.bdetail },
2840
+ { id: 'intodetail', state: this.bintodetail },
2841
+ { id: 'download', state: this.bdownload },
2842
+ { id: 'upload', state: this.bupload },
2843
+ { id: 'print', state: this.bprint },
2844
+ { id: 'table', state: this.btable },
2845
+ { id: 'save', state: this.save },
2846
+ { id: 'refreshgrid', state: this.refreshgrid },
2847
+ ]);
2848
+ const actionStates: [string, boolean][] = [
2849
+ ['plus', this.bplus],
2850
+ ['minus', this.bminus],
2851
+ ['next', this.bnext],
2852
+ ['previous', this.bprevious],
2853
+ ['first', this.bfirst],
2854
+ ['last', this.blast],
2855
+ ['table', this.btable],
2856
+ ['detail', this.bdetail],
2857
+ ['download', this.bdownload],
2858
+ ['upload', this.bupload],
2859
+ ['save', this.save],
2860
+ ];
2861
+ for (const [name, state] of actionStates) {
2862
+ if (this.gE('action_' + name)) {
2863
+ this.gF('action_' + name).enabled = !state;
2864
+ }
2865
+ }
2866
+ }
2867
+
2868
+ /** Resets all toolbar button states to their default (all disabled) values. */
2869
+ private resetCommands() {
2870
+ this.bplus = true;
2871
+ this.bminus = true;
2872
+ this.updateNavButtons(0);
2873
+ this.bdetail = true;
2874
+ this.bintodetail = true;
2875
+ this.bdownload = true;
2876
+ this.bupload = true;
2877
+ this.bprint = this.viewList;
2878
+ this.btable = true;
2879
+ this.save = true;
2880
+ this.refreshgrid = false;
2881
+ this.sendCommands();
2882
+ }
2883
+
2884
+ /**
2885
+ * Parses the server response (JSON + SVG), instantiates the BASELogic layer,
2886
+ * and triggers commandData('readed') to populate the first/last record.
2887
+ */
2888
+ private async backDataFromCache(cached: EntitiesData) {
2889
+ this.data = cached;
2890
+ await this.initLogicAndReady();
2891
+ }
2892
+
2893
+ private async backData(data: TransportInterface) {
2894
+ if (data.dataFile) {
2895
+ this.data = JSON.parse(data.dataFile as string);
2896
+ await this.initLogicAndReady();
2897
+ }
2898
+ }
2899
+
2900
+ private async initLogicAndReady() {
2901
+ this.logic = await this.config.logicFactory(
2902
+ this.gsv, this.msv, this.msg, this.managment.entId, this.data
2903
+ );
2904
+ this.lastKey = 0;
2905
+ await this.logic.init();
2906
+ await this.commandData('readed');
2907
+ }
2908
+
2909
+ /** Left- or right-pads a string value with the given character up to the target size. */
2910
+ private padding(
2911
+ direction: PadType,
2912
+ value: string,
2913
+ char: string,
2914
+ size: number
2915
+ ): string {
2916
+ let s = value.toString();
2917
+ if (char.charCodeAt(0) !== 0 && s !== '' && s !== '0') {
2918
+ while (s.length < size) {
2919
+ if (direction === PadType.Right) {
2920
+ s = s + char;
2921
+ } else {
2922
+ s = char + s;
2923
+ }
2924
+ }
2925
+ }
2926
+ return s;
2927
+ }
2928
+
2929
+ /** Applies the field's match/replace regular expression to the DOM input and the Member value. */
2930
+ private regExpression(id: string): boolean {
2931
+ const element = this.gE(id);
2932
+ if (element !== null) {
2933
+ if (
2934
+ element.getAttribute(FieldAttr.get('match')!) !== null &&
2935
+ element.getAttribute(FieldAttr.get('replace')!) !== null
2936
+ ) {
2937
+ const rem = element.getAttribute(FieldAttr.get('match')!)!;
2938
+ const rep = element.getAttribute(FieldAttr.get('replace')!)!;
2939
+ if (rem !== '' && rep !== '') {
2940
+ const regExp = new RegExp(rem);
2941
+ this.gE(id).value = this.gV(id)
2942
+ .toString()
2943
+ .toUpperCase()
2944
+ .replace(regExp, rep);
2945
+ this.gF(id).value = this.gE(id).value;
2946
+ const fld = this.gE(id).value;
2947
+ const ret = this.gE(id).value.match(rem.toUpperCase());
2948
+ return ret || fld.length === 0 ? true : false;
2949
+ }
2950
+ }
2951
+ }
2952
+ return true;
2953
+ }
2954
+
2955
+ /** Pads or trims a field value based on padleftchar / padrightchar / autoformatter attributes. */
2956
+ private PadSpacing(id: string) {
2957
+ let value: string = this.gV(id);
2958
+ if (value?.toString().length > 0) {
2959
+ if (this.gE(id) && this.gF(id).padleftchar !== '') {
2960
+ this.gE(id).value = this.padding(
2961
+ PadType.Left,
2962
+ this.gV(id),
2963
+ this.gF(id).padleftchar,
2964
+ this.gF(id).length ?? this.gF(id).precision ?? 0
2965
+ );
2966
+ this.gF(id).value = this.gE(id)!.value;
2967
+ }
2968
+ if (this.gE(id) && this.gF(id).padrightchar !== '') {
2969
+ this.gE(id)!.value = this.padding(
2970
+ PadType.Right,
2971
+ this.gV(id),
2972
+ this.gF(id).padrightchar,
2973
+ this.gF(id).length ?? this.gF(id).precision ?? 0
2974
+ );
2975
+ this.gF(id).value = this.gE(id)!.value;
2976
+ }
2977
+ if (this.gF(id).autoformatter && this.gF(id).match === '') {
2978
+ const spacing = ' ';
2979
+ let ret = '';
2980
+ if (this.gT(id) === FieldType.Date) {
2981
+ this.gF(id).value = String(this.gF(id).value ?? '').replace(/\//g, ' ');
2982
+ }
2983
+ const strVal = String(this.gF(id).value ?? '');
2984
+ for (let i = 0; i < strVal.length; i++) {
2985
+ if (i > 0) {
2986
+ ret += spacing.repeat(this.gF(id).spacing ?? 1);
2987
+ }
2988
+ ret += strVal[i];
2989
+ }
2990
+ this.gE(id).value = ret;
2991
+ this.gF(id).value = this.gE(id).value;
2992
+ }
2993
+ }
2994
+ }
2995
+
2996
+ /** Initialises the component from the given ItemInterface (model config). */
2997
+ private initItem(model: ItemInterface) {
2998
+ this.model = model;
2999
+ if (model.svg !== null && model.svg !== '') {
3000
+ this.svg = this.gsv.sanitizeurl(
3001
+ 'assets/svg/' +
3002
+ this.gsv.getProject().year +
3003
+ '/' +
3004
+ this.gsv.getProject().project +
3005
+ '/' +
3006
+ model.svg +
3007
+ '.svg'
3008
+ );
3009
+ }
3010
+ this.managment.appId = this.gsv.getProject().project;
3011
+ this.managment.year = this.gsv.getYear();
3012
+ this.managment.entId = this.model.name;
3013
+ this.managment.keyId = this.gsv.getKeyId();
3014
+ this.managment.name = model.title;
3015
+ for (let i = 0; i < model.prgDim; i++) {
3016
+ if (this.managment.keys !== '') {
3017
+ this.managment.keys += ';';
3018
+ }
3019
+ if (i === 0 && this.gsv.getCurrentLinkItem().groupName !== '') {
3020
+ this.managment.keys += 'prg=' + this.gsv.getCurrentLinkItem().prg[0];
3021
+ } else {
3022
+ this.managment.keys += 'prg=1';
3023
+ }
3024
+ }
3025
+
3026
+ this.fieldsId =
3027
+ this.managment.appId +
3028
+ '.' +
3029
+ this.managment.entId +
3030
+ '.' +
3031
+ this.managment.year;
3032
+
3033
+ this.insertMode = true;
3034
+
3035
+ this.root = this.gsv.getSchemaJson(
3036
+ this.managment.appId + '.' + this.managment.year
3037
+ );
3038
+
3039
+ this.entity = this.root.entities.find(
3040
+ e => e.name == this.managment.entId.toLocaleLowerCase()
3041
+ )!;
3042
+ this._memberMap = null;
3043
+
3044
+ this.setViewList(this.entity.viewlist);
3045
+
3046
+ if (this.gsv.getCurrentKeys().length > 0) {
3047
+ this.managment.keys = this.gsv.getCurrentKeys();
3048
+ this.gsv.setCurrentKeys('');
3049
+ }
3050
+
3051
+ this.closing = false;
3052
+ }
3053
+
3054
+ /**
3055
+ * Handles the main toolbar command dispatch (exit, save, plus, minus, table, print,
3056
+ * go:<keys>, etc.) and updates entity state after each operation.
3057
+ */
3058
+ public async sendCommand(command: ItemCommandInterface) {
3059
+ if (command.id === 'exit') {
3060
+ // Check for a saved intodetail state BEFORE navigating, so we can
3061
+ // decide the correct destination and avoid a spurious intermediate navigation.
3062
+ let restoredManagment: ManagmentInterface | null = null;
3063
+ let managmentKey: string | null = null;
3064
+
3065
+ if (localStorage.length > 0) {
3066
+ const allKeys: string[] = [];
3067
+ for (let i = 0; i < localStorage.length; i++) {
3068
+ const key = localStorage.key(i);
3069
+ if (key && /^\d+\./.test(key)) {
3070
+ allKeys.push(key);
3071
+ }
3072
+ }
3073
+ // Sort descending by numeric prefix so [0] is the most-recently saved
3074
+ // intodetail entry (LIFO order: last pushed = first to pop on exit).
3075
+ allKeys.sort((a, b) => parseInt(b, 10) - parseInt(a, 10));
3076
+ managmentKey = allKeys[0] ?? null;
3077
+
3078
+ if (managmentKey && localStorage.getItem(managmentKey) !== null) {
3079
+ const storedValue = localStorage.getItem(managmentKey);
3080
+ restoredManagment = JSON.parse(storedValue!);
3081
+ }
3082
+ }
3083
+
3084
+ await this.close();
3085
+
3086
+ if (restoredManagment !== null && restoredManagment.appId) {
3087
+ // --- Back navigation: return to the component that launched intodetail ---
3088
+ localStorage.removeItem(managmentKey!);
3089
+
3090
+ if (restoredManagment?.keyId?.length === 36) {
3091
+ localStorage.setItem('search', restoredManagment?.keyId);
3092
+ }
3093
+ const originKey =
3094
+ restoredManagment?.appId +
3095
+ '.' +
3096
+ restoredManagment?.year +
3097
+ '.' +
3098
+ restoredManagment?.entId +
3099
+ '.' +
3100
+ restoredManagment.currentFieldId +
3101
+ '.' +
3102
+ restoredManagment.keys;
3103
+ localStorage.setItem('origin', originKey);
3104
+
3105
+ localStorage.setItem(
3106
+ 'targetModel',
3107
+ restoredManagment?.entId?.toUpperCase()
3108
+ );
3109
+
3110
+ // Await navigation so that NavigationComponent is fully mounted and
3111
+ // subscribed to projectChanged before we fire activateProject.
3112
+ // This mirrors the same pattern used in handleIntoDetail (forward direction).
3113
+ await this.gsv.navigate('/home');
3114
+
3115
+ const projectName = this.gsv
3116
+ .getProjects()
3117
+ [
3118
+ this.gsv.getYear()
3119
+ ]?.find(e => e.project === restoredManagment?.appId?.toUpperCase());
3120
+
3121
+ if (projectName) {
3122
+ this.gsv.activateProject(projectName);
3123
+ } else {
3124
+ console.error(
3125
+ 'Project not found for appId:',
3126
+ restoredManagment?.appId
3127
+ );
3128
+ }
3129
+ } else {
3130
+ // --- Normal exit: no saved intodetail state ---
3131
+ if (
3132
+ this.gsv.getProject().type === 'D' ||
3133
+ this.gsv.getProject().type === 'G'
3134
+ ) {
3135
+ this.gsv.setActiveLink('/home/modelsearch');
3136
+ this.gsv.navigate('/home/modelsearch');
3137
+ } else {
3138
+ this.gsv.setActiveLink('/home/dashboard');
3139
+ this.gsv.navigate('/home/dashboard');
3140
+ }
3141
+ }
3142
+ return;
3143
+ } else if (command.id === 'print') {
3144
+ if (!this.viewList) {
3145
+ this.logic.function(
3146
+ 'save',
3147
+ this.lastKey,
3148
+ this.getKeyArray(),
3149
+ this.currentFieldId
3150
+ );
3151
+ this.commandData('print');
3152
+ return;
3153
+ } else {
3154
+ this.sendCommand({ id: 'print', state: false });
3155
+ }
3156
+ } else if (command.id === 'table') {
3157
+ if (this.gS(this.currentFieldId) !== FieldState.Fatal) {
3158
+ const json = this.getJsonData();
3159
+ await this.throwSave(json).then(async e => {
3160
+ await this.commandData(command.id).then(e => {
3161
+ this.resetCommands();
3162
+ this.bplus = !this.model.allowAdd;
3163
+ this.bprint = false;
3164
+ this.refreshData(true);
3165
+ });
3166
+ });
3167
+ }
3168
+ } else if (command.id === 'save') {
3169
+ this.logic.function(
3170
+ command.id,
3171
+ this.lastKey,
3172
+ this.getKeyArray(),
3173
+ this.currentFieldId
3174
+ );
3175
+ const json = this.getJsonData();
3176
+ await this.throwSave(json).then(async e => {
3177
+ await this.commandData('readed');
3178
+ });
3179
+ } else if (command.id === 'minus') {
3180
+ this.checkConstraint().then(ret => {
3181
+ if (ret) {
3182
+ this.logic.function(
3183
+ command.id,
3184
+ this.lastKey,
3185
+ this.getKeyArray(),
3186
+ this.currentFieldId
3187
+ );
3188
+ this.commandData(command.id);
3189
+ } else {
3190
+ this.showMessage(this.msg.get('app.unabletodelete'));
3191
+ this.setFocus(this.currentFieldId);
3192
+ }
3193
+ });
3194
+ } else if (command.id === 'detail') {
3195
+ this.commandsResult({});
3196
+ } else if (command.id === 'intodetail') {
3197
+ // Defer heavy operations to avoid blocking main thread
3198
+ Promise.resolve()
3199
+ .then(() => this.handleIntoDetail())
3200
+ .catch(error => {
3201
+ console.error('Error in intodetail command:', error);
3202
+ this.showMessage("Errore durante l'accesso al dettaglio");
3203
+ });
3204
+ return;
3205
+ } else {
3206
+ if (command.id.startsWith('go:')) {
3207
+ this.managment.keys = '';
3208
+ const keys: string[] = command.id.substring(3).split(';');
3209
+ // tslint:disable-next-line:prefer-for-of
3210
+ for (let i = 0; i < keys.length; i++) {
3211
+ if (this.managment.keys !== '') {
3212
+ this.managment.keys += ';';
3213
+ }
3214
+ this.managment.keys += 'prg=' + keys[i];
3215
+ }
3216
+ command.id = 'table';
3217
+ this.setLastKey(this.getLastKey());
3218
+ }
3219
+ if (!this.insertMode) {
3220
+ if (command.id === 'table') {
3221
+ this.checkKey(this.model.focusOnInsert);
3222
+ }
3223
+ await this.commandData(command.id);
3224
+ }
3225
+ }
3226
+ if (command.id === 'plus') {
3227
+ this.gsv.setExtendedDescription('');
3228
+ }
3229
+ }
3230
+
3231
+ /** Toggles the viewList flag and updates related navigation button states. */
3232
+ private setViewList(state: boolean) {
3233
+ this.viewList = state;
3234
+ }
3235
+
3236
+ /**
3237
+ * Handles the "intodetail" toolbar action: persists the current managment context
3238
+ * to localStorage, navigates to the home route, and activates the target project.
3239
+ */
3240
+ private async handleIntoDetail() {
3241
+ const progressState =
3242
+ this.managment.year +
3243
+ '.' +
3244
+ this.managment.appId +
3245
+ '.' +
3246
+ this.managment.entId +
3247
+ '.' +
3248
+ this.model.title;
3249
+
3250
+ if (localStorage.getItem(progressState) !== null) {
3251
+ localStorage.removeItem(progressState);
3252
+ }
3253
+
3254
+ this.managment.currentFieldId = this.currentFieldId;
3255
+ const fieldLookup = this.gF(this.currentFieldId).lookup;
3256
+
3257
+ if (!fieldLookup || !fieldLookup.includes('#')) {
3258
+ console.error('Invalid fieldLookup format:', fieldLookup);
3259
+ this.showMessage('Errore: configurazione campo non valida');
3260
+ return;
3261
+ }
3262
+
3263
+ const parts = fieldLookup.split('#');
3264
+
3265
+ localStorage.setItem(
3266
+ localStorage.length + '.' + progressState,
3267
+ JSON.stringify(this.managment)
3268
+ );
3269
+
3270
+ const originKey =
3271
+ parts[0].toUpperCase() +
3272
+ '.' +
3273
+ parts[1].toUpperCase() +
3274
+ '.' +
3275
+ parts[2].toUpperCase();
3276
+
3277
+ localStorage.setItem('origin', originKey);
3278
+
3279
+ // Save target model name for navigation component to load it after project activation
3280
+ const modelName = parts[2].toUpperCase();
3281
+ localStorage.setItem('targetModel', modelName);
3282
+
3283
+ await this.gsv.navigate('/home');
3284
+
3285
+ const projectYear = this.gsv.getYear();
3286
+ const projects = this.gsv.getProjects()[projectYear];
3287
+ const projectName = parts[0].toUpperCase();
3288
+
3289
+ const project = projects?.find(e => e.project === projectName);
3290
+
3291
+ if (!project) {
3292
+ console.error('Project not found:', projectName);
3293
+ console.error('Available projects:', projects);
3294
+ this.showMessage('Errore: progetto non trovato');
3295
+ return;
3296
+ }
3297
+
3298
+ this.gsv.activateProject(project);
3299
+ this.resetCommands();
3300
+ }
3301
+
3302
+ /** Iterates all project items and recomputes each entity's aggregated Error/Warning/Valid state. */
3303
+ private updateState() {
3304
+ if (this.data) {
3305
+ const projectId =
3306
+ this.gsv.getProject().project + '.' + this.gsv.getYear();
3307
+ this.gsv.getProjectItems(projectId).forEach(item => {
3308
+ // Only update state for entities owned by this component's data.
3309
+ // Other components manage their own entities; overwriting their states
3310
+ // here would reset Error/Warning badges set by a sibling component.
3311
+ const entityName = item.name.toLocaleLowerCase();
3312
+ if (this.data.entities.find(e => e.name === entityName)) {
3313
+ item.state = this.updateEntityState(entityName);
3314
+ }
3315
+ });
3316
+ this.gsv.emitItemState();
3317
+ }
3318
+ }
3319
+
3320
+ /** Returns the aggregated ItemState (Error / Warning / Valid / Empty) for the given entity name. */
3321
+ updateEntityState(name: string): ItemState {
3322
+ const p1 = this.data.entities.find(m => m.name === name);
3323
+ if (p1) {
3324
+ let state = p1.modules.filter(e =>
3325
+ e.members?.find(m => m.state === ItemState.Error)
3326
+ );
3327
+ if (state.length > 0) {
3328
+ return ItemState.Error;
3329
+ } else {
3330
+ state = p1.modules.filter(e =>
3331
+ e.members?.find(m => m.state === ItemState.Warning)
3332
+ );
3333
+ if (state.length > 0) {
3334
+ return ItemState.Warning;
3335
+ } else {
3336
+ return ItemState.Valid;
3337
+ }
3338
+ }
3339
+ }
3340
+ return ItemState.Empty;
3341
+ }
3342
+
3343
+ /** Handles a button-type field click: delegates to sendCommand or fires a logic click_ function. */
3344
+ public buttonClick(field: string, action: string) {
3345
+ this.setFocus(this.currentFieldId);
3346
+ if (action !== LabelAction.Field.toLocaleString()) {
3347
+ if (this.gE('action_' + action) && this.gF('action_' + action).enabled) {
3348
+ this.sendCommand({ id: action.toLocaleLowerCase(), state: true });
3349
+ }
3350
+ } else {
3351
+ this.logic.function(
3352
+ 'click_' + field,
3353
+ this.lastKey,
3354
+ this.getKeyArray(),
3355
+ this.currentFieldId
3356
+ );
3357
+ }
3358
+ }
3359
+
3360
+ /** TrackBy function for the entity member field loop. Returns the field name or index. */
3361
+ public trackByField(index: number, field: Member): string {
3362
+ return field.name || index.toString();
3363
+ }
3364
+
3365
+ /** Returns the ngClass map for standard input/select fields (Combo, Date, Number, Text, TextualCheck). */
3366
+ tooltipClass(field: Member): string {
3367
+ switch (field?.state) {
3368
+ case this.fieldState.Error:
3369
+ return 'tooltip-error';
3370
+ case this.fieldState.Fatal:
3371
+ return 'tooltip-fatal';
3372
+ case this.fieldState.Warning:
3373
+ return 'tooltip-warning';
3374
+ default:
3375
+ return '';
3376
+ }
3377
+ }
3378
+
3379
+ fieldClasses(field: Member): Record<string, boolean> {
3380
+ const hasSvg = this.entity.svg.length > 0;
3381
+ return {
3382
+ field: !hasSvg,
3383
+ 'field-mod': hasSvg,
3384
+ 'text-right': field.alignment === 'right',
3385
+ 'text-left': field.alignment === 'left',
3386
+ 'text-center': field.alignment === 'center',
3387
+ 'has-error': field?.state === this.fieldState.Error,
3388
+ 'has-fatal': field?.state === this.fieldState.Fatal,
3389
+ 'has-warning': field?.state === this.fieldState.Warning,
3390
+ 'has-readonly':
3391
+ ((field?.state === this.fieldState.ReadOnly || !field.enabled) &&
3392
+ field?.state !== this.fieldState.Error &&
3393
+ field?.state !== this.fieldState.Warning) ||
3394
+ this._readonlySet.has(field.id),
3395
+ 'has-detail': field?.state === this.fieldState.Detail,
3396
+ 'has-forced': field?.state === this.fieldState.Forced,
3397
+ 'has-hidden': field?.state === this.fieldState.Hidden,
3398
+ };
3399
+ }
3400
+
3401
+ /** Returns the ngClass map for Label fields. */
3402
+ labelClasses(field: Member): Record<string, boolean> {
3403
+ return {
3404
+ label: this.entity.svg.length > 0,
3405
+ 'text-right': field.alignment === 'right',
3406
+ 'text-left': field.alignment === 'left',
3407
+ 'text-center': field.alignment === 'center',
3408
+ 'has-error': field?.state === this.fieldState.Error,
3409
+ 'has-fatal': field?.state === this.fieldState.Fatal,
3410
+ 'has-warning': field?.state === this.fieldState.Warning,
3411
+ 'has-readonly':
3412
+ ((field?.state === this.fieldState.ReadOnly || !field.enabled) &&
3413
+ field?.state !== this.fieldState.Error &&
3414
+ field?.state !== this.fieldState.Warning) ||
3415
+ this._readonlySet.has(field.id),
3416
+ 'has-detail': field?.state === this.fieldState.Detail,
3417
+ 'has-forced': field?.state === this.fieldState.Forced,
3418
+ 'has-hidden': field?.state === this.fieldState.Hidden,
3419
+ };
3420
+ }
3421
+
3422
+ /** Returns the ngClass map for Button fields. */
3423
+ buttonClasses(field: Member): Record<string, boolean> {
3424
+ return {
3425
+ label: this.entity.svg.length > 0,
3426
+ btn: true,
3427
+ 'bg-primary': field.id === 'action_exit',
3428
+ 'bg-secondary': field.id !== 'action_exit',
3429
+ 'bg-success': field.id === 'action_save',
3430
+ 'text-white': true,
3431
+ 'text-right': field.alignment === 'right',
3432
+ 'text-left': field.alignment === 'left',
3433
+ 'text-center': field.alignment === 'center',
3434
+ 'has-error': field?.state === this.fieldState.Error,
3435
+ 'has-fatal': field?.state === this.fieldState.Fatal,
3436
+ 'has-warning': field?.state === this.fieldState.Warning,
3437
+ 'has-readonly':
3438
+ ((field?.state === this.fieldState.ReadOnly || !field.enabled) &&
3439
+ field?.state !== this.fieldState.Error &&
3440
+ field?.state !== this.fieldState.Warning) ||
3441
+ this._readonlySet.has(field.id),
3442
+ 'has-detail': field?.state === this.fieldState.Detail,
3443
+ 'has-forced': field?.state === this.fieldState.Forced,
3444
+ 'has-hidden': field?.state === this.fieldState.Hidden,
3445
+ };
3446
+ }
3447
+
3448
+ /** TrackBy function for combo option lists. Returns the item string or its index. */
3449
+ public trackByCombo(index: number, item: string): string {
3450
+ return item || index.toString();
3451
+ }
3452
+
3453
+ /** Parses a CSS pixel width string and returns the numeric value. */
3454
+ public parseWidth(width: string): number {
3455
+ return parseInt(width.replace('px', ''), 10);
3456
+ }
3457
+ }