View | Details | Raw Unified | Return to bug 40383
Collapse All | Expand All

(-)a/koha-tmpl/intranet-tmpl/prog/css/src/edifact_interchange.scss (+635 lines)
Line 0 Link Here
1
/* EDIFACT Interchange Display Styles */
2
3
// Variables
4
$monospace-font: 'Courier New', monospace;
5
$base-font-size: 0.9em;
6
$line-height: 1.4;
7
$border-radius: 3px;
8
$border-radius-small: 2px;
9
$border-radius-large: 4px;
10
$transition-duration: 0.2s;
11
$transition-easing: ease;
12
13
// Mixins
14
@mixin segment-styling($color-var, $bg-color-var, $border-color-var) {
15
  color: var(#{$color-var}) !important;
16
  background-color: var(#{$bg-color-var}) !important;
17
  border-left: 4px solid var(#{$border-color-var}) !important;
18
  padding-left: 8px;
19
  
20
  &:hover {
21
    background-color: var(#{$bg-color-var}) !important;
22
    opacity: 0.8;
23
  }
24
}
25
26
// Enhanced mixin for better contrast using proper Bootstrap color combinations
27
@mixin header-styling($theme-color) {
28
  @if $theme-color == 'info' {
29
    color: var(--bs-info-text-emphasis) !important;
30
    background-color: var(--bs-info-bg-subtle) !important;
31
    border-left: 4px solid var(--bs-info) !important;
32
    border-top: 1px solid var(--bs-info) !important;
33
    border-right: 1px solid var(--bs-info) !important;
34
    border-bottom: 1px solid var(--bs-info) !important;
35
  } @else if $theme-color == 'danger' {
36
    color: var(--bs-danger-text-emphasis) !important;
37
    background-color: var(--bs-danger-bg-subtle) !important;
38
    border-left: 4px solid var(--bs-danger) !important;
39
    border-top: 1px solid var(--bs-danger) !important;
40
    border-right: 1px solid var(--bs-danger) !important;
41
    border-bottom: 1px solid var(--bs-danger) !important;
42
  } @else if $theme-color == 'success' {
43
    color: var(--bs-success-text-emphasis) !important;
44
    background-color: var(--bs-success-bg-subtle) !important;
45
    border-left: 4px solid var(--bs-success) !important;
46
    border-top: 1px solid var(--bs-success) !important;
47
    border-right: 1px solid var(--bs-success) !important;
48
    border-bottom: 1px solid var(--bs-success) !important;
49
  } @else if $theme-color == 'warning' {
50
    color: var(--bs-warning-text-emphasis) !important;
51
    background-color: var(--bs-warning-bg-subtle) !important;
52
    border-left: 4px solid var(--bs-warning) !important;
53
    border-top: 1px solid var(--bs-warning) !important;
54
    border-right: 1px solid var(--bs-warning) !important;
55
    border-bottom: 1px solid var(--bs-warning) !important;
56
  }
57
  
58
  padding-left: 8px;
59
  
60
  &:hover {
61
    opacity: 0.8;
62
  }
63
}
64
65
@mixin responsive-size($font-size, $margin-left, $padding-left, $segment-padding) {
66
  .edi-tree {
67
    font-size: $font-size;
68
    
69
    ul {
70
      margin-left: $margin-left;
71
      padding-left: $padding-left;
72
    }
73
  }
74
  
75
  .segment {
76
    padding: $segment-padding;
77
  }
78
}
79
80
// Main styles
81
.edi-tree {
82
  font-family: $monospace-font;
83
  font-size: $base-font-size;
84
  line-height: $line-height;
85
  
86
  &.hidden {
87
    display: none !important;
88
  }
89
  
90
  ul {
91
    margin-left: 1.2rem;
92
    border-left: 2px solid #e0e0e0;
93
    padding-left: 0.8rem;
94
    margin-top: 0.25rem;
95
    margin-bottom: 0.25rem;
96
    transition: max-height 0.3s ease-in-out;
97
    overflow: hidden;
98
    
99
    &:hover {
100
      border-left-color: #007bff;
101
    }
102
  }
103
  
104
  // Segment base styles
105
  .segment {
106
    font-family: $monospace-font;
107
    padding: 3px 6px;
108
    margin: 1px 0;
109
    border-radius: $border-radius;
110
    transition: background-color $transition-duration $transition-easing;
111
    position: relative;
112
    
113
    &:hover {
114
      background-color: #f8f9fa;
115
    }
116
    
117
    // Segment type variations
118
    &.header {
119
      @include header-styling('primary');
120
      cursor: pointer;
121
      
122
      // Bootstrap collapse integration
123
      &[data-bs-toggle="collapse"] {
124
        .fa-chevron-down {
125
          transition: transform $transition-duration $transition-easing;
126
        }
127
        
128
        &[aria-expanded="false"] .fa-chevron-down {
129
          transform: rotate(-90deg);
130
        }
131
      }
132
      
133
      // Accessibility
134
      &[aria-expanded="true"] .chevron::after {
135
        content: " (expanded)";
136
        position: absolute;
137
        left: -9999px;
138
      }
139
      
140
      &[aria-expanded="false"] .chevron::after {
141
        content: " (collapsed)";
142
        position: absolute;
143
        left: -9999px;
144
      }
145
      
146
      &:focus {
147
        outline: 2px solid #007bff;
148
        outline-offset: 2px;
149
      }
150
    }
151
    
152
  }
153
  
154
  // Hierarchical color scheme for different nesting levels
155
  // Level 1: UNB (Interchange) - Blue
156
  > li > .segment {
157
      &.header {
158
        @include header-styling('info');
159
      }
160
161
      &.content {
162
        @include segment-styling(--bs-info-text-emphasis, --bs-info-bg-subtle, --bs-info-border-subtle);
163
      }
164
  }
165
  
166
  > li .segment-group.related-segments .segment.content {
167
    @include segment-styling(--bs-info-text-emphasis, --bs-info-bg-subtle, --bs-info-border-subtle);
168
  }
169
  
170
  // Level 2: UNH (Message) - Red  
171
  > li > ul > li > .segment {
172
      &.header {
173
        @include header-styling('danger');
174
      }
175
      &.content {
176
        @include segment-styling(--bs-danger-text-emphasis, --bs-danger-bg-subtle, --bs-danger-border-subtle);
177
      }
178
  }
179
  
180
  > li > ul > li .segment-group.related-segments .segment.content {
181
    @include segment-styling(--bs-danger-text-emphasis, --bs-danger-bg-subtle, --bs-danger-border-subtle);
182
  }
183
  
184
  // Level 3: LIN (Line groups) - Green
185
  > li > ul > li > ul > li > .segment {
186
      &.header {
187
        @include header-styling('success');
188
      }
189
190
      &.content {
191
        @include segment-styling(--bs-success-text-emphasis, --bs-success-bg-subtle, --bs-success-border-subtle);
192
      }
193
  }
194
  
195
  > li > ul > li > ul > li .segment-group.related-segments .segment.content {
196
    @include segment-styling(--bs-success-text-emphasis, --bs-success-bg-subtle, --bs-success-border-subtle);
197
  }
198
  
199
  // Level 4: Individual segments - Yellow
200
  > li > ul > li > ul > li > ul > li > .segment {
201
      &.header {
202
        @include header-styling('warning');
203
      }
204
205
      &.content {
206
        @include segment-styling(--bs-warning-text-emphasis, --bs-warning-bg-subtle, --bs-warning-border-subtle);
207
      }
208
  }
209
  
210
  > li > ul > li > ul > li > ul > li .segment-group.related-segments .segment.content {
211
    @include segment-styling(--bs-warning-text-emphasis, --bs-warning-bg-subtle, --bs-warning-border-subtle);
212
  }
213
  
214
  // Global trailer styling (for all levels)
215
  .segment.trailer {
216
    @include segment-styling(--bs-secondary-text-emphasis, --bs-secondary-bg-subtle, --bs-secondary-border-subtle);
217
    font-style: italic;
218
  }
219
  
220
  // Bold segment tags
221
  .segment-tag {
222
    font-weight: bold;
223
  }
224
225
  // Related segment grouping
226
  .segment-group {
227
    position: relative;
228
    margin: 2px 0;
229
    
230
    &.related-segments {
231
      background-color: rgba(13, 110, 253, 0.04); // Very subtle blue tint
232
      border-radius: $border-radius-small;
233
      padding: 2px 0;
234
      margin: 3px 0;
235
      
236
      // Connecting line for related segments
237
      &::before {
238
        content: "";
239
        position: absolute;
240
        left: -8px;
241
        top: 0;
242
        bottom: 0;
243
        width: 2px;
244
        background-color: rgba(13, 110, 253, 0.15);
245
        border-radius: 1px;
246
      }
247
      
248
      .segment {
249
        margin: 1px 0;
250
        
251
        &:first-child {
252
          margin-top: 2px;
253
        }
254
        
255
        &:last-child {
256
          margin-bottom: 2px;
257
        }
258
      }
259
    }
260
  }
261
  
262
  // Line group styling
263
  li > .segment.header {
264
    + ul {
265
      border-left-color: #17a2b8;
266
    }
267
    
268
    &:hover + ul {
269
      border-left-color: #138496;
270
    }
271
  }
272
  
273
  // Collapsed state
274
  [data-toggle].collapsed + ul {
275
    max-height: 0;
276
    margin-top: 0;
277
    margin-bottom: 0;
278
  }
279
  
280
  // Theme variants
281
  &.compact {
282
    font-size: 0.8em;
283
    
284
    ul {
285
      margin-left: 0.8rem;
286
      padding-left: 0.4rem;
287
    }
288
    
289
    .segment {
290
      padding: 1px 4px;
291
      margin: 0;
292
    }
293
  }
294
}
295
296
// Segment details
297
.segment-details {
298
  margin-top: 0.25rem;
299
  padding: 0.25rem;
300
  background-color: #f1f3f4;
301
  border-radius: $border-radius-small;
302
  font-size: 0.8em;
303
  color: #5f6368;
304
  border-left: 2px solid #dadce0;
305
  
306
  small {
307
    font-family: $monospace-font;
308
  }
309
}
310
311
// Loading states
312
.edi-loading {
313
  text-align: center;
314
  padding: 2rem;
315
  color: #6c757d;
316
  
317
  img {
318
    margin-right: 0.5rem;
319
  }
320
}
321
322
// Raw view styling
323
.edi-raw-view {
324
  font-family: $monospace-font;
325
  font-size: $base-font-size;
326
  line-height: $line-height;
327
  white-space: pre-wrap;
328
  
329
  &.hidden {
330
    display: none !important;
331
  }
332
  
333
  .segment-line {
334
    margin: 2px 0;
335
    padding: 1px 0;
336
    
337
    .segment-tag {
338
      font-weight: bold;
339
    }
340
  }
341
  
342
  // Raw view focus highlighting for focused message segments
343
  .edi-focused-segment-line {
344
    background-color: rgba(13, 110, 253, 0.08);
345
    border-left: 3px solid rgba(13, 110, 253, 0.6);
346
    padding-left: 8px;
347
    border-radius: $border-radius-small;
348
    margin: 1px 0;
349
    transition: background-color 0.3s ease;
350
    
351
    &:hover {
352
      background-color: rgba(13, 110, 253, 0.12);
353
    }
354
  }
355
}
356
357
// Modal body scrolling for EDIFACT content
358
.modal-body.edi-content {
359
  max-height: 60vh;
360
  overflow-y: auto;
361
}
362
363
// Main navbar styling (between modal-header and modal-body)
364
.edi-main-navbar {
365
  border-radius: 0;
366
  margin-bottom: 0;
367
  border-left: 0;
368
  border-right: 0;
369
  border-top: 0;
370
  
371
  // Prevent form submission on enter
372
  form {
373
    button[type="submit"] {
374
      display: none;
375
    }
376
  }
377
  
378
  // View toggle buttons
379
  .btn-group .btn {
380
    &.active {
381
      background-color: var(--bs-primary);
382
      border-color: var(--bs-primary);
383
      color: white;
384
    }
385
  }
386
}
387
388
// Tree toolbar styling (now inside modal-body)
389
.edi-tree-toolbar {
390
  // Hide by default (raw view)
391
  &.d-none {
392
    display: none !important;
393
  }
394
}
395
396
// Legacy toolbar styling (for backward compatibility)
397
.edi-toolbar {
398
  display: flex;
399
  justify-content: space-between;
400
  align-items: center;
401
  padding: 10px 0;
402
  border-bottom: 1px solid #dee2e6;
403
  margin-bottom: 15px;
404
  
405
  &-left {
406
    display: flex;
407
    gap: 10px;
408
  }
409
  
410
  &-center {
411
    flex: 1;
412
    max-width: 400px;
413
    margin: 0 20px;
414
  }
415
  
416
  &-right {
417
    display: flex;
418
    gap: 5px;
419
  }
420
  
421
  .btn {
422
    border-radius: $border-radius-large;
423
    font-size: 0.85em;
424
    padding: 4px 8px;
425
    
426
    &.active {
427
      background-color: #007bff;
428
      color: white;
429
    }
430
  }
431
}
432
433
// Search functionality styling
434
.edi-search-container {
435
  display: flex;
436
  align-items: center;
437
  gap: 8px;
438
  
439
  .input-group {
440
    flex: 1;
441
    min-width: 200px;
442
    margin-bottom: 0;
443
  }
444
  
445
  .edi-search-input {
446
    border-radius: $border-radius-small;
447
    font-size: 0.85em;
448
    
449
    &:focus {
450
      border-color: #007bff;
451
      box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
452
    }
453
  }
454
}
455
456
.edi-search-results {
457
  display: flex;
458
  align-items: center;
459
  gap: 6px;
460
  white-space: nowrap;
461
  
462
  .edi-search-count {
463
    font-size: 0.8em;
464
    color: #6c757d;
465
    min-width: 70px;
466
    text-align: right;
467
  }
468
  
469
  .btn-group {
470
    .btn {
471
      padding: 3px 6px;
472
      font-size: 0.75em;
473
      
474
      &:disabled {
475
        opacity: 0.5;
476
        cursor: not-allowed;
477
      }
478
    }
479
  }
480
}
481
482
// Search highlighting styles
483
.edi-search-highlight {
484
  background-color: #fff3cd;
485
  border: 1px solid #ffeaa7;
486
  border-radius: $border-radius-small;
487
  padding: 1px 2px;
488
  font-weight: bold;
489
}
490
491
.edi-search-current {
492
  position: relative;
493
  background-color: rgba(255, 193, 7, 0.2) !important;
494
  
495
  &::before {
496
    content: "";
497
    position: absolute;
498
    top: -3px;
499
    left: -3px;
500
    right: -3px;
501
    bottom: -3px;
502
    background-color: rgba(255, 193, 7, 0.1);
503
    border: 3px solid #ffc107;
504
    border-radius: $border-radius;
505
    z-index: -1;
506
    animation: pulse-search 2s ease-in-out infinite;
507
  }
508
  
509
  // Make the search highlights within the current result more prominent
510
  .edi-search-highlight {
511
    background-color: #ffc107 !important;
512
    border-color: #ff9800 !important;
513
    color: #000 !important;
514
    font-weight: bold !important;
515
    box-shadow: 0 0 4px rgba(255, 193, 7, 0.5);
516
  }
517
}
518
519
@keyframes pulse-search {
520
  0% {
521
    box-shadow: 0 0 0 0 rgba(255, 193, 7, 0.4);
522
  }
523
  70% {
524
    box-shadow: 0 0 0 8px rgba(255, 193, 7, 0);
525
  }
526
  100% {
527
    box-shadow: 0 0 0 0 rgba(255, 193, 7, 0);
528
  }
529
}
530
531
// Focus message highlighting
532
.edi-focused-message {
533
  position: relative;
534
  
535
  &::before {
536
    content: "";
537
    position: absolute;
538
    top: -6px;
539
    left: -6px;
540
    right: -6px;
541
    bottom: -6px;
542
    background-color: rgba(13, 110, 253, 0.15);
543
    border: 3px solid #0d6efd;
544
    border-radius: $border-radius;
545
    z-index: 1;
546
    // Initial animation for first 6 seconds, then static highlight
547
    animation: pulse-focus 2s ease-in-out 3;
548
  }
549
  
550
  // Ensure the message content is above the highlight
551
  > * {
552
    position: relative;
553
    z-index: 2;
554
  }
555
  
556
  // After animation, maintain a subtle but persistent highlight
557
  &::after {
558
    content: "";
559
    position: absolute;
560
    top: -6px;
561
    left: -6px;
562
    right: -6px;
563
    bottom: -6px;
564
    background-color: rgba(13, 110, 253, 0.08);
565
    border: 2px solid rgba(13, 110, 253, 0.6);
566
    border-radius: $border-radius;
567
    z-index: 1;
568
    // This will be visible after the animation completes
569
    animation: fade-in-persistent 6s ease-in-out forwards;
570
  }
571
}
572
573
@keyframes pulse-focus {
574
  0% {
575
    box-shadow: 0 0 0 0 rgba(13, 110, 253, 0.4);
576
  }
577
  70% {
578
    box-shadow: 0 0 0 10px rgba(13, 110, 253, 0);
579
  }
580
  100% {
581
    box-shadow: 0 0 0 0 rgba(13, 110, 253, 0);
582
  }
583
}
584
585
@keyframes fade-in-persistent {
586
  0% {
587
    opacity: 0;
588
  }
589
  100% {
590
    opacity: 1;
591
  }
592
}
593
594
// View toggle styling
595
.edi-view-toggle .btn {
596
  border-radius: $border-radius-large;
597
  font-size: 0.85em;
598
  padding: 4px 8px;
599
  
600
  &.active {
601
    background-color: #007bff;
602
    color: white;
603
  }
604
}
605
606
// Responsive design
607
@media (max-width: 768px) {
608
  @include responsive-size(0.8em, 0.8rem, 0.5rem, 2px 4px);
609
}
610
611
// Print styles
612
@media print {
613
  .edi-tree {
614
    font-size: 0.7em;
615
    
616
    .segment {
617
      &.header {
618
        background-color: transparent !important;
619
        border: 1px solid #000;
620
      }
621
      
622
      &.content {
623
        background-color: transparent !important;
624
      }
625
    }
626
  }
627
  
628
  .chevron {
629
    display: none;
630
  }
631
  
632
  [data-toggle].collapsed + ul {
633
    display: block !important;
634
  }
635
}
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/edifact-modal.inc (-38 lines)
Lines 1-38 Link Here
1
[%# EDIFACT interchange modal include %]
2
[%# This modal can be used on any page that needs to display EDIFACT interchanges %]
3
4
<!-- Modal to display EDIFACT interchanges -->
5
<div class="modal" id="EDI_modal" tabindex="-1" role="dialog" aria-labelledby="EDI_modal_label" aria-hidden="true">
6
    <div class="modal-dialog modal-lg">
7
        <div class="modal-content">
8
            <div class="modal-header">
9
                <h1 class="modal-title" id="EDI_modal_label">EDIFACT Interchange</h1>
10
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
11
            </div>
12
            <div class="modal-body">
13
                <div id="edi_loading"><img src="[% interface | html %]/[% theme | html %]/img/spinner-small.gif" alt="" /> Loading</div>
14
            </div>
15
            <div class="modal-footer">
16
                <button class="btn btn-default" data-bs-dismiss="modal">Close</button>
17
            </div>
18
        </div>
19
    </div>
20
</div>
21
22
<!-- Modal to display EDIFACT errors -->
23
<div class="modal" id="EDI_errors_modal" tabindex="-1" role="dialog" aria-labelledby="EDI_errors_modal_label" aria-hidden="true">
24
    <div class="modal-dialog modal-lg">
25
        <div class="modal-content">
26
            <div class="modal-header">
27
                <h1 class="modal-title" id="EDI_errors_modal_label">EDIFACT processing errors for <span id="EDI_errors_filename"></span></h1>
28
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
29
            </div>
30
            <div class="modal-body">
31
                <div id="edi_errors_loading"><img src="[% interface | html %]/[% theme | html %]/img/spinner-small.gif" alt="" /> Loading</div>
32
            </div>
33
            <div class="modal-footer">
34
                <button class="btn btn-default" data-bs-dismiss="modal">Close</button>
35
            </div>
36
        </div>
37
    </div>
38
</div>
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/edifact_errors.inc (+30 lines)
Line 0 Link Here
1
[%# EDIFACT errors modal %]
2
[%# This modal provides EDIFACT interchange parsing error display %]
3
[%# Usage: INCLUDE 'modals/edifact_errors.inc' modal_id='custom_errors_modal' modal_size='lg' %]
4
[%# Requires: Asset.js("js/modals/edifact_errors.js") %]
5
6
[%# Set defaults if not provided %]
7
[% UNLESS modal_id %][% modal_id = 'EDI_errors_modal' %][% END %]
8
[% UNLESS modal_size %][% modal_size = 'lg' %][% END %]
9
[% UNLESS modal_title %][% modal_title = 'EDIFACT processing errors for' %][% END %]
10
11
<!-- Modal to display EDIFACT errors -->
12
<div class="modal fade" id="[% modal_id | html %]" tabindex="-1" role="dialog" aria-labelledby="[% modal_id | html %]_label" aria-hidden="true">
13
    <div class="modal-dialog modal-[% modal_size | html %]">
14
        <div class="modal-content">
15
            <div class="modal-header">
16
                <h1 class="modal-title" id="[% modal_id | html %]_label"> [% modal_title | html %] <span class="filename"></span> </h1>
17
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
18
            </div>
19
            <div class="modal-body">
20
                <div class="edi-loading">
21
                    <img src="[% interface | html %]/[% theme | html %]/img/spinner-small.gif" alt="" />
22
                    [% t("Loading errors...") | html %]
23
                </div>
24
            </div>
25
            <div class="modal-footer">
26
                <button type="button" class="btn btn-primary" data-bs-dismiss="modal">[% t("Close") | html %]</button>
27
            </div>
28
        </div>
29
    </div>
30
</div>
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/edifact_interchange.inc (+33 lines)
Line 0 Link Here
1
[%# EDIFACT interchange modal %]
2
[%# This modal provides EDIFACT interchange display functionality %]
3
[%# Usage: INCLUDE 'modals/edifact_interchange.inc' modal_id='custom_modal' modal_size='xl' %]
4
[%# Requires: Asset.js("js/modals/edifact_interchange.js") %]
5
6
[%# Set defaults if not provided %]
7
[% UNLESS modal_id %][% modal_id = 'EDI_modal' %][% END %]
8
[% UNLESS modal_size %][% modal_size = 'lg' %][% END %]
9
[% UNLESS modal_title %][% modal_title = 'EDIFACT interchange' %][% END %]
10
[% UNLESS show_actions.defined %][% show_actions = 1 %][% END %]
11
12
<!-- Modal to display EDIFACT interchanges -->
13
<div class="modal fade" id="[% modal_id | html %]" tabindex="-1" role="dialog" aria-labelledby="[% modal_id | html %]_label" aria-hidden="true">
14
    <div class="modal-dialog modal-[% modal_size | html %]">
15
        <div class="modal-content">
16
            <div class="modal-header">
17
                <h1 class="modal-title" id="[% modal_id | html %]_label">[% modal_title | html %]</h1>
18
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
19
            </div>
20
            <div class="modal-body">
21
                <div class="edi-loading">
22
                    <img src="[% interface | html %]/[% theme | html %]/img/spinner-small.gif" alt="" />
23
                    [% t("Loading EDIFACT interchange...") | html %]
24
                </div>
25
            </div>
26
            [% IF show_actions %]
27
                <div class="modal-footer">
28
                    <button type="button" class="btn btn-primary" data-bs-dismiss="modal">[% t("Close") | html %]</button>
29
                </div>
30
            [% END %]
31
        </div>
32
    </div>
33
</div>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/basket.tt (-10 / +26 lines)
Lines 141-154 Link Here
141
                                        <ul class="dropdown-menu">
141
                                        <ul class="dropdown-menu">
142
                                            [% FOR message IN quote_messages %]
142
                                            [% FOR message IN quote_messages %]
143
                                                <li>
143
                                                <li>
144
                                                    <a class="dropdown-item view_edifact_message" href="#" data-message-id="[% message.id | html %]">
144
                                                    <a class="dropdown-item view_edifact_message" href="#" data-message-id="[% message.id | html %]" data-basketno="[% basketno | html %]" data-basketname="[% basketname | html %]">
145
                                                        <i class="fa fa-quote-left"></i> View quote ([% message.transfer_date | $KohaDates %])
145
                                                        <i class="fa fa-quote-left"></i> View quote ([% message.transfer_date | $KohaDates %])
146
                                                    </a>
146
                                                    </a>
147
                                                </li>
147
                                                </li>
148
                                            [% END %]
148
                                            [% END %]
149
                                            [% FOR message IN order_messages %]
149
                                            [% FOR message IN order_messages %]
150
                                                <li>
150
                                                <li>
151
                                                    <a class="dropdown-item view_edifact_message" href="#" data-message-id="[% message.id | html %]">
151
                                                    <a class="dropdown-item view_edifact_message" href="#" data-message-id="[% message.id | html %]" data-basketno="[% basketno | html %]" data-basketname="[% basketname | html %]">
152
                                                        <i class="fa fa-shopping-cart"></i> View order ([% message.transfer_date | $KohaDates %])
152
                                                        <i class="fa fa-shopping-cart"></i> View order ([% message.transfer_date | $KohaDates %])
153
                                                    </a>
153
                                                    </a>
154
                                                </li>
154
                                                </li>
Lines 158-169 Link Here
158
                                [% ELSIF quote_messages.size > 0 %]
158
                                [% ELSIF quote_messages.size > 0 %]
159
                                    <!-- Only quote messages exist -->
159
                                    <!-- Only quote messages exist -->
160
                                    <div class="btn-group">
160
                                    <div class="btn-group">
161
                                        <button type="button" class="btn btn-default view_edifact_message" data-message-id="[% quote_messages.0.id | html %]"> <i class="fa fa-quote-left"></i> View EDIFACT quote </button>
161
                                        <button type="button" class="btn btn-default view_edifact_message" data-message-id="[% quote_messages.0.id | html %]" data-basketno="[% basketno | html %]" data-basketname="[% basketname | html %]">
162
                                            <i class="fa fa-quote-left"></i> View EDIFACT quote
163
                                        </button>
162
                                    </div>
164
                                    </div>
163
                                [% ELSIF order_messages.size > 0 %]
165
                                [% ELSIF order_messages.size > 0 %]
164
                                    <!-- Only order messages exist -->
166
                                    <!-- Only order messages exist -->
165
                                    <div class="btn-group">
167
                                    <div class="btn-group">
166
                                        <button type="button" class="btn btn-default view_edifact_message" data-message-id="[% order_messages.0.id | html %]"> <i class="fa fa-shopping-cart"></i> View EDIFACT order </button>
168
                                        <button type="button" class="btn btn-default view_edifact_message" data-message-id="[% order_messages.0.id | html %]" data-basketno="[% basketno | html %]" data-basketname="[% basketname | html %]">
169
                                            <i class="fa fa-shopping-cart"></i> View EDIFACT order
170
                                        </button>
167
                                    </div>
171
                                    </div>
168
                                [% END %]
172
                                [% END %]
169
                            [% END %]
173
                            [% END %]
Lines 323-336 Link Here
323
                                        <ul class="dropdown-menu">
327
                                        <ul class="dropdown-menu">
324
                                            [% FOR message IN quote_messages %]
328
                                            [% FOR message IN quote_messages %]
325
                                                <li>
329
                                                <li>
326
                                                    <a class="dropdown-item view_edifact_message" href="#" data-message-id="[% message.id | html %]">
330
                                                    <a class="dropdown-item view_edifact_message" href="#" data-message-id="[% message.id | html %]" data-basketno="[% basketno | html %]" data-basketname="[% basketname | html %]">
327
                                                        <i class="fa fa-quote-left"></i> View quote ([% message.transfer_date | $KohaDates %])
331
                                                        <i class="fa fa-quote-left"></i> View quote ([% message.transfer_date | $KohaDates %])
328
                                                    </a>
332
                                                    </a>
329
                                                </li>
333
                                                </li>
330
                                            [% END %]
334
                                            [% END %]
331
                                            [% FOR message IN order_messages %]
335
                                            [% FOR message IN order_messages %]
332
                                                <li>
336
                                                <li>
333
                                                    <a class="dropdown-item view_edifact_message" href="#" data-message-id="[% message.id | html %]">
337
                                                    <a class="dropdown-item view_edifact_message" href="#" data-message-id="[% message.id | html %]" data-basketno="[% basketno | html %]" data-basketname="[% basketname | html %]">
334
                                                        <i class="fa fa-shopping-cart"></i> View order ([% message.transfer_date | $KohaDates %])
338
                                                        <i class="fa fa-shopping-cart"></i> View order ([% message.transfer_date | $KohaDates %])
335
                                                    </a>
339
                                                    </a>
336
                                                </li>
340
                                                </li>
Lines 340-351 Link Here
340
                                [% ELSIF quote_messages.size > 0 %]
344
                                [% ELSIF quote_messages.size > 0 %]
341
                                    <!-- Only quote interchanges exist -->
345
                                    <!-- Only quote interchanges exist -->
342
                                    <div class="btn-group">
346
                                    <div class="btn-group">
343
                                        <button type="button" class="btn btn-default view_edifact_message" data-message-id="[% quote_messages.0.id | html %]"> <i class="fa fa-quote-left"></i> View EDIFACT quote </button>
347
                                        <button type="button" class="btn btn-default view_edifact_message" data-message-id="[% quote_messages.0.id | html %]" data-basketno="[% basketno | html %]" data-basketname="[% basketname | html %]">
348
                                            <i class="fa fa-quote-left"></i> View EDIFACT quote
349
                                        </button>
344
                                    </div>
350
                                    </div>
345
                                [% ELSIF order_messages.size > 0 %]
351
                                [% ELSIF order_messages.size > 0 %]
346
                                    <!-- Only order messages exist -->
352
                                    <!-- Only order messages exist -->
347
                                    <div class="btn-group">
353
                                    <div class="btn-group">
348
                                        <button type="button" class="btn btn-default view_edifact_message" data-message-id="[% order_messages.0.id | html %]"> <i class="fa fa-shopping-cart"></i> View EDIFACT order </button>
354
                                        <button type="button" class="btn btn-default view_edifact_message" data-message-id="[% order_messages.0.id | html %]" data-basketno="[% basketno | html %]" data-basketname="[% basketname | html %]">
355
                                            <i class="fa fa-shopping-cart"></i> View EDIFACT order
356
                                        </button>
349
                                    </div>
357
                                    </div>
350
                                [% END %]
358
                                [% END %]
351
                            [% END %]
359
                            [% END %]
Lines 1249-1255 Link Here
1249
<!-- /.modal#dateEditor -->
1257
<!-- /.modal#dateEditor -->
1250
1258
1251
[% IF edifact_enabled && edifact_messages.size > 0 %]
1259
[% IF edifact_enabled && edifact_messages.size > 0 %]
1252
    [% INCLUDE 'modals/edifact-modal.inc' %]
1260
    [% INCLUDE 'modals/edifact_interchange.inc' modal_id='EDI_modal' modal_title='EDIFACT Message' modal_size='xl' show_actions=1 %]
1253
[% END %]
1261
[% END %]
1254
1262
1255
[% MACRO jsinclude BLOCK %]
1263
[% MACRO jsinclude BLOCK %]
Lines 1479-1485 Link Here
1479
    [% PROCESS patron_search_js columns => columns, actions => ["add"], preview_on_name_click => 1 %]
1487
    [% PROCESS patron_search_js columns => columns, actions => ["add"], preview_on_name_click => 1 %]
1480
1488
1481
    [% IF edifact_enabled && edifact_messages.size > 0 %]
1489
    [% IF edifact_enabled && edifact_messages.size > 0 %]
1482
        [% Asset.js("js/modals/edifact-modal.js") | $raw %]
1490
        [% Asset.css("css/edifact_interchange.css") | $raw %]
1491
        [% Asset.js("js/modals/edifact_interchange.js") | $raw %]
1492
        <script>
1493
            EdifactDisplay.init({
1494
                modalId: "#EDI_modal",
1495
                expandByDefault: true,
1496
                showElementDetails: false,
1497
            });
1498
        </script>
1483
    [% END %]
1499
    [% END %]
1484
[% END %]
1500
[% END %]
1485
1501
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/edifactmsgs.tt (-24 / +37 lines)
Lines 12-17 Link Here
12
    [% END %]</title
12
    [% END %]</title
13
>
13
>
14
[% INCLUDE 'doc-head-close.inc' %]
14
[% INCLUDE 'doc-head-close.inc' %]
15
[% Asset.css("css/edifact_interchange.css") | $raw %]
15
</head>
16
</head>
16
17
17
<body id="acq_edifactmsgs" class="acq">
18
<body id="acq_edifactmsgs" class="acq">
Lines 37-53 Link Here
37
    </div>
38
    </div>
38
    <!-- /#acqui_edifactmsgs -->
39
    <!-- /#acqui_edifactmsgs -->
39
40
40
    [% INCLUDE 'modals/edifact-modal.inc' %]
41
    [% INCLUDE 'modals/edifact_interchange.inc' modal_id='EDI_modal' modal_title='EDIFACT Message' modal_size='xl' show_actions=1 %]
42
    [% INCLUDE 'modals/edifact_errors.inc' modal_id='EDI_errors_modal' modal_title='EDIFACT processing errors for' modal_size='lg' %]
41
[% END %]
43
[% END %]
42
44
43
[% MACRO jsinclude BLOCK %]
45
[% MACRO jsinclude BLOCK %]
44
    [% Asset.js("js/acquisitions-menu.js") | $raw %]
46
    [% Asset.js("js/acquisitions-menu.js") | $raw %]
45
    [% INCLUDE 'datatables.inc' %]
47
    [% INCLUDE 'datatables.inc' %]
46
    [% Asset.js("js/modals/edifact-modal.js") | $raw %]
48
    [% Asset.js("js/modals/edifact_interchange.js") | $raw %]
49
    [% Asset.js("js/modals/edifact_errors.js") | $raw %]
47
    <script>
50
    <script>
48
        $(document).ready(function () {
51
        var interface = "[% interface | html %]";
49
            let edi_msgs_table_url = "/api/v1/acquisitions/edifiles?";
52
        var theme = "[% theme | html %]";
53
        $(document).ready(function() {
54
            // Initialize enhanced EDIFACT display
55
            EdifactDisplay.init({
56
                modalId: '#EDI_modal',
57
                expandByDefault: true,
58
                showElementDetails: false
59
            });
60
61
            // Get CSRF token once for reuse
62
            var csrfToken = $('meta[name="csrf-token"]').attr("content") ? $('meta[name="csrf-token"]').attr("content").trim() : '';
50
63
64
            var edi_msgs_table_url = '/api/v1/acquisitions/edifiles?';
51
            var edi_msgs_table = $("#edi_msgs").kohaTable(
65
            var edi_msgs_table = $("#edi_msgs").kohaTable(
52
                {
66
                {
53
                    ajax: {
67
                    ajax: {
Lines 79-97 Link Here
79
                            render: function (daya, type, row, meta) {
93
                            render: function (daya, type, row, meta) {
80
                                let rendered = "";
94
                                let rendered = "";
81
                                if (row.errors.length > 0) {
95
                                if (row.errors.length > 0) {
82
                                    let errorsList = "<ul>";
96
                                    // JSON encode the errors instead of constructing HTML
83
                                    row.errors.forEach(error => {
97
                                    let errorsJson = JSON.stringify(row.errors);
84
                                        if (error.details) {
98
                                    // Escape quotes for HTML attribute
85
                                            errorsList += "<li>";
99
                                    errorsJson = errorsJson.replace(/"/g, '&quot;');
86
                                            errorsList += "<strong>" + error.details + "</strong>";
100
                                    rendered = row.status + ' (<a href="#" data-bs-toggle="modal" data-bs-target="#EDI_errors_modal" data-bs-errors="' + errorsJson + '" data-bs-filename="' + row.filename + '">' + _("with errors") + "</a>)";
87
                                            if (error.section) {
88
                                                errorsList += "<br/><pre>" + error.section + "</pre>";
89
                                            }
90
                                            errorsList += "</li>";
91
                                        }
92
                                    });
93
                                    errorsList += "</ul>";
94
                                    rendered = row.status + ' (<a href="#" data-bs-toggle="modal" data-bs-target="#EDI_errors_modal" data-bs-errors="' + errorsList + '" data-bs-filename="' + row.filename + '">' + _("with errors") + "</a>)";
95
                                } else {
101
                                } else {
96
                                    rendered = row.status;
102
                                    rendered = row.status;
97
                                }
103
                                }
Lines 142-156 Link Here
142
                            searchable: false,
148
                            searchable: false,
143
                            orderable: false,
149
                            orderable: false,
144
                            render: function (data, type, row, meta) {
150
                            render: function (data, type, row, meta) {
145
                                var result =
151
                                var result = '<div class="btn-group" role="group">';
146
                                    '<a class="btn btn-default btn-xs view_message" target="_blank" href="/cgi-bin/koha/acqui/edimsg.pl?id=' + encodeURIComponent(row.id) + '"><i class="fa fa-search"></i> ' + _("View message") + "</a> ";
152
147
                                result += '<form action="/cgi-bin/koha/acqui/edifactmsgs.pl" method="post"><input type="hidden" name="message_id" value="' + encodeURIComponent(row.id) + '" />';
153
                                // Enhanced view button
148
                                result += '<input type="hidden" name="csrf_token" value="' + $('meta[name="csrf-token"]').attr("content").trim() + '" />';
154
                                result += '<a class="btn btn-info btn-xs view_edifact_message" href="#" data-message-id="' + encodeURIComponent(row.id) + '"><i class="fa fa-eye"></i> ' + _("View message") + "</a>";
149
                                result += '<input type="hidden" name="op" value="cud-delete" /><button type="submit" class="btn btn-default btn-xs delete_msg"><i class="fa fa-trash-can"></i> ' + _("Delete") + "</button> ";
155
156
                                // Delete button
157
                                result += '<form action="/cgi-bin/koha/acqui/edifactmsgs.pl" method="post" style="display:inline;"><input type="hidden" name="message_id" value="' + encodeURIComponent(row.id) + '" />';
158
                                result += '<input type="hidden" name="csrf_token" value="' + csrfToken + '" />';
159
                                result += '<input type="hidden" name="op" value="cud-delete" /><button type="submit" class="btn btn-default btn-xs delete_msg"><i class="fa fa-trash-can"></i> ' + _("Delete") + "</button></form>";
160
161
                                // Import button
150
                                if (row.status == "new") {
162
                                if (row.status == "new") {
151
                                    result +=
163
                                    result += '<a class="btn btn-default btn-xs import_msg" href="/cgi-bin/koha/acqui/edifactmsgs.pl?op=import&amp;message_id=' + encodeURIComponent(row.id) + '"><i class="fa fa-cog"></i> ' + _("Import") + "</a>";
152
                                        '<a class="btn btn-default btn-xs import_msg" href="/cgi-bin/koha/acqui/edifactmsgs.pl?op=import&amp;message_id=' + encodeURIComponent(row.id) + '"><i class="fa fa-cog"></i> ' + _("Import") + "</a> ";
153
                                }
164
                                }
165
166
                                result += '</div>';
154
                                return result;
167
                                return result;
155
                            },
168
                            },
156
                        },
169
                        },
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/invoice.tt (-3 / +11 lines)
Lines 67-73 Link Here
67
                [% END %]
67
                [% END %]
68
            </p>
68
            </p>
69
            <p>
69
            <p>
70
                <a href="#" class="btn btn-default btn-sm view_edifact_message" data-message-id="[% edifact_message.id | html %]"> <i class="fa fa-search"></i> View interchange</a>
70
                <a href="#" class="btn btn-default btn-sm view_edifact_message" data-message-id="[% edifact_message.id | html %]" data-invoicenumber="[% invoicenumber | html %]"> <i class="fa fa-search"></i> View interchange</a>
71
            </p>
71
            </p>
72
72
73
            [% IF edifact_errors.size > 0 %]
73
            [% IF edifact_errors.size > 0 %]
Lines 555-561 Link Here
555
</div>
555
</div>
556
<!-- /#updateFund.modal -->
556
<!-- /#updateFund.modal -->
557
557
558
[% INCLUDE 'modals/edifact-modal.inc' %]
558
[% INCLUDE 'modals/edifact_interchange.inc' modal_id='EDI_modal' modal_title='EDIFACT Message' modal_size='xl' show_actions=1 %]
559
559
560
<div style="display:none;">
560
<div style="display:none;">
561
    <select id="all_fund_dropdown">
561
    <select id="all_fund_dropdown">
Lines 578-584 Link Here
578
    [% Asset.js("js/additional-fields-entry.js") | $raw %]
578
    [% Asset.js("js/additional-fields-entry.js") | $raw %]
579
    [% INCLUDE 'calendar.inc' %]
579
    [% INCLUDE 'calendar.inc' %]
580
    [% INCLUDE 'datatables.inc' %]
580
    [% INCLUDE 'datatables.inc' %]
581
    [% Asset.js("js/modals/edifact-modal.js") | $raw %]
581
    [% Asset.css("css/edifact_interchange.css") | $raw %]
582
    [% Asset.js("js/modals/edifact_interchange.js") | $raw %]
583
    <script>
584
        EdifactDisplay.init({
585
            modalId: "#EDI_modal",
586
            expandByDefault: true,
587
            showElementDetails: false,
588
        });
589
    </script>
582
    <script>
590
    <script>
583
        function updateColumnsVisibility(visible) {
591
        function updateColumnsVisibility(visible) {
584
            if ( visible ) {
592
            if ( visible ) {
(-)a/koha-tmpl/intranet-tmpl/prog/js/modals/edifact-modal.js (-70 lines)
Lines 1-70 Link Here
1
// EDIFACT modal JavaScript functionality
2
// This script handles the EDIFACT message modal interactions
3
// Include this after the edifact-modal.inc template
4
5
/* global __ interface theme */
6
$(document).ready(function () {
7
    // Initialize modal elements
8
    var EDIModal = $("#EDI_modal");
9
    var EDIModalBody = $("#EDI_modal .modal-body");
10
    var EDIErrorsModal = $("#EDI_errors_modal");
11
12
    // Handle view EDIFACT message button clicks
13
    $("body").on("click", ".view_edifact_message", function (e) {
14
        e.preventDefault();
15
        var message_id = $(this).data("message-id");
16
        var page =
17
            "/cgi-bin/koha/acqui/edimsg.pl?id=" +
18
            encodeURIComponent(message_id);
19
        EDIModalBody.load(page + " #edimsg");
20
        EDIModal.modal("show");
21
    });
22
23
    // Handle view_message button clicks (for compatibility with existing code)
24
    $("body").on("click", ".view_message", function (e) {
25
        e.preventDefault();
26
        var page = $(this).attr("href");
27
        EDIModalBody.load(page + " #edimsg");
28
        EDIModal.modal("show");
29
    });
30
31
    // Handle modal close button
32
    EDIModal.on("click", ".btn-close", function (e) {
33
        e.preventDefault();
34
        EDIModal.modal("hide");
35
    });
36
37
    // Reset modal content when hidden
38
    EDIModal.on("hidden.bs.modal", function () {
39
        EDIModalBody.html(
40
            "<div id='edi_loading'><img style='display:inline-block' src='" +
41
                interface +
42
                "/" +
43
                theme +
44
                "/img/spinner-small.gif' alt='' /> Loading</div>"
45
        );
46
    });
47
48
    // Handle EDIFACT errors modal
49
    const errorsModal = document.getElementById("EDI_errors_modal");
50
    if (errorsModal) {
51
        errorsModal.addEventListener("show.bs.modal", function (event) {
52
            // Link that triggered the modal
53
            const link = event.relatedTarget;
54
55
            // Extract info from data-bs-* attributes
56
            const filename = link.getAttribute("data-bs-filename");
57
            const errors = link.getAttribute("data-bs-errors");
58
59
            // Update the modal's title
60
            const modalTitleSpan = errorsModal.querySelector(
61
                ".modal-title #EDI_errors_filename"
62
            );
63
            modalTitleSpan.textContent = filename;
64
65
            // Update the modal's content
66
            const modalBody = errorsModal.querySelector(".modal-body");
67
            modalBody.innerHTML = errors;
68
        });
69
    }
70
});
(-)a/koha-tmpl/intranet-tmpl/prog/js/modals/edifact_errors.js (+133 lines)
Line 0 Link Here
1
/**
2
 * EDIFACT Errors Display Module
3
 */
4
const EdifactErrorsDisplay = (() => {
5
    "use strict";
6
7
    const defaults = {
8
        modalId: "#EDI_errors_modal",
9
        modalBodySelector: ".modal-body",
10
        filenameSelectorInTitle: ".filename",
11
    };
12
13
    const init = (options = {}) => {
14
        const settings = { ...defaults, ...options };
15
        initializeModal(settings);
16
    };
17
18
    const initializeModal = settings => {
19
        const modal = $(settings.modalId);
20
        if (!modal.length) return;
21
22
        modal.on("show.bs.modal", event => {
23
            const button = event.relatedTarget;
24
            if (!button) return;
25
26
            const filename =
27
                button.getAttribute("data-bs-filename") ||
28
                button.getAttribute("data-filename");
29
            const errors =
30
                button.getAttribute("data-bs-errors") ||
31
                button.getAttribute("data-errors");
32
33
            updateFilename(modal, settings, filename);
34
            updateErrorsContent(modal, settings, errors);
35
        });
36
37
        modal.on("hidden.bs.modal", () => {
38
            resetModalContent(modal, settings);
39
        });
40
    };
41
42
    const updateFilename = (modal, settings, filename) => {
43
        const filenameSpan = modal.find(settings.filenameSelectorInTitle);
44
        if (filenameSpan.length && filename) {
45
            filenameSpan.text(filename);
46
        }
47
    };
48
49
    const updateErrorsContent = (modal, settings, errors) => {
50
        const modalBody = modal.find(settings.modalBodySelector);
51
        if (!modalBody.length) return;
52
53
        if (errors) {
54
            try {
55
                const errorsData = JSON.parse(errors.replace(/&quot;/g, '"'));
56
                modalBody.html(buildErrorsDisplay(errorsData));
57
            } catch (e) {
58
                console.error("Error parsing errors JSON:", e);
59
                modalBody.html(
60
                    `<div class="alert alert-danger">Error loading error details: ${escapeHtml(e.message)}</div>`
61
                );
62
            }
63
        } else {
64
            modalBody.html(
65
                '<div class="alert alert-info">No errors found for this interchange.</div>'
66
            );
67
        }
68
    };
69
70
    const buildErrorsDisplay = errorsData => {
71
        if (!errorsData?.length) {
72
            return '<div class="alert alert-info">No errors found for this interchange.</div>';
73
        }
74
75
        const errorItems = errorsData
76
            .map(
77
                error => `
78
            <li class="list-group-item list-group-item-danger">
79
                <strong>Error:</strong> ${escapeHtml(error.details || "Unknown error")}
80
                ${
81
                    error.section
82
                        ? `
83
                    <br><small><strong>Section:</strong></small>
84
                    <pre class="mt-1 mb-0" style="font-size: 0.85em; background-color: #f8f9fa; padding: 0.5rem; border-radius: 0.25rem;">${escapeHtml(error.section)}</pre>
85
                `
86
                        : ""
87
                }
88
                ${
89
                    error.date
90
                        ? `
91
                    <br><small class="text-muted"><strong>Date:</strong> ${escapeHtml(error.date)}</small>
92
                `
93
                        : ""
94
                }
95
            </li>
96
        `
97
            )
98
            .join("");
99
100
        return `<ul class="list-group">${errorItems}</ul>`;
101
    };
102
103
    const resetModalContent = (modal, settings) => {
104
        const modalBody = modal.find(settings.modalBodySelector);
105
        const filenameSpan = modal.find(settings.filenameSelectorInTitle);
106
107
        modalBody.html(`
108
            <div class="edi-loading">
109
                <img src="/intranet-tmpl/${window.theme || "prog"}/img/spinner-small.gif" alt="" />
110
                Loading errors...
111
            </div>
112
        `);
113
        filenameSpan.text("");
114
    };
115
116
    const escapeHtml = str => {
117
        if (!str) return "";
118
        const escapeMap = {
119
            "&": "&amp;",
120
            "<": "&lt;",
121
            ">": "&gt;",
122
            '"': "&quot;",
123
            "'": "&#039;",
124
        };
125
        return str.replace(/[&<>"']/g, match => escapeMap[match]);
126
    };
127
128
    return { init };
129
})();
130
131
$(document).ready(() => {
132
    EdifactErrorsDisplay.init();
133
});
(-)a/koha-tmpl/intranet-tmpl/prog/js/modals/edifact_interchange.js (-1 / +1031 lines)
Line 0 Link Here
0
- 
1
/**
2
 * EDIFACT Display Module
3
 */
4
const EdifactDisplay = (() => {
5
    "use strict";
6
7
    const defaults = {
8
        modalId: "#EDI_modal",
9
        modalBodySelector: ".modal-body",
10
        jsonEndpoint: "/cgi-bin/koha/acqui/edimsg.pl",
11
        expandByDefault: true,
12
    };
13
14
    const extractFocusOptions = element => {
15
        const options = {};
16
        const basketno = element.dataset.basketno;
17
        const basketname = element.dataset.basketname;
18
        const invoicenumber = element.dataset.invoicenumber;
19
20
        if (basketno) options.basketno = basketno;
21
        if (basketname) options.basketname = basketname;
22
        if (invoicenumber) options.invoicenumber = invoicenumber;
23
24
        return Object.keys(options).length > 0 ? options : null;
25
    };
26
27
    const init = (options = {}) => {
28
        const settings = { ...defaults, ...options };
29
30
        document.body.addEventListener("click", e => {
31
            if (e.target.closest(".view_edifact_message")) {
32
                e.preventDefault();
33
                const button = e.target.closest(".view_edifact_message");
34
                const messageId = button.dataset.messageId;
35
                const customUrl = button.dataset.url;
36
                const focusOptions = extractFocusOptions(button);
37
38
                if (messageId) {
39
                    showMessage(messageId, settings, focusOptions);
40
                } else if (customUrl) {
41
                    showMessageFromUrl(customUrl, settings, focusOptions);
42
                }
43
            }
44
45
            if (e.target.closest(".view_message_enhanced")) {
46
                e.preventDefault();
47
                const link = e.target.closest(".view_message_enhanced");
48
                const href = link.getAttribute("href");
49
                const focusOptions = extractFocusOptions(link);
50
51
                showMessageFromUrl(href, settings, focusOptions);
52
            }
53
        });
54
55
        initializeModal(settings);
56
    };
57
58
    const showMessage = (messageId, settings, focusOptions) => {
59
        const url = `${settings.jsonEndpoint}?id=${encodeURIComponent(messageId)}&format=json`;
60
        showMessageFromUrl(url, settings, focusOptions);
61
    };
62
63
    const showMessageFromUrl = (url, settings, focusOptions) => {
64
        const modal = $(settings.modalId);
65
        const modalContent = modal.find(".modal-content");
66
        const modalBody = modal.find(settings.modalBodySelector);
67
        const loadingHtml = `<div class="edi-loading">
68
            <img src="/intranet-tmpl/${window.theme || "prog"}/img/spinner-small.gif" alt="" />
69
            Loading
70
        </div>`;
71
72
        // Remove any existing EDIFACT navbars
73
        modalContent.find(".edi-main-navbar, .edi-tree-toolbar").remove();
74
75
        modalBody.html(loadingHtml);
76
        modal.modal("show");
77
78
        const jsonUrl = url + (url.includes("?") ? "&" : "?") + "format=json";
79
80
        $.ajax({
81
            url: jsonUrl,
82
            type: "GET",
83
            dataType: "json",
84
            success: data => {
85
                // Build components separately
86
                const mainNavbar = createMainNavbar();
87
                const treeToolbar = createTreeToolbar();
88
                const treeView = buildEdiTree(data, settings, focusOptions);
89
                const rawView = buildEdiRaw(data, settings, focusOptions);
90
                rawView.classList.add("hidden");
91
92
                // Add class for identification
93
                mainNavbar.classList.add("edi-main-navbar");
94
95
                // Insert navbar between header and body
96
                modalBody.before(mainNavbar);
97
98
                // Insert tree toolbar between navbar and body
99
                modalBody.before(treeToolbar);
100
101
                // Insert views directly into modal body
102
                modalBody.empty();
103
                modalBody.addClass("edi-content");
104
                modalBody.append(treeView, rawView);
105
106
                // Re-run initialization since DOM structure changed
107
                setTimeout(() => {
108
                    initializeViewToggle(modalContent[0], data);
109
                    initializeExpandCollapse(modalContent[0]);
110
                    initializeSearch(modalContent[0]);
111
112
                    if (focusOptions) {
113
                        applyFocusOptions(modalContent[0], focusOptions);
114
                    }
115
                }, 0);
116
            },
117
            error: () => {
118
                modalBody.html(
119
                    '<div class="alert alert-danger">Failed to load message</div>'
120
                );
121
            },
122
        });
123
    };
124
125
    const createMainNavbar = () => {
126
        const nav = document.createElement("nav");
127
        nav.className = "navbar navbar-light bg-light border-bottom py-2";
128
        nav.innerHTML = `
129
            <div class="container-fluid align-items-center">
130
                <!-- Left: View toggle buttons -->
131
                <div class="btn-group me-2" role="group" aria-label="Display style">
132
                    <button type="button" class="btn btn-outline-secondary active" data-view="tree" title="Tree view">
133
                        <i class="fa fa-sitemap fa-fw"></i>
134
                    </button>
135
                    <button type="button" class="btn btn-outline-secondary" data-view="raw" title="Raw view">
136
                        <i class="fa fa-file-text fa-fw"></i>
137
                    </button>
138
                </div>
139
140
                <!-- Center: Search form -->
141
                <form class="d-flex flex-grow-1 mx-2 edi-search-form" role="search">
142
                    <input class="form-control me-2 edi-search-input" type="search" placeholder="Search segments..." aria-label="Search segments">
143
                    <button class="btn btn-outline-secondary me-1 edi-search-prev" type="button" title="Previous result" disabled>
144
                        <i class="fa fa-chevron-up"></i>
145
                    </button>
146
                    <button class="btn btn-outline-secondary edi-search-next" type="button" title="Next result" disabled>
147
                        <i class="fa fa-chevron-down"></i>
148
                    </button>
149
                </form>
150
151
                <!-- Right: Search results -->
152
                <div class="text-muted small text-nowrap">
153
                    <span class="edi-search-count">0 results</span>
154
                </div>
155
            </div>`;
156
        return nav;
157
    };
158
159
    const createTreeToolbar = () => {
160
        const toolbar = document.createElement("div");
161
        toolbar.className =
162
            "navbar navbar-light bg-light border-bottom py-2 edi-tree-toolbar";
163
        toolbar.innerHTML = `
164
            <div class="container-fluid d-flex flex-row-reverse">
165
                <div class="btn-group btn-group-sm" role="group" aria-label="Tree controls">
166
                    <button type="button" class="btn btn-outline-secondary expand-all-btn" title="Expand all sections">
167
                        <i class="fa fa-expand me-1"></i>Expand all
168
                    </button>
169
                    <button type="button" class="btn btn-outline-secondary collapse-all-btn" title="Collapse all sections">
170
                        <i class="fa fa-compress me-1"></i>Collapse all
171
                    </button>
172
                </div>
173
            </div>`;
174
        return toolbar;
175
    };
176
177
    const buildEdiRaw = (data, settings, focusOptions) => {
178
        const rawDiv = document.createElement("div");
179
        rawDiv.className = "edi-raw-view";
180
181
        const lines = [];
182
183
        if (data.header) {
184
            lines.push(
185
                `<div class="segment-line"><span class="segment-tag">UNB</span>${data.header.substring(3)}</div>`
186
            );
187
        }
188
189
        data.messages.forEach((message, messageIndex) => {
190
            const isMessageFocused =
191
                focusOptions &&
192
                messageMatchesFocusOptions(message, focusOptions);
193
            const focusClass = isMessageFocused
194
                ? " edi-focused-segment-line"
195
                : "";
196
            const messageAttr = ` data-message-index="${messageIndex}"`;
197
198
            if (message.header) {
199
                lines.push(
200
                    `<div class="segment-line${focusClass}"${messageAttr}><span class="segment-tag">UNH</span>${message.header.substring(3)}</div>`
201
                );
202
            }
203
204
            message.segments.forEach(segment => {
205
                const tag = segment.tag || "";
206
                const content = (segment.raw || "").substring(tag.length);
207
                lines.push(
208
                    `<div class="segment-line${focusClass}"${messageAttr}><span class="segment-tag">${escapeHtml(tag)}</span>${escapeHtml(content)}</div>`
209
                );
210
            });
211
212
            if (message.trailer) {
213
                lines.push(
214
                    `<div class="segment-line${focusClass}"${messageAttr}><span class="segment-tag">UNT</span>${message.trailer.substring(3)}</div>`
215
                );
216
            }
217
        });
218
219
        if (data.trailer) {
220
            lines.push(
221
                `<div class="segment-line"><span class="segment-tag">UNZ</span>${data.trailer.substring(3)}</div>`
222
            );
223
        }
224
225
        rawDiv.innerHTML = lines.join("");
226
        return rawDiv;
227
    };
228
229
    const initializeViewToggle = (container, data) => {
230
        const toggleButtons = container.querySelectorAll("button[data-view]");
231
        const treeView = container.querySelector(".edi-tree");
232
        const rawView = container.querySelector(".edi-raw-view");
233
        const treeToolbar = container.querySelector(".edi-tree-toolbar");
234
235
        container.edifactData = data;
236
237
        toggleButtons.forEach(button => {
238
            button.addEventListener("click", () => {
239
                const viewType = button.dataset.view;
240
241
                // Update button states
242
                toggleButtons.forEach(btn => btn.classList.remove("active"));
243
                button.classList.add("active");
244
245
                // Toggle views and tree toolbar
246
                if (viewType === "tree") {
247
                    treeView.classList.remove("hidden");
248
                    rawView.classList.add("hidden");
249
                    treeToolbar.classList.remove("d-none");
250
                } else {
251
                    treeView.classList.add("hidden");
252
                    rawView.classList.remove("hidden");
253
                    treeToolbar.classList.add("d-none");
254
                }
255
256
                // Re-execute search if active
257
                if (container.searchFunctions) {
258
                    const searchInput =
259
                        container.querySelector(".edi-search-input");
260
                    if (searchInput?.value.trim()) {
261
                        container.searchFunctions.performSearch(
262
                            searchInput.value.trim()
263
                        );
264
                    }
265
                }
266
267
                // Re-apply focus highlighting
268
                if (container.focusOptions) {
269
                    setTimeout(() => {
270
                        const focusTarget =
271
                            viewType === "tree"
272
                                ? container.querySelector(
273
                                      '[data-focus-message="true"].edi-focused-message'
274
                                  )
275
                                : rawView.querySelector(
276
                                      ".edi-focused-segment-line"
277
                                  );
278
279
                        focusTarget?.scrollIntoView({
280
                            behavior: "smooth",
281
                            block: "start",
282
                        });
283
                    }, 100);
284
                }
285
            });
286
        });
287
    };
288
289
    const initializeExpandCollapse = container => {
290
        const treeView = container.querySelector(".edi-tree");
291
292
        container
293
            .querySelector(".expand-all-btn")
294
            ?.addEventListener("click", () => {
295
                const collapseElements = treeView.querySelectorAll(".collapse");
296
                collapseElements.forEach(el => {
297
                    el.classList.add("show");
298
                    if (typeof bootstrap !== "undefined") {
299
                        bootstrap.Collapse.getOrCreateInstance(el)?.show();
300
                    }
301
                });
302
            });
303
304
        container
305
            .querySelector(".collapse-all-btn")
306
            ?.addEventListener("click", () => {
307
                const collapseElements = treeView.querySelectorAll(".collapse");
308
                collapseElements.forEach(el => {
309
                    if (typeof bootstrap !== "undefined") {
310
                        bootstrap.Collapse.getOrCreateInstance(el).hide();
311
                    } else {
312
                        el.classList.remove("show");
313
                    }
314
                });
315
            });
316
    };
317
318
    const initializeSearch = container => {
319
        const searchInput = container.querySelector(".edi-search-input");
320
        const searchCount = container.querySelector(".edi-search-count");
321
        const searchPrev = container.querySelector(".edi-search-prev");
322
        const searchNext = container.querySelector(".edi-search-next");
323
        const treeView = container.querySelector(".edi-tree");
324
        const rawView = container.querySelector(".edi-raw-view");
325
326
        let currentResults = [];
327
        let currentIndex = -1;
328
        let searchTimeout = null;
329
330
        const performSearch = query => {
331
            clearSearch(false);
332
333
            if (!query || query.length < 2) {
334
                updateSearchUI(0, -1);
335
                return;
336
            }
337
338
            const activeView = container.querySelector(".edi-tree:not(.hidden)")
339
                ? "tree"
340
                : "raw";
341
            const searchTargets =
342
                activeView === "tree"
343
                    ? treeView.querySelectorAll(".segment")
344
                    : rawView.querySelectorAll(".segment-line");
345
346
            const regex = new RegExp(escapeRegExp(query), "i");
347
348
            searchTargets.forEach(element => {
349
                const textContent = element.textContent || element.innerText;
350
                if (regex.test(textContent)) {
351
                    highlightMatches(element, query);
352
                    currentResults.push({ element, view: activeView });
353
                }
354
            });
355
356
            if (activeView === "tree") {
357
                smartExpandCollapseForSearch(currentResults);
358
            }
359
360
            updateSearchUI(currentResults.length, currentIndex);
361
362
            if (currentResults.length > 0) {
363
                navigateToResult(0);
364
            }
365
        };
366
367
        const highlightMatches = (element, query) => {
368
            const regex = new RegExp(`(${escapeRegExp(query)})`, "gi");
369
            const walker = document.createTreeWalker(
370
                element,
371
                NodeFilter.SHOW_TEXT
372
            );
373
374
            const textNodes = [];
375
            let node;
376
            while ((node = walker.nextNode())) {
377
                textNodes.push(node);
378
            }
379
380
            textNodes.forEach(textNode => {
381
                if (regex.test(textNode.textContent)) {
382
                    const highlightedHTML = textNode.textContent.replace(
383
                        regex,
384
                        '<mark class="edi-search-highlight">$1</mark>'
385
                    );
386
                    const wrapper = document.createElement("span");
387
                    wrapper.innerHTML = highlightedHTML;
388
                    textNode.parentNode.replaceChild(wrapper, textNode);
389
                }
390
            });
391
        };
392
393
        const smartExpandCollapseForSearch = results => {
394
            setTimeout(() => {
395
                const allCollapseElements =
396
                    treeView.querySelectorAll(".collapse");
397
                const elementsWithResults = new Set();
398
399
                results.forEach(result => {
400
                    let current = result.element;
401
                    while (current && current !== treeView) {
402
                        if (current.classList?.contains("collapse")) {
403
                            elementsWithResults.add(current);
404
                        }
405
                        current = current.parentElement;
406
                    }
407
                });
408
409
                allCollapseElements.forEach(collapseElement => {
410
                    const collapse =
411
                        typeof bootstrap !== "undefined"
412
                            ? bootstrap.Collapse.getOrCreateInstance(
413
                                  collapseElement,
414
                                  { toggle: false }
415
                              )
416
                            : null;
417
418
                    if (elementsWithResults.has(collapseElement)) {
419
                        collapse
420
                            ? collapse.show()
421
                            : collapseElement.classList.add("show");
422
                    } else {
423
                        collapse
424
                            ? collapse.hide()
425
                            : collapseElement.classList.remove("show");
426
                    }
427
                });
428
            }, 50);
429
        };
430
431
        const navigateResults = direction => {
432
            if (currentResults.length === 0) return;
433
434
            let newIndex = currentIndex + direction;
435
            if (newIndex >= currentResults.length) newIndex = 0;
436
            if (newIndex < 0) newIndex = currentResults.length - 1;
437
438
            navigateToResult(newIndex);
439
        };
440
441
        const navigateToResult = index => {
442
            if (index < 0 || index >= currentResults.length) return;
443
444
            currentResults[currentIndex]?.element.classList.remove(
445
                "edi-search-current"
446
            );
447
448
            currentIndex = index;
449
            const result = currentResults[currentIndex];
450
451
            result.element.classList.add("edi-search-current");
452
            result.element.scrollIntoView({
453
                behavior: "smooth",
454
                block: "center",
455
            });
456
457
            updateSearchUI(currentResults.length, currentIndex);
458
        };
459
460
        const clearSearch = (clearInput = true) => {
461
            if (clearInput && searchInput) {
462
                searchInput.value = "";
463
            }
464
465
            container
466
                .querySelectorAll(".edi-search-highlight, .edi-search-current")
467
                .forEach(el => {
468
                    if (el.classList.contains("edi-search-highlight")) {
469
                        el.parentNode.replaceChild(
470
                            document.createTextNode(el.textContent),
471
                            el
472
                        );
473
                    } else {
474
                        el.classList.remove("edi-search-current");
475
                    }
476
                });
477
478
            container.querySelectorAll("span:not([class])").forEach(wrapper => {
479
                if (
480
                    wrapper.children.length === 0 ||
481
                    (wrapper.children.length === 1 &&
482
                        wrapper.children[0].tagName === "MARK")
483
                ) {
484
                    wrapper.parentNode.replaceChild(
485
                        document.createTextNode(wrapper.textContent),
486
                        wrapper
487
                    );
488
                }
489
            });
490
491
            currentResults = [];
492
            currentIndex = -1;
493
            updateSearchUI(0, -1);
494
        };
495
496
        const updateSearchUI = (totalResults, currentIdx) => {
497
            if (searchCount) {
498
                searchCount.textContent =
499
                    totalResults === 0
500
                        ? "0 results"
501
                        : `${currentIdx + 1} of ${totalResults}`;
502
            }
503
504
            const hasResults = totalResults > 0;
505
            if (searchPrev) searchPrev.disabled = !hasResults;
506
            if (searchNext) searchNext.disabled = !hasResults;
507
        };
508
509
        const escapeRegExp = string =>
510
            string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
511
512
        // Event listeners
513
        searchInput?.addEventListener("input", () => {
514
            clearTimeout(searchTimeout);
515
            const value = searchInput.value.trim();
516
517
            // If field was cleared (native clear button or manual), clear search
518
            if (value === "") {
519
                clearSearch(false); // Don't clear input since it's already empty
520
            } else {
521
                searchTimeout = setTimeout(() => performSearch(value), 500);
522
            }
523
        });
524
525
        searchInput?.addEventListener("keydown", e => {
526
            if (e.key === "Enter") {
527
                e.preventDefault();
528
                navigateResults(e.shiftKey ? -1 : 1);
529
            }
530
        });
531
532
        // Prevent form submission
533
        const searchForm = container.querySelector(".edi-search-form");
534
        searchForm?.addEventListener("submit", e => e.preventDefault());
535
536
        searchPrev?.addEventListener("click", () => navigateResults(-1));
537
        searchNext?.addEventListener("click", () => navigateResults(1));
538
539
        container.searchFunctions = {
540
            performSearch,
541
            clearSearch,
542
            navigateResults,
543
        };
544
    };
545
546
    const messageMatchesFocusOptions = (message, focusOptions) => {
547
        if (!message.segments || !focusOptions) return false;
548
549
        // Check if basketno is a whole number and pad it
550
        if (/^\d+$/.test(String(focusOptions.basketno))) {
551
            const strVal = String(focusOptions.basketno);
552
            if (11 > strVal.length) {
553
                focusOptions.basketno = strVal.padStart(11, "0");
554
            }
555
        }
556
557
        return message.segments.some(segment => {
558
            // Check basketno/invoicenumber in BGM segments
559
            if (segment.tag === "BGM" && segment.elements?.length > 1) {
560
                const bgmValue = String(segment.elements[1]);
561
562
                if (
563
                    (focusOptions.basketno &&
564
                        bgmValue === String(focusOptions.basketno)) ||
565
                    (focusOptions.invoicenumber &&
566
                        bgmValue === String(focusOptions.invoicenumber))
567
                ) {
568
                    return true;
569
                }
570
            }
571
572
            // Check basketname/invoicenumber in RFF segments
573
            if (segment.tag === "RFF" && segment.elements?.length >= 2) {
574
                const refType = segment.elements[0];
575
                const refValue = String(segment.elements[1]);
576
577
                if (
578
                    focusOptions.basketname &&
579
                    refType === "ON" &&
580
                    refValue === String(focusOptions.basketname)
581
                ) {
582
                    return true;
583
                }
584
585
                if (
586
                    focusOptions.invoicenumber &&
587
                    (refType === "IV" || refType === "VN") &&
588
                    refValue === String(focusOptions.invoicenumber)
589
                ) {
590
                    return true;
591
                }
592
            }
593
594
            return false;
595
        });
596
    };
597
598
    const applyFocusOptions = (container, focusOptions) => {
599
        container.focusOptions = focusOptions;
600
601
        setTimeout(() => {
602
            const focusedMessage = container.querySelector(
603
                '[data-focus-message="true"]'
604
            );
605
            if (focusedMessage) {
606
                focusedMessage.classList.add("edi-focused-message");
607
608
                const treeView = container.querySelector(".edi-tree");
609
                if (treeView && !treeView.classList.contains("hidden")) {
610
                    focusedMessage.scrollIntoView({
611
                        behavior: "smooth",
612
                        block: "start",
613
                    });
614
                }
615
616
                container.setAttribute("data-has-focused-message", "true");
617
            }
618
619
            const rawView = container.querySelector(".edi-raw-view");
620
            if (rawView && !rawView.classList.contains("hidden")) {
621
                const firstFocusedLine = rawView.querySelector(
622
                    ".edi-focused-segment-line"
623
                );
624
                firstFocusedLine?.scrollIntoView({
625
                    behavior: "smooth",
626
                    block: "start",
627
                });
628
            }
629
        }, 200);
630
    };
631
632
    const buildEdiTree = (data, settings, focusOptions) => {
633
        const rootUl = document.createElement("ul");
634
        rootUl.className = "edi-tree list-unstyled";
635
636
        const hasMatchingMessage =
637
            focusOptions &&
638
            data.messages &&
639
            data.messages.some(msg =>
640
                messageMatchesFocusOptions(msg, focusOptions)
641
            );
642
643
        const effectiveFocusOptions =
644
            focusOptions && !hasMatchingMessage ? null : focusOptions;
645
        const effectiveSettings =
646
            focusOptions && !hasMatchingMessage
647
                ? { ...settings, expandByDefault: true }
648
                : settings;
649
650
        const interchangeLi = buildInterchangeLevel(
651
            data,
652
            effectiveSettings,
653
            effectiveFocusOptions
654
        );
655
        rootUl.appendChild(interchangeLi);
656
657
        return rootUl;
658
    };
659
660
    const buildInterchangeLevel = (data, settings, focusOptions) => {
661
        const interchangeLi = document.createElement("li");
662
        const interchangeId = `interchange_${Date.now()}`;
663
        const shouldExpand = focusOptions ? true : settings.expandByDefault;
664
665
        const headerDiv = createSegmentDiv(
666
            "header",
667
            data.header,
668
            shouldExpand,
669
            true,
670
            interchangeId
671
        );
672
        interchangeLi.appendChild(headerDiv);
673
674
        const messagesUl = document.createElement("ul");
675
        messagesUl.id = interchangeId;
676
        messagesUl.className = `collapse${shouldExpand ? " show" : ""}`;
677
678
        data.messages.forEach((message, i) => {
679
            const messageLi = buildMessageLevel(
680
                message,
681
                i,
682
                settings,
683
                focusOptions
684
            );
685
            messagesUl.appendChild(messageLi);
686
        });
687
688
        const trailerDiv = createSegmentDiv(
689
            "trailer",
690
            data.trailer,
691
            false,
692
            true
693
        );
694
695
        interchangeLi.append(messagesUl, trailerDiv);
696
        return interchangeLi;
697
    };
698
699
    const buildMessageLevel = (message, index, settings, focusOptions) => {
700
        const messageLi = document.createElement("li");
701
        const messageId = `message_${index}_${Date.now()}`;
702
703
        const shouldFocusMessage =
704
            focusOptions && messageMatchesFocusOptions(message, focusOptions);
705
        const shouldExpand = focusOptions
706
            ? shouldFocusMessage
707
            : settings.expandByDefault;
708
709
        const headerDiv = createSegmentDiv(
710
            "header",
711
            message.header || "UNH",
712
            shouldExpand,
713
            true,
714
            messageId
715
        );
716
        messageLi.appendChild(headerDiv);
717
718
        if (shouldFocusMessage) {
719
            messageLi.setAttribute("data-focus-message", "true");
720
        }
721
722
        const segmentsUl = document.createElement("ul");
723
        segmentsUl.id = messageId;
724
        segmentsUl.className = `collapse${shouldExpand ? " show" : ""}`;
725
726
        const groupedSegments = groupSegmentsByLineId(message.segments);
727
728
        groupedSegments.forEach((group, i) => {
729
            if (group.isLineGroup) {
730
                const lineGroupLi = buildLineGroup(
731
                    group,
732
                    settings,
733
                    `${messageId}_line_${i}`
734
                );
735
                segmentsUl.appendChild(lineGroupLi);
736
            } else {
737
                const relatedGrouping = groupRelatedSegments(group.segments);
738
739
                relatedGrouping.forEach(relatedItem => {
740
                    if (relatedItem.type === "group") {
741
                        const groupLi = document.createElement("li");
742
                        const groupDiv = document.createElement("div");
743
                        groupDiv.className = "segment-group related-segments";
744
                        groupDiv.setAttribute(
745
                            "data-relationship",
746
                            relatedItem.relationship
747
                        );
748
749
                        relatedItem.segments.forEach(segment => {
750
                            const segmentDiv = createSegmentDiv(
751
                                "content",
752
                                segment.raw,
753
                                false,
754
                                true
755
                            );
756
                            if (segment.line_id)
757
                                segmentDiv.dataset.lineId = segment.line_id;
758
                            groupDiv.appendChild(segmentDiv);
759
                        });
760
761
                        groupLi.appendChild(groupDiv);
762
                        segmentsUl.appendChild(groupLi);
763
                    } else {
764
                        const segmentLi = buildSegmentElement(
765
                            relatedItem.segment,
766
                            settings
767
                        );
768
                        segmentsUl.appendChild(segmentLi);
769
                    }
770
                });
771
            }
772
        });
773
774
        const trailerDiv = createSegmentDiv(
775
            "trailer",
776
            message.trailer || "UNT",
777
            false,
778
            true
779
        );
780
781
        messageLi.append(segmentsUl, trailerDiv);
782
        return messageLi;
783
    };
784
785
    const buildLineGroup = (group, settings, lineId) => {
786
        const lineGroupLi = document.createElement("li");
787
        const linSegment = group.segments[0];
788
789
        const headerDiv = createSegmentDiv(
790
            "header",
791
            linSegment.raw,
792
            settings.expandByDefault,
793
            true,
794
            lineId
795
        );
796
        lineGroupLi.appendChild(headerDiv);
797
798
        const lineSegmentsUl = document.createElement("ul");
799
        lineSegmentsUl.id = lineId;
800
        lineSegmentsUl.className = `collapse${settings.expandByDefault ? " show" : ""}`;
801
802
        const lineSegments = group.segments.slice(1);
803
        const groupedSegments = groupRelatedSegments(lineSegments);
804
805
        groupedSegments.forEach(groupItem => {
806
            if (groupItem.type === "group") {
807
                const groupLi = document.createElement("li");
808
                const groupDiv = document.createElement("div");
809
                groupDiv.className = "segment-group related-segments";
810
                groupDiv.setAttribute(
811
                    "data-relationship",
812
                    groupItem.relationship
813
                );
814
815
                groupItem.segments.forEach(segment => {
816
                    const segmentDiv = createSegmentDiv(
817
                        "content",
818
                        segment.raw,
819
                        false,
820
                        true
821
                    );
822
                    if (segment.line_id)
823
                        segmentDiv.dataset.lineId = segment.line_id;
824
                    groupDiv.appendChild(segmentDiv);
825
                });
826
827
                groupLi.appendChild(groupDiv);
828
                lineSegmentsUl.appendChild(groupLi);
829
            } else {
830
                const segmentLi = buildSegmentElement(
831
                    groupItem.segment,
832
                    settings
833
                );
834
                lineSegmentsUl.appendChild(segmentLi);
835
            }
836
        });
837
838
        lineGroupLi.appendChild(lineSegmentsUl);
839
        return lineGroupLi;
840
    };
841
842
    const buildSegmentElement = (segment, settings) => {
843
        const segmentLi = document.createElement("li");
844
        const segmentDiv = createSegmentDiv(
845
            "content",
846
            segment.raw,
847
            false,
848
            true
849
        );
850
851
        if (segment.line_id) {
852
            segmentDiv.dataset.lineId = segment.line_id;
853
        }
854
855
        if (settings.showElementDetails && segment.elements) {
856
            const detailsDiv = document.createElement("div");
857
            detailsDiv.className = "segment-details";
858
            detailsDiv.innerHTML = `<small>Elements: ${JSON.stringify(segment.elements)}</small>`;
859
            segmentDiv.appendChild(detailsDiv);
860
        }
861
862
        segmentLi.appendChild(segmentDiv);
863
        return segmentLi;
864
    };
865
866
    const groupRelatedSegments = segments => {
867
        const grouped = [];
868
        let i = 0;
869
870
        const relationships = {
871
            ALC: ["MOA", "TAX"],
872
            PRI: ["MOA"],
873
            QTY: ["MOA"],
874
            DTM: ["MOA"],
875
            TAX: ["MOA"],
876
            RFF: ["DTM"],
877
        };
878
879
        while (i < segments.length) {
880
            const segment = segments[i];
881
            const relatedTags = relationships[segment.tag];
882
883
            if (relatedTags) {
884
                const relatedSegments = [segment];
885
                let j = i + 1;
886
887
                while (
888
                    j < segments.length &&
889
                    relatedTags.includes(segments[j].tag)
890
                ) {
891
                    relatedSegments.push(segments[j]);
892
                    j++;
893
                }
894
895
                if (relatedSegments.length > 1) {
896
                    grouped.push({
897
                        type: "group",
898
                        relationship: `${segment.tag}_group`,
899
                        segments: relatedSegments,
900
                    });
901
                    i = j;
902
                } else {
903
                    grouped.push({ type: "single", segment });
904
                    i++;
905
                }
906
            } else {
907
                grouped.push({ type: "single", segment });
908
                i++;
909
            }
910
        }
911
912
        return grouped;
913
    };
914
915
    const createSegmentDiv = (
916
        type,
917
        content,
918
        expandByDefault,
919
        boldTag,
920
        targetId
921
    ) => {
922
        const div = document.createElement("div");
923
        div.className = `segment ${type}`;
924
925
        if (type === "header" && targetId) {
926
            div.setAttribute("data-bs-toggle", "collapse");
927
            div.setAttribute("data-bs-target", `#${targetId}`);
928
            div.setAttribute(
929
                "aria-expanded",
930
                expandByDefault ? "true" : "false"
931
            );
932
            div.setAttribute("aria-controls", targetId);
933
            div.style.cursor = "pointer";
934
935
            if (boldTag && content && content.length >= 3) {
936
                const tag = content.substring(0, 3);
937
                const rest = content.substring(3);
938
                div.innerHTML = `<i class="fa fa-chevron-down me-1"></i> <span class="segment-tag">${escapeHtml(tag)}</span>${escapeHtml(rest)}`;
939
            } else {
940
                div.innerHTML = `<i class="fa fa-chevron-down me-1"></i> ${escapeHtml(content)}`;
941
            }
942
        } else if (boldTag && content && content.length >= 3) {
943
            const tag = content.substring(0, 3);
944
            const rest = content.substring(3);
945
            div.innerHTML = `<span class="segment-tag">${escapeHtml(tag)}</span>${escapeHtml(rest)}`;
946
        } else {
947
            div.textContent = content;
948
        }
949
950
        return div;
951
    };
952
953
    const groupSegmentsByLineId = segments => {
954
        const groups = [];
955
        let currentGroup = null;
956
957
        segments.forEach(segment => {
958
            if (segment.tag === "LIN") {
959
                if (currentGroup) groups.push(currentGroup);
960
                currentGroup = {
961
                    isLineGroup: true,
962
                    lineId: segment.line_id,
963
                    segments: [segment],
964
                };
965
            } else if (
966
                currentGroup &&
967
                segment.line_id === currentGroup.lineId
968
            ) {
969
                currentGroup.segments.push(segment);
970
            } else {
971
                if (currentGroup) {
972
                    groups.push(currentGroup);
973
                    currentGroup = null;
974
                }
975
                groups.push({ isLineGroup: false, segments: [segment] });
976
            }
977
        });
978
979
        if (currentGroup) groups.push(currentGroup);
980
        return groups;
981
    };
982
983
    const initializeModal = settings => {
984
        const modal = $(settings.modalId);
985
986
        modal.on("hidden.bs.modal", function () {
987
            const modalContent = $(this).find(".modal-content");
988
            const modalBody = $(this).find(settings.modalBodySelector);
989
990
            // Remove focus highlighting and navbar
991
            modalContent
992
                .find(".edi-focused-message")
993
                .removeClass("edi-focused-message");
994
            modalContent.find(".edi-main-navbar").remove();
995
996
            // Remove scrolling class and reset modal body content
997
            modalBody.removeClass("edi-content");
998
            modalBody.html(`
999
                <div class="edi-loading">
1000
                    <img src="/intranet-tmpl/${window.theme || "prog"}/img/spinner-small.gif" alt="" />
1001
                    Loading
1002
                </div>
1003
            `);
1004
        });
1005
    };
1006
1007
    const escapeHtml = str => {
1008
        if (!str) return "";
1009
        const escapeMap = {
1010
            "&": "&amp;",
1011
            "<": "&lt;",
1012
            ">": "&gt;",
1013
            '"': "&quot;",
1014
            "'": "&#039;",
1015
        };
1016
        return str.replace(/[&<>"']/g, match => escapeMap[match]);
1017
    };
1018
1019
    return {
1020
        init,
1021
        showMessage,
1022
        showMessageFromUrl,
1023
        buildEdiTree,
1024
    };
1025
})();
1026
1027
if (typeof $ !== "undefined") {
1028
    $(document).ready(() => {
1029
        // Individual pages handle initialization
1030
    });
1031
}

Return to bug 40383