Bugzilla – Attachment 184489 Details for
Bug 40383
Modernise the EDIFACT Message display modal
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 40383: Enhanced EDIFACT message modal with search and focus functionality
Bug-40383-Enhanced-EDIFACT-message-modal-with-sear.patch (text/plain), 85.72 KB, created by
Martin Renvoize (ashimema)
on 2025-07-22 16:18:02 UTC
(
hide
)
Description:
Bug 40383: Enhanced EDIFACT message modal with search and focus functionality
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2025-07-22 16:18:02 UTC
Size:
85.72 KB
patch
obsolete
>From 2dc228b267d98828f7b540081c9a6425ec92344d Mon Sep 17 00:00:00 2001 >From: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Date: Tue, 22 Jul 2025 14:17:21 +0100 >Subject: [PATCH] Bug 40383: Enhanced EDIFACT message modal with search and > focus functionality > >This patch completely rebuilds the EDIFACT message modal interface, featuring: > >**Core Features:** >- Modern Bootstrap 5 navbar with integrated search functionality >- Tree view and raw view display modes for message structure >- Real-time search with regex support and result navigation >- Smart focus functionality that highlights relevant segments based on > context (basket, invoice) > >**User Interface:** >- Bootstrap navbar between modal header and body >- Search form with previous/next navigation and live result counts >- Clean separation between always-visible controls and view-specific toolbars >- Accessibility conscious with proper ARIA labels and semantic markup >- Streamlined DOM structure with direct modal-body scrolling > >**Test Plan:** >1. Apply patch and run: yarn css:build to compile SCSS >2. Navigate to Acquisitions > EDIFACT messages >3. Click "View message" on any EDIFACT message >4. **Modal Display Testing:** > - Verify modal opens with Bootstrap navbar between header and body > - Confirm tree view is active by default with segment structure visible > - Click raw view button to verify plain text display > - Test modal responsiveness by resizing window >5. **Search Functionality Testing:** > - Enter "UNH" in search field, verify navbar shows result count > - Use previous/next buttons to navigate between results > - Test search with terms like "ORDER", "NAD", "LIN" - all results should be found > - Verify native browser clear button appears and works > - Test case-insensitive search behavior >6. **Focus Functionality Testing:** > - From basket page, click enhanced view link - should highlight relevant segments > - From invoice page, click enhanced view - should focus on invoice-related segments > - Verify focus highlighting persists during search operations >7. **Error Handling Testing:** > - Test with invalid message ID, verify error display > - Test network errors, confirm loading states work properly >8. **Accessibility Testing:** > - Verify keyboard navigation works with search controls > - Test screen reader compatibility with ARIA labels > - Confirm modal can be closed with Escape key >--- > .../prog/css/src/edifact_interchange.scss | 635 ++++++++++ > .../prog/en/includes/modals/edifact-modal.inc | 38 - > .../en/includes/modals/edifact_errors.inc | 30 + > .../includes/modals/edifact_interchange.inc | 33 + > .../prog/en/modules/acqui/basket.tt | 36 +- > .../prog/en/modules/acqui/edifactmsgs.tt | 61 +- > .../prog/en/modules/acqui/invoice.tt | 14 +- > .../prog/js/modals/edifact-modal.js | 70 -- > .../prog/js/modals/edifact_errors.js | 133 +++ > .../prog/js/modals/edifact_interchange.js | 1031 +++++++++++++++++ > 10 files changed, 1936 insertions(+), 145 deletions(-) > create mode 100644 koha-tmpl/intranet-tmpl/prog/css/src/edifact_interchange.scss > delete mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/modals/edifact-modal.inc > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/modals/edifact_errors.inc > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/modals/edifact_interchange.inc > delete mode 100644 koha-tmpl/intranet-tmpl/prog/js/modals/edifact-modal.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/modals/edifact_errors.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/modals/edifact_interchange.js > >diff --git a/koha-tmpl/intranet-tmpl/prog/css/src/edifact_interchange.scss b/koha-tmpl/intranet-tmpl/prog/css/src/edifact_interchange.scss >new file mode 100644 >index 00000000000..d8fea68ebad >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/css/src/edifact_interchange.scss >@@ -0,0 +1,635 @@ >+/* EDIFACT Interchange Display Styles */ >+ >+// Variables >+$monospace-font: 'Courier New', monospace; >+$base-font-size: 0.9em; >+$line-height: 1.4; >+$border-radius: 3px; >+$border-radius-small: 2px; >+$border-radius-large: 4px; >+$transition-duration: 0.2s; >+$transition-easing: ease; >+ >+// Mixins >+@mixin segment-styling($color-var, $bg-color-var, $border-color-var) { >+ color: var(#{$color-var}) !important; >+ background-color: var(#{$bg-color-var}) !important; >+ border-left: 4px solid var(#{$border-color-var}) !important; >+ padding-left: 8px; >+ >+ &:hover { >+ background-color: var(#{$bg-color-var}) !important; >+ opacity: 0.8; >+ } >+} >+ >+// Enhanced mixin for better contrast using proper Bootstrap color combinations >+@mixin header-styling($theme-color) { >+ @if $theme-color == 'info' { >+ color: var(--bs-info-text-emphasis) !important; >+ background-color: var(--bs-info-bg-subtle) !important; >+ border-left: 4px solid var(--bs-info) !important; >+ border-top: 1px solid var(--bs-info) !important; >+ border-right: 1px solid var(--bs-info) !important; >+ border-bottom: 1px solid var(--bs-info) !important; >+ } @else if $theme-color == 'danger' { >+ color: var(--bs-danger-text-emphasis) !important; >+ background-color: var(--bs-danger-bg-subtle) !important; >+ border-left: 4px solid var(--bs-danger) !important; >+ border-top: 1px solid var(--bs-danger) !important; >+ border-right: 1px solid var(--bs-danger) !important; >+ border-bottom: 1px solid var(--bs-danger) !important; >+ } @else if $theme-color == 'success' { >+ color: var(--bs-success-text-emphasis) !important; >+ background-color: var(--bs-success-bg-subtle) !important; >+ border-left: 4px solid var(--bs-success) !important; >+ border-top: 1px solid var(--bs-success) !important; >+ border-right: 1px solid var(--bs-success) !important; >+ border-bottom: 1px solid var(--bs-success) !important; >+ } @else if $theme-color == 'warning' { >+ color: var(--bs-warning-text-emphasis) !important; >+ background-color: var(--bs-warning-bg-subtle) !important; >+ border-left: 4px solid var(--bs-warning) !important; >+ border-top: 1px solid var(--bs-warning) !important; >+ border-right: 1px solid var(--bs-warning) !important; >+ border-bottom: 1px solid var(--bs-warning) !important; >+ } >+ >+ padding-left: 8px; >+ >+ &:hover { >+ opacity: 0.8; >+ } >+} >+ >+@mixin responsive-size($font-size, $margin-left, $padding-left, $segment-padding) { >+ .edi-tree { >+ font-size: $font-size; >+ >+ ul { >+ margin-left: $margin-left; >+ padding-left: $padding-left; >+ } >+ } >+ >+ .segment { >+ padding: $segment-padding; >+ } >+} >+ >+// Main styles >+.edi-tree { >+ font-family: $monospace-font; >+ font-size: $base-font-size; >+ line-height: $line-height; >+ >+ &.hidden { >+ display: none !important; >+ } >+ >+ ul { >+ margin-left: 1.2rem; >+ border-left: 2px solid #e0e0e0; >+ padding-left: 0.8rem; >+ margin-top: 0.25rem; >+ margin-bottom: 0.25rem; >+ transition: max-height 0.3s ease-in-out; >+ overflow: hidden; >+ >+ &:hover { >+ border-left-color: #007bff; >+ } >+ } >+ >+ // Segment base styles >+ .segment { >+ font-family: $monospace-font; >+ padding: 3px 6px; >+ margin: 1px 0; >+ border-radius: $border-radius; >+ transition: background-color $transition-duration $transition-easing; >+ position: relative; >+ >+ &:hover { >+ background-color: #f8f9fa; >+ } >+ >+ // Segment type variations >+ &.header { >+ @include header-styling('primary'); >+ cursor: pointer; >+ >+ // Bootstrap collapse integration >+ &[data-bs-toggle="collapse"] { >+ .fa-chevron-down { >+ transition: transform $transition-duration $transition-easing; >+ } >+ >+ &[aria-expanded="false"] .fa-chevron-down { >+ transform: rotate(-90deg); >+ } >+ } >+ >+ // Accessibility >+ &[aria-expanded="true"] .chevron::after { >+ content: " (expanded)"; >+ position: absolute; >+ left: -9999px; >+ } >+ >+ &[aria-expanded="false"] .chevron::after { >+ content: " (collapsed)"; >+ position: absolute; >+ left: -9999px; >+ } >+ >+ &:focus { >+ outline: 2px solid #007bff; >+ outline-offset: 2px; >+ } >+ } >+ >+ } >+ >+ // Hierarchical color scheme for different nesting levels >+ // Level 1: UNB (Interchange) - Blue >+ > li > .segment { >+ &.header { >+ @include header-styling('info'); >+ } >+ >+ &.content { >+ @include segment-styling(--bs-info-text-emphasis, --bs-info-bg-subtle, --bs-info-border-subtle); >+ } >+ } >+ >+ > li .segment-group.related-segments .segment.content { >+ @include segment-styling(--bs-info-text-emphasis, --bs-info-bg-subtle, --bs-info-border-subtle); >+ } >+ >+ // Level 2: UNH (Message) - Red >+ > li > ul > li > .segment { >+ &.header { >+ @include header-styling('danger'); >+ } >+ &.content { >+ @include segment-styling(--bs-danger-text-emphasis, --bs-danger-bg-subtle, --bs-danger-border-subtle); >+ } >+ } >+ >+ > li > ul > li .segment-group.related-segments .segment.content { >+ @include segment-styling(--bs-danger-text-emphasis, --bs-danger-bg-subtle, --bs-danger-border-subtle); >+ } >+ >+ // Level 3: LIN (Line groups) - Green >+ > li > ul > li > ul > li > .segment { >+ &.header { >+ @include header-styling('success'); >+ } >+ >+ &.content { >+ @include segment-styling(--bs-success-text-emphasis, --bs-success-bg-subtle, --bs-success-border-subtle); >+ } >+ } >+ >+ > li > ul > li > ul > li .segment-group.related-segments .segment.content { >+ @include segment-styling(--bs-success-text-emphasis, --bs-success-bg-subtle, --bs-success-border-subtle); >+ } >+ >+ // Level 4: Individual segments - Yellow >+ > li > ul > li > ul > li > ul > li > .segment { >+ &.header { >+ @include header-styling('warning'); >+ } >+ >+ &.content { >+ @include segment-styling(--bs-warning-text-emphasis, --bs-warning-bg-subtle, --bs-warning-border-subtle); >+ } >+ } >+ >+ > li > ul > li > ul > li > ul > li .segment-group.related-segments .segment.content { >+ @include segment-styling(--bs-warning-text-emphasis, --bs-warning-bg-subtle, --bs-warning-border-subtle); >+ } >+ >+ // Global trailer styling (for all levels) >+ .segment.trailer { >+ @include segment-styling(--bs-secondary-text-emphasis, --bs-secondary-bg-subtle, --bs-secondary-border-subtle); >+ font-style: italic; >+ } >+ >+ // Bold segment tags >+ .segment-tag { >+ font-weight: bold; >+ } >+ >+ // Related segment grouping >+ .segment-group { >+ position: relative; >+ margin: 2px 0; >+ >+ &.related-segments { >+ background-color: rgba(13, 110, 253, 0.04); // Very subtle blue tint >+ border-radius: $border-radius-small; >+ padding: 2px 0; >+ margin: 3px 0; >+ >+ // Connecting line for related segments >+ &::before { >+ content: ""; >+ position: absolute; >+ left: -8px; >+ top: 0; >+ bottom: 0; >+ width: 2px; >+ background-color: rgba(13, 110, 253, 0.15); >+ border-radius: 1px; >+ } >+ >+ .segment { >+ margin: 1px 0; >+ >+ &:first-child { >+ margin-top: 2px; >+ } >+ >+ &:last-child { >+ margin-bottom: 2px; >+ } >+ } >+ } >+ } >+ >+ // Line group styling >+ li > .segment.header { >+ + ul { >+ border-left-color: #17a2b8; >+ } >+ >+ &:hover + ul { >+ border-left-color: #138496; >+ } >+ } >+ >+ // Collapsed state >+ [data-toggle].collapsed + ul { >+ max-height: 0; >+ margin-top: 0; >+ margin-bottom: 0; >+ } >+ >+ // Theme variants >+ &.compact { >+ font-size: 0.8em; >+ >+ ul { >+ margin-left: 0.8rem; >+ padding-left: 0.4rem; >+ } >+ >+ .segment { >+ padding: 1px 4px; >+ margin: 0; >+ } >+ } >+} >+ >+// Segment details >+.segment-details { >+ margin-top: 0.25rem; >+ padding: 0.25rem; >+ background-color: #f1f3f4; >+ border-radius: $border-radius-small; >+ font-size: 0.8em; >+ color: #5f6368; >+ border-left: 2px solid #dadce0; >+ >+ small { >+ font-family: $monospace-font; >+ } >+} >+ >+// Loading states >+.edi-loading { >+ text-align: center; >+ padding: 2rem; >+ color: #6c757d; >+ >+ img { >+ margin-right: 0.5rem; >+ } >+} >+ >+// Raw view styling >+.edi-raw-view { >+ font-family: $monospace-font; >+ font-size: $base-font-size; >+ line-height: $line-height; >+ white-space: pre-wrap; >+ >+ &.hidden { >+ display: none !important; >+ } >+ >+ .segment-line { >+ margin: 2px 0; >+ padding: 1px 0; >+ >+ .segment-tag { >+ font-weight: bold; >+ } >+ } >+ >+ // Raw view focus highlighting for focused message segments >+ .edi-focused-segment-line { >+ background-color: rgba(13, 110, 253, 0.08); >+ border-left: 3px solid rgba(13, 110, 253, 0.6); >+ padding-left: 8px; >+ border-radius: $border-radius-small; >+ margin: 1px 0; >+ transition: background-color 0.3s ease; >+ >+ &:hover { >+ background-color: rgba(13, 110, 253, 0.12); >+ } >+ } >+} >+ >+// Modal body scrolling for EDIFACT content >+.modal-body.edi-content { >+ max-height: 60vh; >+ overflow-y: auto; >+} >+ >+// Main navbar styling (between modal-header and modal-body) >+.edi-main-navbar { >+ border-radius: 0; >+ margin-bottom: 0; >+ border-left: 0; >+ border-right: 0; >+ border-top: 0; >+ >+ // Prevent form submission on enter >+ form { >+ button[type="submit"] { >+ display: none; >+ } >+ } >+ >+ // View toggle buttons >+ .btn-group .btn { >+ &.active { >+ background-color: var(--bs-primary); >+ border-color: var(--bs-primary); >+ color: white; >+ } >+ } >+} >+ >+// Tree toolbar styling (now inside modal-body) >+.edi-tree-toolbar { >+ // Hide by default (raw view) >+ &.d-none { >+ display: none !important; >+ } >+} >+ >+// Legacy toolbar styling (for backward compatibility) >+.edi-toolbar { >+ display: flex; >+ justify-content: space-between; >+ align-items: center; >+ padding: 10px 0; >+ border-bottom: 1px solid #dee2e6; >+ margin-bottom: 15px; >+ >+ &-left { >+ display: flex; >+ gap: 10px; >+ } >+ >+ &-center { >+ flex: 1; >+ max-width: 400px; >+ margin: 0 20px; >+ } >+ >+ &-right { >+ display: flex; >+ gap: 5px; >+ } >+ >+ .btn { >+ border-radius: $border-radius-large; >+ font-size: 0.85em; >+ padding: 4px 8px; >+ >+ &.active { >+ background-color: #007bff; >+ color: white; >+ } >+ } >+} >+ >+// Search functionality styling >+.edi-search-container { >+ display: flex; >+ align-items: center; >+ gap: 8px; >+ >+ .input-group { >+ flex: 1; >+ min-width: 200px; >+ margin-bottom: 0; >+ } >+ >+ .edi-search-input { >+ border-radius: $border-radius-small; >+ font-size: 0.85em; >+ >+ &:focus { >+ border-color: #007bff; >+ box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); >+ } >+ } >+} >+ >+.edi-search-results { >+ display: flex; >+ align-items: center; >+ gap: 6px; >+ white-space: nowrap; >+ >+ .edi-search-count { >+ font-size: 0.8em; >+ color: #6c757d; >+ min-width: 70px; >+ text-align: right; >+ } >+ >+ .btn-group { >+ .btn { >+ padding: 3px 6px; >+ font-size: 0.75em; >+ >+ &:disabled { >+ opacity: 0.5; >+ cursor: not-allowed; >+ } >+ } >+ } >+} >+ >+// Search highlighting styles >+.edi-search-highlight { >+ background-color: #fff3cd; >+ border: 1px solid #ffeaa7; >+ border-radius: $border-radius-small; >+ padding: 1px 2px; >+ font-weight: bold; >+} >+ >+.edi-search-current { >+ position: relative; >+ background-color: rgba(255, 193, 7, 0.2) !important; >+ >+ &::before { >+ content: ""; >+ position: absolute; >+ top: -3px; >+ left: -3px; >+ right: -3px; >+ bottom: -3px; >+ background-color: rgba(255, 193, 7, 0.1); >+ border: 3px solid #ffc107; >+ border-radius: $border-radius; >+ z-index: -1; >+ animation: pulse-search 2s ease-in-out infinite; >+ } >+ >+ // Make the search highlights within the current result more prominent >+ .edi-search-highlight { >+ background-color: #ffc107 !important; >+ border-color: #ff9800 !important; >+ color: #000 !important; >+ font-weight: bold !important; >+ box-shadow: 0 0 4px rgba(255, 193, 7, 0.5); >+ } >+} >+ >+@keyframes pulse-search { >+ 0% { >+ box-shadow: 0 0 0 0 rgba(255, 193, 7, 0.4); >+ } >+ 70% { >+ box-shadow: 0 0 0 8px rgba(255, 193, 7, 0); >+ } >+ 100% { >+ box-shadow: 0 0 0 0 rgba(255, 193, 7, 0); >+ } >+} >+ >+// Focus message highlighting >+.edi-focused-message { >+ position: relative; >+ >+ &::before { >+ content: ""; >+ position: absolute; >+ top: -6px; >+ left: -6px; >+ right: -6px; >+ bottom: -6px; >+ background-color: rgba(13, 110, 253, 0.15); >+ border: 3px solid #0d6efd; >+ border-radius: $border-radius; >+ z-index: 1; >+ // Initial animation for first 6 seconds, then static highlight >+ animation: pulse-focus 2s ease-in-out 3; >+ } >+ >+ // Ensure the message content is above the highlight >+ > * { >+ position: relative; >+ z-index: 2; >+ } >+ >+ // After animation, maintain a subtle but persistent highlight >+ &::after { >+ content: ""; >+ position: absolute; >+ top: -6px; >+ left: -6px; >+ right: -6px; >+ bottom: -6px; >+ background-color: rgba(13, 110, 253, 0.08); >+ border: 2px solid rgba(13, 110, 253, 0.6); >+ border-radius: $border-radius; >+ z-index: 1; >+ // This will be visible after the animation completes >+ animation: fade-in-persistent 6s ease-in-out forwards; >+ } >+} >+ >+@keyframes pulse-focus { >+ 0% { >+ box-shadow: 0 0 0 0 rgba(13, 110, 253, 0.4); >+ } >+ 70% { >+ box-shadow: 0 0 0 10px rgba(13, 110, 253, 0); >+ } >+ 100% { >+ box-shadow: 0 0 0 0 rgba(13, 110, 253, 0); >+ } >+} >+ >+@keyframes fade-in-persistent { >+ 0% { >+ opacity: 0; >+ } >+ 100% { >+ opacity: 1; >+ } >+} >+ >+// View toggle styling >+.edi-view-toggle .btn { >+ border-radius: $border-radius-large; >+ font-size: 0.85em; >+ padding: 4px 8px; >+ >+ &.active { >+ background-color: #007bff; >+ color: white; >+ } >+} >+ >+// Responsive design >+@media (max-width: 768px) { >+ @include responsive-size(0.8em, 0.8rem, 0.5rem, 2px 4px); >+} >+ >+// Print styles >+@media print { >+ .edi-tree { >+ font-size: 0.7em; >+ >+ .segment { >+ &.header { >+ background-color: transparent !important; >+ border: 1px solid #000; >+ } >+ >+ &.content { >+ background-color: transparent !important; >+ } >+ } >+ } >+ >+ .chevron { >+ display: none; >+ } >+ >+ [data-toggle].collapsed + ul { >+ display: block !important; >+ } >+} >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/edifact-modal.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/edifact-modal.inc >deleted file mode 100644 >index 5be799cff53..00000000000 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/edifact-modal.inc >+++ /dev/null >@@ -1,38 +0,0 @@ >-[%# EDIFACT interchange modal include %] >-[%# This modal can be used on any page that needs to display EDIFACT interchanges %] >- >-<!-- Modal to display EDIFACT interchanges --> >-<div class="modal" id="EDI_modal" tabindex="-1" role="dialog" aria-labelledby="EDI_modal_label" aria-hidden="true"> >- <div class="modal-dialog modal-lg"> >- <div class="modal-content"> >- <div class="modal-header"> >- <h1 class="modal-title" id="EDI_modal_label">EDIFACT Interchange</h1> >- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> >- </div> >- <div class="modal-body"> >- <div id="edi_loading"><img src="[% interface | html %]/[% theme | html %]/img/spinner-small.gif" alt="" /> Loading</div> >- </div> >- <div class="modal-footer"> >- <button class="btn btn-default" data-bs-dismiss="modal">Close</button> >- </div> >- </div> >- </div> >-</div> >- >-<!-- Modal to display EDIFACT errors --> >-<div class="modal" id="EDI_errors_modal" tabindex="-1" role="dialog" aria-labelledby="EDI_errors_modal_label" aria-hidden="true"> >- <div class="modal-dialog modal-lg"> >- <div class="modal-content"> >- <div class="modal-header"> >- <h1 class="modal-title" id="EDI_errors_modal_label">EDIFACT processing errors for <span id="EDI_errors_filename"></span></h1> >- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> >- </div> >- <div class="modal-body"> >- <div id="edi_errors_loading"><img src="[% interface | html %]/[% theme | html %]/img/spinner-small.gif" alt="" /> Loading</div> >- </div> >- <div class="modal-footer"> >- <button class="btn btn-default" data-bs-dismiss="modal">Close</button> >- </div> >- </div> >- </div> >-</div> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/edifact_errors.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/edifact_errors.inc >new file mode 100644 >index 00000000000..139e1bbea1c >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/edifact_errors.inc >@@ -0,0 +1,30 @@ >+[%# EDIFACT errors modal %] >+[%# This modal provides EDIFACT interchange parsing error display %] >+[%# Usage: INCLUDE 'modals/edifact_errors.inc' modal_id='custom_errors_modal' modal_size='lg' %] >+[%# Requires: Asset.js("js/modals/edifact_errors.js") %] >+ >+[%# Set defaults if not provided %] >+[% UNLESS modal_id %][% modal_id = 'EDI_errors_modal' %][% END %] >+[% UNLESS modal_size %][% modal_size = 'lg' %][% END %] >+[% UNLESS modal_title %][% modal_title = 'EDIFACT processing errors for' %][% END %] >+ >+<!-- Modal to display EDIFACT errors --> >+<div class="modal fade" id="[% modal_id | html %]" tabindex="-1" role="dialog" aria-labelledby="[% modal_id | html %]_label" aria-hidden="true"> >+ <div class="modal-dialog modal-[% modal_size | html %]"> >+ <div class="modal-content"> >+ <div class="modal-header"> >+ <h1 class="modal-title" id="[% modal_id | html %]_label"> [% modal_title | html %] <span class="filename"></span> </h1> >+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> >+ </div> >+ <div class="modal-body"> >+ <div class="edi-loading"> >+ <img src="[% interface | html %]/[% theme | html %]/img/spinner-small.gif" alt="" /> >+ [% t("Loading errors...") | html %] >+ </div> >+ </div> >+ <div class="modal-footer"> >+ <button type="button" class="btn btn-primary" data-bs-dismiss="modal">[% t("Close") | html %]</button> >+ </div> >+ </div> >+ </div> >+</div> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/edifact_interchange.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/edifact_interchange.inc >new file mode 100644 >index 00000000000..21a03143cd2 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/edifact_interchange.inc >@@ -0,0 +1,33 @@ >+[%# EDIFACT interchange modal %] >+[%# This modal provides EDIFACT interchange display functionality %] >+[%# Usage: INCLUDE 'modals/edifact_interchange.inc' modal_id='custom_modal' modal_size='xl' %] >+[%# Requires: Asset.js("js/modals/edifact_interchange.js") %] >+ >+[%# Set defaults if not provided %] >+[% UNLESS modal_id %][% modal_id = 'EDI_modal' %][% END %] >+[% UNLESS modal_size %][% modal_size = 'lg' %][% END %] >+[% UNLESS modal_title %][% modal_title = 'EDIFACT interchange' %][% END %] >+[% UNLESS show_actions.defined %][% show_actions = 1 %][% END %] >+ >+<!-- Modal to display EDIFACT interchanges --> >+<div class="modal fade" id="[% modal_id | html %]" tabindex="-1" role="dialog" aria-labelledby="[% modal_id | html %]_label" aria-hidden="true"> >+ <div class="modal-dialog modal-[% modal_size | html %]"> >+ <div class="modal-content"> >+ <div class="modal-header"> >+ <h1 class="modal-title" id="[% modal_id | html %]_label">[% modal_title | html %]</h1> >+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> >+ </div> >+ <div class="modal-body"> >+ <div class="edi-loading"> >+ <img src="[% interface | html %]/[% theme | html %]/img/spinner-small.gif" alt="" /> >+ [% t("Loading EDIFACT interchange...") | html %] >+ </div> >+ </div> >+ [% IF show_actions %] >+ <div class="modal-footer"> >+ <button type="button" class="btn btn-primary" data-bs-dismiss="modal">[% t("Close") | html %]</button> >+ </div> >+ [% END %] >+ </div> >+ </div> >+</div> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/basket.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/basket.tt >index d4cee909473..c9c86136a03 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/basket.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/basket.tt >@@ -141,14 +141,14 @@ > <ul class="dropdown-menu"> > [% FOR message IN quote_messages %] > <li> >- <a class="dropdown-item view_edifact_message" href="#" data-message-id="[% message.id | html %]"> >+ <a class="dropdown-item view_edifact_message" href="#" data-message-id="[% message.id | html %]" data-basketno="[% basketno | html %]" data-basketname="[% basketname | html %]"> > <i class="fa fa-quote-left"></i> View quote ([% message.transfer_date | $KohaDates %]) > </a> > </li> > [% END %] > [% FOR message IN order_messages %] > <li> >- <a class="dropdown-item view_edifact_message" href="#" data-message-id="[% message.id | html %]"> >+ <a class="dropdown-item view_edifact_message" href="#" data-message-id="[% message.id | html %]" data-basketno="[% basketno | html %]" data-basketname="[% basketname | html %]"> > <i class="fa fa-shopping-cart"></i> View order ([% message.transfer_date | $KohaDates %]) > </a> > </li> >@@ -158,12 +158,16 @@ > [% ELSIF quote_messages.size > 0 %] > <!-- Only quote messages exist --> > <div class="btn-group"> >- <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> >+ <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 %]"> >+ <i class="fa fa-quote-left"></i> View EDIFACT quote >+ </button> > </div> > [% ELSIF order_messages.size > 0 %] > <!-- Only order messages exist --> > <div class="btn-group"> >- <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> >+ <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 %]"> >+ <i class="fa fa-shopping-cart"></i> View EDIFACT order >+ </button> > </div> > [% END %] > [% END %] >@@ -323,14 +327,14 @@ > <ul class="dropdown-menu"> > [% FOR message IN quote_messages %] > <li> >- <a class="dropdown-item view_edifact_message" href="#" data-message-id="[% message.id | html %]"> >+ <a class="dropdown-item view_edifact_message" href="#" data-message-id="[% message.id | html %]" data-basketno="[% basketno | html %]" data-basketname="[% basketname | html %]"> > <i class="fa fa-quote-left"></i> View quote ([% message.transfer_date | $KohaDates %]) > </a> > </li> > [% END %] > [% FOR message IN order_messages %] > <li> >- <a class="dropdown-item view_edifact_message" href="#" data-message-id="[% message.id | html %]"> >+ <a class="dropdown-item view_edifact_message" href="#" data-message-id="[% message.id | html %]" data-basketno="[% basketno | html %]" data-basketname="[% basketname | html %]"> > <i class="fa fa-shopping-cart"></i> View order ([% message.transfer_date | $KohaDates %]) > </a> > </li> >@@ -340,12 +344,16 @@ > [% ELSIF quote_messages.size > 0 %] > <!-- Only quote interchanges exist --> > <div class="btn-group"> >- <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> >+ <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 %]"> >+ <i class="fa fa-quote-left"></i> View EDIFACT quote >+ </button> > </div> > [% ELSIF order_messages.size > 0 %] > <!-- Only order messages exist --> > <div class="btn-group"> >- <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> >+ <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 %]"> >+ <i class="fa fa-shopping-cart"></i> View EDIFACT order >+ </button> > </div> > [% END %] > [% END %] >@@ -1249,7 +1257,7 @@ > <!-- /.modal#dateEditor --> > > [% IF edifact_enabled && edifact_messages.size > 0 %] >- [% INCLUDE 'modals/edifact-modal.inc' %] >+ [% INCLUDE 'modals/edifact_interchange.inc' modal_id='EDI_modal' modal_title='EDIFACT Message' modal_size='xl' show_actions=1 %] > [% END %] > > [% MACRO jsinclude BLOCK %] >@@ -1479,7 +1487,15 @@ > [% PROCESS patron_search_js columns => columns, actions => ["add"], preview_on_name_click => 1 %] > > [% IF edifact_enabled && edifact_messages.size > 0 %] >- [% Asset.js("js/modals/edifact-modal.js") | $raw %] >+ [% Asset.css("css/edifact_interchange.css") | $raw %] >+ [% Asset.js("js/modals/edifact_interchange.js") | $raw %] >+ <script> >+ EdifactDisplay.init({ >+ modalId: "#EDI_modal", >+ expandByDefault: true, >+ showElementDetails: false, >+ }); >+ </script> > [% END %] > [% END %] > >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/edifactmsgs.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/edifactmsgs.tt >index eb2ae3a82f4..166e7852ff6 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/edifactmsgs.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/edifactmsgs.tt >@@ -12,6 +12,7 @@ > [% END %]</title > > > [% INCLUDE 'doc-head-close.inc' %] >+[% Asset.css("css/edifact_interchange.css") | $raw %] > </head> > > <body id="acq_edifactmsgs" class="acq"> >@@ -37,17 +38,30 @@ > </div> > <!-- /#acqui_edifactmsgs --> > >- [% INCLUDE 'modals/edifact-modal.inc' %] >+ [% INCLUDE 'modals/edifact_interchange.inc' modal_id='EDI_modal' modal_title='EDIFACT Message' modal_size='xl' show_actions=1 %] >+ [% INCLUDE 'modals/edifact_errors.inc' modal_id='EDI_errors_modal' modal_title='EDIFACT processing errors for' modal_size='lg' %] > [% END %] > > [% MACRO jsinclude BLOCK %] > [% Asset.js("js/acquisitions-menu.js") | $raw %] > [% INCLUDE 'datatables.inc' %] >- [% Asset.js("js/modals/edifact-modal.js") | $raw %] >+ [% Asset.js("js/modals/edifact_interchange.js") | $raw %] >+ [% Asset.js("js/modals/edifact_errors.js") | $raw %] > <script> >- $(document).ready(function () { >- let edi_msgs_table_url = "/api/v1/acquisitions/edifiles?"; >+ var interface = "[% interface | html %]"; >+ var theme = "[% theme | html %]"; >+ $(document).ready(function() { >+ // Initialize enhanced EDIFACT display >+ EdifactDisplay.init({ >+ modalId: '#EDI_modal', >+ expandByDefault: true, >+ showElementDetails: false >+ }); >+ >+ // Get CSRF token once for reuse >+ var csrfToken = $('meta[name="csrf-token"]').attr("content") ? $('meta[name="csrf-token"]').attr("content").trim() : ''; > >+ var edi_msgs_table_url = '/api/v1/acquisitions/edifiles?'; > var edi_msgs_table = $("#edi_msgs").kohaTable( > { > ajax: { >@@ -79,19 +93,11 @@ > render: function (daya, type, row, meta) { > let rendered = ""; > if (row.errors.length > 0) { >- let errorsList = "<ul>"; >- row.errors.forEach(error => { >- if (error.details) { >- errorsList += "<li>"; >- errorsList += "<strong>" + error.details + "</strong>"; >- if (error.section) { >- errorsList += "<br/><pre>" + error.section + "</pre>"; >- } >- errorsList += "</li>"; >- } >- }); >- errorsList += "</ul>"; >- 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>)"; >+ // JSON encode the errors instead of constructing HTML >+ let errorsJson = JSON.stringify(row.errors); >+ // Escape quotes for HTML attribute >+ errorsJson = errorsJson.replace(/"/g, '"'); >+ 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>)"; > } else { > rendered = row.status; > } >@@ -142,15 +148,22 @@ > searchable: false, > orderable: false, > render: function (data, type, row, meta) { >- var result = >- '<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> "; >- result += '<form action="/cgi-bin/koha/acqui/edifactmsgs.pl" method="post"><input type="hidden" name="message_id" value="' + encodeURIComponent(row.id) + '" />'; >- result += '<input type="hidden" name="csrf_token" value="' + $('meta[name="csrf-token"]').attr("content").trim() + '" />'; >- 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> "; >+ var result = '<div class="btn-group" role="group">'; >+ >+ // Enhanced view button >+ 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>"; >+ >+ // Delete button >+ result += '<form action="/cgi-bin/koha/acqui/edifactmsgs.pl" method="post" style="display:inline;"><input type="hidden" name="message_id" value="' + encodeURIComponent(row.id) + '" />'; >+ result += '<input type="hidden" name="csrf_token" value="' + csrfToken + '" />'; >+ 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>"; >+ >+ // Import button > if (row.status == "new") { >- result += >- '<a class="btn btn-default btn-xs import_msg" href="/cgi-bin/koha/acqui/edifactmsgs.pl?op=import&message_id=' + encodeURIComponent(row.id) + '"><i class="fa fa-cog"></i> ' + _("Import") + "</a> "; >+ result += '<a class="btn btn-default btn-xs import_msg" href="/cgi-bin/koha/acqui/edifactmsgs.pl?op=import&message_id=' + encodeURIComponent(row.id) + '"><i class="fa fa-cog"></i> ' + _("Import") + "</a>"; > } >+ >+ result += '</div>'; > return result; > }, > }, >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/invoice.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/invoice.tt >index 9997f998e2f..5da08e6be4a 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/invoice.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/invoice.tt >@@ -67,7 +67,7 @@ > [% END %] > </p> > <p> >- <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> >+ <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> > </p> > > [% IF edifact_errors.size > 0 %] >@@ -555,7 +555,7 @@ > </div> > <!-- /#updateFund.modal --> > >-[% INCLUDE 'modals/edifact-modal.inc' %] >+[% INCLUDE 'modals/edifact_interchange.inc' modal_id='EDI_modal' modal_title='EDIFACT Message' modal_size='xl' show_actions=1 %] > > <div style="display:none;"> > <select id="all_fund_dropdown"> >@@ -578,7 +578,15 @@ > [% Asset.js("js/additional-fields-entry.js") | $raw %] > [% INCLUDE 'calendar.inc' %] > [% INCLUDE 'datatables.inc' %] >- [% Asset.js("js/modals/edifact-modal.js") | $raw %] >+ [% Asset.css("css/edifact_interchange.css") | $raw %] >+ [% Asset.js("js/modals/edifact_interchange.js") | $raw %] >+ <script> >+ EdifactDisplay.init({ >+ modalId: "#EDI_modal", >+ expandByDefault: true, >+ showElementDetails: false, >+ }); >+ </script> > <script> > function updateColumnsVisibility(visible) { > if ( visible ) { >diff --git a/koha-tmpl/intranet-tmpl/prog/js/modals/edifact-modal.js b/koha-tmpl/intranet-tmpl/prog/js/modals/edifact-modal.js >deleted file mode 100644 >index 1e4c22a73e6..00000000000 >--- a/koha-tmpl/intranet-tmpl/prog/js/modals/edifact-modal.js >+++ /dev/null >@@ -1,70 +0,0 @@ >-// EDIFACT modal JavaScript functionality >-// This script handles the EDIFACT message modal interactions >-// Include this after the edifact-modal.inc template >- >-/* global __ interface theme */ >-$(document).ready(function () { >- // Initialize modal elements >- var EDIModal = $("#EDI_modal"); >- var EDIModalBody = $("#EDI_modal .modal-body"); >- var EDIErrorsModal = $("#EDI_errors_modal"); >- >- // Handle view EDIFACT message button clicks >- $("body").on("click", ".view_edifact_message", function (e) { >- e.preventDefault(); >- var message_id = $(this).data("message-id"); >- var page = >- "/cgi-bin/koha/acqui/edimsg.pl?id=" + >- encodeURIComponent(message_id); >- EDIModalBody.load(page + " #edimsg"); >- EDIModal.modal("show"); >- }); >- >- // Handle view_message button clicks (for compatibility with existing code) >- $("body").on("click", ".view_message", function (e) { >- e.preventDefault(); >- var page = $(this).attr("href"); >- EDIModalBody.load(page + " #edimsg"); >- EDIModal.modal("show"); >- }); >- >- // Handle modal close button >- EDIModal.on("click", ".btn-close", function (e) { >- e.preventDefault(); >- EDIModal.modal("hide"); >- }); >- >- // Reset modal content when hidden >- EDIModal.on("hidden.bs.modal", function () { >- EDIModalBody.html( >- "<div id='edi_loading'><img style='display:inline-block' src='" + >- interface + >- "/" + >- theme + >- "/img/spinner-small.gif' alt='' /> Loading</div>" >- ); >- }); >- >- // Handle EDIFACT errors modal >- const errorsModal = document.getElementById("EDI_errors_modal"); >- if (errorsModal) { >- errorsModal.addEventListener("show.bs.modal", function (event) { >- // Link that triggered the modal >- const link = event.relatedTarget; >- >- // Extract info from data-bs-* attributes >- const filename = link.getAttribute("data-bs-filename"); >- const errors = link.getAttribute("data-bs-errors"); >- >- // Update the modal's title >- const modalTitleSpan = errorsModal.querySelector( >- ".modal-title #EDI_errors_filename" >- ); >- modalTitleSpan.textContent = filename; >- >- // Update the modal's content >- const modalBody = errorsModal.querySelector(".modal-body"); >- modalBody.innerHTML = errors; >- }); >- } >-}); >diff --git a/koha-tmpl/intranet-tmpl/prog/js/modals/edifact_errors.js b/koha-tmpl/intranet-tmpl/prog/js/modals/edifact_errors.js >new file mode 100644 >index 00000000000..b2ad30921fe >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/modals/edifact_errors.js >@@ -0,0 +1,133 @@ >+/** >+ * EDIFACT Errors Display Module >+ */ >+const EdifactErrorsDisplay = (() => { >+ "use strict"; >+ >+ const defaults = { >+ modalId: "#EDI_errors_modal", >+ modalBodySelector: ".modal-body", >+ filenameSelectorInTitle: ".filename", >+ }; >+ >+ const init = (options = {}) => { >+ const settings = { ...defaults, ...options }; >+ initializeModal(settings); >+ }; >+ >+ const initializeModal = settings => { >+ const modal = $(settings.modalId); >+ if (!modal.length) return; >+ >+ modal.on("show.bs.modal", event => { >+ const button = event.relatedTarget; >+ if (!button) return; >+ >+ const filename = >+ button.getAttribute("data-bs-filename") || >+ button.getAttribute("data-filename"); >+ const errors = >+ button.getAttribute("data-bs-errors") || >+ button.getAttribute("data-errors"); >+ >+ updateFilename(modal, settings, filename); >+ updateErrorsContent(modal, settings, errors); >+ }); >+ >+ modal.on("hidden.bs.modal", () => { >+ resetModalContent(modal, settings); >+ }); >+ }; >+ >+ const updateFilename = (modal, settings, filename) => { >+ const filenameSpan = modal.find(settings.filenameSelectorInTitle); >+ if (filenameSpan.length && filename) { >+ filenameSpan.text(filename); >+ } >+ }; >+ >+ const updateErrorsContent = (modal, settings, errors) => { >+ const modalBody = modal.find(settings.modalBodySelector); >+ if (!modalBody.length) return; >+ >+ if (errors) { >+ try { >+ const errorsData = JSON.parse(errors.replace(/"/g, '"')); >+ modalBody.html(buildErrorsDisplay(errorsData)); >+ } catch (e) { >+ console.error("Error parsing errors JSON:", e); >+ modalBody.html( >+ `<div class="alert alert-danger">Error loading error details: ${escapeHtml(e.message)}</div>` >+ ); >+ } >+ } else { >+ modalBody.html( >+ '<div class="alert alert-info">No errors found for this interchange.</div>' >+ ); >+ } >+ }; >+ >+ const buildErrorsDisplay = errorsData => { >+ if (!errorsData?.length) { >+ return '<div class="alert alert-info">No errors found for this interchange.</div>'; >+ } >+ >+ const errorItems = errorsData >+ .map( >+ error => ` >+ <li class="list-group-item list-group-item-danger"> >+ <strong>Error:</strong> ${escapeHtml(error.details || "Unknown error")} >+ ${ >+ error.section >+ ? ` >+ <br><small><strong>Section:</strong></small> >+ <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> >+ ` >+ : "" >+ } >+ ${ >+ error.date >+ ? ` >+ <br><small class="text-muted"><strong>Date:</strong> ${escapeHtml(error.date)}</small> >+ ` >+ : "" >+ } >+ </li> >+ ` >+ ) >+ .join(""); >+ >+ return `<ul class="list-group">${errorItems}</ul>`; >+ }; >+ >+ const resetModalContent = (modal, settings) => { >+ const modalBody = modal.find(settings.modalBodySelector); >+ const filenameSpan = modal.find(settings.filenameSelectorInTitle); >+ >+ modalBody.html(` >+ <div class="edi-loading"> >+ <img src="/intranet-tmpl/${window.theme || "prog"}/img/spinner-small.gif" alt="" /> >+ Loading errors... >+ </div> >+ `); >+ filenameSpan.text(""); >+ }; >+ >+ const escapeHtml = str => { >+ if (!str) return ""; >+ const escapeMap = { >+ "&": "&", >+ "<": "<", >+ ">": ">", >+ '"': """, >+ "'": "'", >+ }; >+ return str.replace(/[&<>"']/g, match => escapeMap[match]); >+ }; >+ >+ return { init }; >+})(); >+ >+$(document).ready(() => { >+ EdifactErrorsDisplay.init(); >+}); >diff --git a/koha-tmpl/intranet-tmpl/prog/js/modals/edifact_interchange.js b/koha-tmpl/intranet-tmpl/prog/js/modals/edifact_interchange.js >new file mode 100644 >index 00000000000..9305afa25d2 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/modals/edifact_interchange.js >@@ -0,0 +1,1031 @@ >+/** >+ * EDIFACT Display Module >+ */ >+const EdifactDisplay = (() => { >+ "use strict"; >+ >+ const defaults = { >+ modalId: "#EDI_modal", >+ modalBodySelector: ".modal-body", >+ jsonEndpoint: "/cgi-bin/koha/acqui/edimsg.pl", >+ expandByDefault: true, >+ }; >+ >+ const extractFocusOptions = element => { >+ const options = {}; >+ const basketno = element.dataset.basketno; >+ const basketname = element.dataset.basketname; >+ const invoicenumber = element.dataset.invoicenumber; >+ >+ if (basketno) options.basketno = basketno; >+ if (basketname) options.basketname = basketname; >+ if (invoicenumber) options.invoicenumber = invoicenumber; >+ >+ return Object.keys(options).length > 0 ? options : null; >+ }; >+ >+ const init = (options = {}) => { >+ const settings = { ...defaults, ...options }; >+ >+ document.body.addEventListener("click", e => { >+ if (e.target.closest(".view_edifact_message")) { >+ e.preventDefault(); >+ const button = e.target.closest(".view_edifact_message"); >+ const messageId = button.dataset.messageId; >+ const customUrl = button.dataset.url; >+ const focusOptions = extractFocusOptions(button); >+ >+ if (messageId) { >+ showMessage(messageId, settings, focusOptions); >+ } else if (customUrl) { >+ showMessageFromUrl(customUrl, settings, focusOptions); >+ } >+ } >+ >+ if (e.target.closest(".view_message_enhanced")) { >+ e.preventDefault(); >+ const link = e.target.closest(".view_message_enhanced"); >+ const href = link.getAttribute("href"); >+ const focusOptions = extractFocusOptions(link); >+ >+ showMessageFromUrl(href, settings, focusOptions); >+ } >+ }); >+ >+ initializeModal(settings); >+ }; >+ >+ const showMessage = (messageId, settings, focusOptions) => { >+ const url = `${settings.jsonEndpoint}?id=${encodeURIComponent(messageId)}&format=json`; >+ showMessageFromUrl(url, settings, focusOptions); >+ }; >+ >+ const showMessageFromUrl = (url, settings, focusOptions) => { >+ const modal = $(settings.modalId); >+ const modalContent = modal.find(".modal-content"); >+ const modalBody = modal.find(settings.modalBodySelector); >+ const loadingHtml = `<div class="edi-loading"> >+ <img src="/intranet-tmpl/${window.theme || "prog"}/img/spinner-small.gif" alt="" /> >+ Loading >+ </div>`; >+ >+ // Remove any existing EDIFACT navbars >+ modalContent.find(".edi-main-navbar, .edi-tree-toolbar").remove(); >+ >+ modalBody.html(loadingHtml); >+ modal.modal("show"); >+ >+ const jsonUrl = url + (url.includes("?") ? "&" : "?") + "format=json"; >+ >+ $.ajax({ >+ url: jsonUrl, >+ type: "GET", >+ dataType: "json", >+ success: data => { >+ // Build components separately >+ const mainNavbar = createMainNavbar(); >+ const treeToolbar = createTreeToolbar(); >+ const treeView = buildEdiTree(data, settings, focusOptions); >+ const rawView = buildEdiRaw(data, settings, focusOptions); >+ rawView.classList.add("hidden"); >+ >+ // Add class for identification >+ mainNavbar.classList.add("edi-main-navbar"); >+ >+ // Insert navbar between header and body >+ modalBody.before(mainNavbar); >+ >+ // Insert tree toolbar between navbar and body >+ modalBody.before(treeToolbar); >+ >+ // Insert views directly into modal body >+ modalBody.empty(); >+ modalBody.addClass("edi-content"); >+ modalBody.append(treeView, rawView); >+ >+ // Re-run initialization since DOM structure changed >+ setTimeout(() => { >+ initializeViewToggle(modalContent[0], data); >+ initializeExpandCollapse(modalContent[0]); >+ initializeSearch(modalContent[0]); >+ >+ if (focusOptions) { >+ applyFocusOptions(modalContent[0], focusOptions); >+ } >+ }, 0); >+ }, >+ error: () => { >+ modalBody.html( >+ '<div class="alert alert-danger">Failed to load message</div>' >+ ); >+ }, >+ }); >+ }; >+ >+ const createMainNavbar = () => { >+ const nav = document.createElement("nav"); >+ nav.className = "navbar navbar-light bg-light border-bottom py-2"; >+ nav.innerHTML = ` >+ <div class="container-fluid align-items-center"> >+ <!-- Left: View toggle buttons --> >+ <div class="btn-group me-2" role="group" aria-label="Display style"> >+ <button type="button" class="btn btn-outline-secondary active" data-view="tree" title="Tree view"> >+ <i class="fa fa-sitemap fa-fw"></i> >+ </button> >+ <button type="button" class="btn btn-outline-secondary" data-view="raw" title="Raw view"> >+ <i class="fa fa-file-text fa-fw"></i> >+ </button> >+ </div> >+ >+ <!-- Center: Search form --> >+ <form class="d-flex flex-grow-1 mx-2 edi-search-form" role="search"> >+ <input class="form-control me-2 edi-search-input" type="search" placeholder="Search segments..." aria-label="Search segments"> >+ <button class="btn btn-outline-secondary me-1 edi-search-prev" type="button" title="Previous result" disabled> >+ <i class="fa fa-chevron-up"></i> >+ </button> >+ <button class="btn btn-outline-secondary edi-search-next" type="button" title="Next result" disabled> >+ <i class="fa fa-chevron-down"></i> >+ </button> >+ </form> >+ >+ <!-- Right: Search results --> >+ <div class="text-muted small text-nowrap"> >+ <span class="edi-search-count">0 results</span> >+ </div> >+ </div>`; >+ return nav; >+ }; >+ >+ const createTreeToolbar = () => { >+ const toolbar = document.createElement("div"); >+ toolbar.className = >+ "navbar navbar-light bg-light border-bottom py-2 edi-tree-toolbar"; >+ toolbar.innerHTML = ` >+ <div class="container-fluid d-flex flex-row-reverse"> >+ <div class="btn-group btn-group-sm" role="group" aria-label="Tree controls"> >+ <button type="button" class="btn btn-outline-secondary expand-all-btn" title="Expand all sections"> >+ <i class="fa fa-expand me-1"></i>Expand all >+ </button> >+ <button type="button" class="btn btn-outline-secondary collapse-all-btn" title="Collapse all sections"> >+ <i class="fa fa-compress me-1"></i>Collapse all >+ </button> >+ </div> >+ </div>`; >+ return toolbar; >+ }; >+ >+ const buildEdiRaw = (data, settings, focusOptions) => { >+ const rawDiv = document.createElement("div"); >+ rawDiv.className = "edi-raw-view"; >+ >+ const lines = []; >+ >+ if (data.header) { >+ lines.push( >+ `<div class="segment-line"><span class="segment-tag">UNB</span>${data.header.substring(3)}</div>` >+ ); >+ } >+ >+ data.messages.forEach((message, messageIndex) => { >+ const isMessageFocused = >+ focusOptions && >+ messageMatchesFocusOptions(message, focusOptions); >+ const focusClass = isMessageFocused >+ ? " edi-focused-segment-line" >+ : ""; >+ const messageAttr = ` data-message-index="${messageIndex}"`; >+ >+ if (message.header) { >+ lines.push( >+ `<div class="segment-line${focusClass}"${messageAttr}><span class="segment-tag">UNH</span>${message.header.substring(3)}</div>` >+ ); >+ } >+ >+ message.segments.forEach(segment => { >+ const tag = segment.tag || ""; >+ const content = (segment.raw || "").substring(tag.length); >+ lines.push( >+ `<div class="segment-line${focusClass}"${messageAttr}><span class="segment-tag">${escapeHtml(tag)}</span>${escapeHtml(content)}</div>` >+ ); >+ }); >+ >+ if (message.trailer) { >+ lines.push( >+ `<div class="segment-line${focusClass}"${messageAttr}><span class="segment-tag">UNT</span>${message.trailer.substring(3)}</div>` >+ ); >+ } >+ }); >+ >+ if (data.trailer) { >+ lines.push( >+ `<div class="segment-line"><span class="segment-tag">UNZ</span>${data.trailer.substring(3)}</div>` >+ ); >+ } >+ >+ rawDiv.innerHTML = lines.join(""); >+ return rawDiv; >+ }; >+ >+ const initializeViewToggle = (container, data) => { >+ const toggleButtons = container.querySelectorAll("button[data-view]"); >+ const treeView = container.querySelector(".edi-tree"); >+ const rawView = container.querySelector(".edi-raw-view"); >+ const treeToolbar = container.querySelector(".edi-tree-toolbar"); >+ >+ container.edifactData = data; >+ >+ toggleButtons.forEach(button => { >+ button.addEventListener("click", () => { >+ const viewType = button.dataset.view; >+ >+ // Update button states >+ toggleButtons.forEach(btn => btn.classList.remove("active")); >+ button.classList.add("active"); >+ >+ // Toggle views and tree toolbar >+ if (viewType === "tree") { >+ treeView.classList.remove("hidden"); >+ rawView.classList.add("hidden"); >+ treeToolbar.classList.remove("d-none"); >+ } else { >+ treeView.classList.add("hidden"); >+ rawView.classList.remove("hidden"); >+ treeToolbar.classList.add("d-none"); >+ } >+ >+ // Re-execute search if active >+ if (container.searchFunctions) { >+ const searchInput = >+ container.querySelector(".edi-search-input"); >+ if (searchInput?.value.trim()) { >+ container.searchFunctions.performSearch( >+ searchInput.value.trim() >+ ); >+ } >+ } >+ >+ // Re-apply focus highlighting >+ if (container.focusOptions) { >+ setTimeout(() => { >+ const focusTarget = >+ viewType === "tree" >+ ? container.querySelector( >+ '[data-focus-message="true"].edi-focused-message' >+ ) >+ : rawView.querySelector( >+ ".edi-focused-segment-line" >+ ); >+ >+ focusTarget?.scrollIntoView({ >+ behavior: "smooth", >+ block: "start", >+ }); >+ }, 100); >+ } >+ }); >+ }); >+ }; >+ >+ const initializeExpandCollapse = container => { >+ const treeView = container.querySelector(".edi-tree"); >+ >+ container >+ .querySelector(".expand-all-btn") >+ ?.addEventListener("click", () => { >+ const collapseElements = treeView.querySelectorAll(".collapse"); >+ collapseElements.forEach(el => { >+ el.classList.add("show"); >+ if (typeof bootstrap !== "undefined") { >+ bootstrap.Collapse.getOrCreateInstance(el)?.show(); >+ } >+ }); >+ }); >+ >+ container >+ .querySelector(".collapse-all-btn") >+ ?.addEventListener("click", () => { >+ const collapseElements = treeView.querySelectorAll(".collapse"); >+ collapseElements.forEach(el => { >+ if (typeof bootstrap !== "undefined") { >+ bootstrap.Collapse.getOrCreateInstance(el).hide(); >+ } else { >+ el.classList.remove("show"); >+ } >+ }); >+ }); >+ }; >+ >+ const initializeSearch = container => { >+ const searchInput = container.querySelector(".edi-search-input"); >+ const searchCount = container.querySelector(".edi-search-count"); >+ const searchPrev = container.querySelector(".edi-search-prev"); >+ const searchNext = container.querySelector(".edi-search-next"); >+ const treeView = container.querySelector(".edi-tree"); >+ const rawView = container.querySelector(".edi-raw-view"); >+ >+ let currentResults = []; >+ let currentIndex = -1; >+ let searchTimeout = null; >+ >+ const performSearch = query => { >+ clearSearch(false); >+ >+ if (!query || query.length < 2) { >+ updateSearchUI(0, -1); >+ return; >+ } >+ >+ const activeView = container.querySelector(".edi-tree:not(.hidden)") >+ ? "tree" >+ : "raw"; >+ const searchTargets = >+ activeView === "tree" >+ ? treeView.querySelectorAll(".segment") >+ : rawView.querySelectorAll(".segment-line"); >+ >+ const regex = new RegExp(escapeRegExp(query), "i"); >+ >+ searchTargets.forEach(element => { >+ const textContent = element.textContent || element.innerText; >+ if (regex.test(textContent)) { >+ highlightMatches(element, query); >+ currentResults.push({ element, view: activeView }); >+ } >+ }); >+ >+ if (activeView === "tree") { >+ smartExpandCollapseForSearch(currentResults); >+ } >+ >+ updateSearchUI(currentResults.length, currentIndex); >+ >+ if (currentResults.length > 0) { >+ navigateToResult(0); >+ } >+ }; >+ >+ const highlightMatches = (element, query) => { >+ const regex = new RegExp(`(${escapeRegExp(query)})`, "gi"); >+ const walker = document.createTreeWalker( >+ element, >+ NodeFilter.SHOW_TEXT >+ ); >+ >+ const textNodes = []; >+ let node; >+ while ((node = walker.nextNode())) { >+ textNodes.push(node); >+ } >+ >+ textNodes.forEach(textNode => { >+ if (regex.test(textNode.textContent)) { >+ const highlightedHTML = textNode.textContent.replace( >+ regex, >+ '<mark class="edi-search-highlight">$1</mark>' >+ ); >+ const wrapper = document.createElement("span"); >+ wrapper.innerHTML = highlightedHTML; >+ textNode.parentNode.replaceChild(wrapper, textNode); >+ } >+ }); >+ }; >+ >+ const smartExpandCollapseForSearch = results => { >+ setTimeout(() => { >+ const allCollapseElements = >+ treeView.querySelectorAll(".collapse"); >+ const elementsWithResults = new Set(); >+ >+ results.forEach(result => { >+ let current = result.element; >+ while (current && current !== treeView) { >+ if (current.classList?.contains("collapse")) { >+ elementsWithResults.add(current); >+ } >+ current = current.parentElement; >+ } >+ }); >+ >+ allCollapseElements.forEach(collapseElement => { >+ const collapse = >+ typeof bootstrap !== "undefined" >+ ? bootstrap.Collapse.getOrCreateInstance( >+ collapseElement, >+ { toggle: false } >+ ) >+ : null; >+ >+ if (elementsWithResults.has(collapseElement)) { >+ collapse >+ ? collapse.show() >+ : collapseElement.classList.add("show"); >+ } else { >+ collapse >+ ? collapse.hide() >+ : collapseElement.classList.remove("show"); >+ } >+ }); >+ }, 50); >+ }; >+ >+ const navigateResults = direction => { >+ if (currentResults.length === 0) return; >+ >+ let newIndex = currentIndex + direction; >+ if (newIndex >= currentResults.length) newIndex = 0; >+ if (newIndex < 0) newIndex = currentResults.length - 1; >+ >+ navigateToResult(newIndex); >+ }; >+ >+ const navigateToResult = index => { >+ if (index < 0 || index >= currentResults.length) return; >+ >+ currentResults[currentIndex]?.element.classList.remove( >+ "edi-search-current" >+ ); >+ >+ currentIndex = index; >+ const result = currentResults[currentIndex]; >+ >+ result.element.classList.add("edi-search-current"); >+ result.element.scrollIntoView({ >+ behavior: "smooth", >+ block: "center", >+ }); >+ >+ updateSearchUI(currentResults.length, currentIndex); >+ }; >+ >+ const clearSearch = (clearInput = true) => { >+ if (clearInput && searchInput) { >+ searchInput.value = ""; >+ } >+ >+ container >+ .querySelectorAll(".edi-search-highlight, .edi-search-current") >+ .forEach(el => { >+ if (el.classList.contains("edi-search-highlight")) { >+ el.parentNode.replaceChild( >+ document.createTextNode(el.textContent), >+ el >+ ); >+ } else { >+ el.classList.remove("edi-search-current"); >+ } >+ }); >+ >+ container.querySelectorAll("span:not([class])").forEach(wrapper => { >+ if ( >+ wrapper.children.length === 0 || >+ (wrapper.children.length === 1 && >+ wrapper.children[0].tagName === "MARK") >+ ) { >+ wrapper.parentNode.replaceChild( >+ document.createTextNode(wrapper.textContent), >+ wrapper >+ ); >+ } >+ }); >+ >+ currentResults = []; >+ currentIndex = -1; >+ updateSearchUI(0, -1); >+ }; >+ >+ const updateSearchUI = (totalResults, currentIdx) => { >+ if (searchCount) { >+ searchCount.textContent = >+ totalResults === 0 >+ ? "0 results" >+ : `${currentIdx + 1} of ${totalResults}`; >+ } >+ >+ const hasResults = totalResults > 0; >+ if (searchPrev) searchPrev.disabled = !hasResults; >+ if (searchNext) searchNext.disabled = !hasResults; >+ }; >+ >+ const escapeRegExp = string => >+ string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); >+ >+ // Event listeners >+ searchInput?.addEventListener("input", () => { >+ clearTimeout(searchTimeout); >+ const value = searchInput.value.trim(); >+ >+ // If field was cleared (native clear button or manual), clear search >+ if (value === "") { >+ clearSearch(false); // Don't clear input since it's already empty >+ } else { >+ searchTimeout = setTimeout(() => performSearch(value), 500); >+ } >+ }); >+ >+ searchInput?.addEventListener("keydown", e => { >+ if (e.key === "Enter") { >+ e.preventDefault(); >+ navigateResults(e.shiftKey ? -1 : 1); >+ } >+ }); >+ >+ // Prevent form submission >+ const searchForm = container.querySelector(".edi-search-form"); >+ searchForm?.addEventListener("submit", e => e.preventDefault()); >+ >+ searchPrev?.addEventListener("click", () => navigateResults(-1)); >+ searchNext?.addEventListener("click", () => navigateResults(1)); >+ >+ container.searchFunctions = { >+ performSearch, >+ clearSearch, >+ navigateResults, >+ }; >+ }; >+ >+ const messageMatchesFocusOptions = (message, focusOptions) => { >+ if (!message.segments || !focusOptions) return false; >+ >+ // Check if basketno is a whole number and pad it >+ if (/^\d+$/.test(String(focusOptions.basketno))) { >+ const strVal = String(focusOptions.basketno); >+ if (11 > strVal.length) { >+ focusOptions.basketno = strVal.padStart(11, "0"); >+ } >+ } >+ >+ return message.segments.some(segment => { >+ // Check basketno/invoicenumber in BGM segments >+ if (segment.tag === "BGM" && segment.elements?.length > 1) { >+ const bgmValue = String(segment.elements[1]); >+ >+ if ( >+ (focusOptions.basketno && >+ bgmValue === String(focusOptions.basketno)) || >+ (focusOptions.invoicenumber && >+ bgmValue === String(focusOptions.invoicenumber)) >+ ) { >+ return true; >+ } >+ } >+ >+ // Check basketname/invoicenumber in RFF segments >+ if (segment.tag === "RFF" && segment.elements?.length >= 2) { >+ const refType = segment.elements[0]; >+ const refValue = String(segment.elements[1]); >+ >+ if ( >+ focusOptions.basketname && >+ refType === "ON" && >+ refValue === String(focusOptions.basketname) >+ ) { >+ return true; >+ } >+ >+ if ( >+ focusOptions.invoicenumber && >+ (refType === "IV" || refType === "VN") && >+ refValue === String(focusOptions.invoicenumber) >+ ) { >+ return true; >+ } >+ } >+ >+ return false; >+ }); >+ }; >+ >+ const applyFocusOptions = (container, focusOptions) => { >+ container.focusOptions = focusOptions; >+ >+ setTimeout(() => { >+ const focusedMessage = container.querySelector( >+ '[data-focus-message="true"]' >+ ); >+ if (focusedMessage) { >+ focusedMessage.classList.add("edi-focused-message"); >+ >+ const treeView = container.querySelector(".edi-tree"); >+ if (treeView && !treeView.classList.contains("hidden")) { >+ focusedMessage.scrollIntoView({ >+ behavior: "smooth", >+ block: "start", >+ }); >+ } >+ >+ container.setAttribute("data-has-focused-message", "true"); >+ } >+ >+ const rawView = container.querySelector(".edi-raw-view"); >+ if (rawView && !rawView.classList.contains("hidden")) { >+ const firstFocusedLine = rawView.querySelector( >+ ".edi-focused-segment-line" >+ ); >+ firstFocusedLine?.scrollIntoView({ >+ behavior: "smooth", >+ block: "start", >+ }); >+ } >+ }, 200); >+ }; >+ >+ const buildEdiTree = (data, settings, focusOptions) => { >+ const rootUl = document.createElement("ul"); >+ rootUl.className = "edi-tree list-unstyled"; >+ >+ const hasMatchingMessage = >+ focusOptions && >+ data.messages && >+ data.messages.some(msg => >+ messageMatchesFocusOptions(msg, focusOptions) >+ ); >+ >+ const effectiveFocusOptions = >+ focusOptions && !hasMatchingMessage ? null : focusOptions; >+ const effectiveSettings = >+ focusOptions && !hasMatchingMessage >+ ? { ...settings, expandByDefault: true } >+ : settings; >+ >+ const interchangeLi = buildInterchangeLevel( >+ data, >+ effectiveSettings, >+ effectiveFocusOptions >+ ); >+ rootUl.appendChild(interchangeLi); >+ >+ return rootUl; >+ }; >+ >+ const buildInterchangeLevel = (data, settings, focusOptions) => { >+ const interchangeLi = document.createElement("li"); >+ const interchangeId = `interchange_${Date.now()}`; >+ const shouldExpand = focusOptions ? true : settings.expandByDefault; >+ >+ const headerDiv = createSegmentDiv( >+ "header", >+ data.header, >+ shouldExpand, >+ true, >+ interchangeId >+ ); >+ interchangeLi.appendChild(headerDiv); >+ >+ const messagesUl = document.createElement("ul"); >+ messagesUl.id = interchangeId; >+ messagesUl.className = `collapse${shouldExpand ? " show" : ""}`; >+ >+ data.messages.forEach((message, i) => { >+ const messageLi = buildMessageLevel( >+ message, >+ i, >+ settings, >+ focusOptions >+ ); >+ messagesUl.appendChild(messageLi); >+ }); >+ >+ const trailerDiv = createSegmentDiv( >+ "trailer", >+ data.trailer, >+ false, >+ true >+ ); >+ >+ interchangeLi.append(messagesUl, trailerDiv); >+ return interchangeLi; >+ }; >+ >+ const buildMessageLevel = (message, index, settings, focusOptions) => { >+ const messageLi = document.createElement("li"); >+ const messageId = `message_${index}_${Date.now()}`; >+ >+ const shouldFocusMessage = >+ focusOptions && messageMatchesFocusOptions(message, focusOptions); >+ const shouldExpand = focusOptions >+ ? shouldFocusMessage >+ : settings.expandByDefault; >+ >+ const headerDiv = createSegmentDiv( >+ "header", >+ message.header || "UNH", >+ shouldExpand, >+ true, >+ messageId >+ ); >+ messageLi.appendChild(headerDiv); >+ >+ if (shouldFocusMessage) { >+ messageLi.setAttribute("data-focus-message", "true"); >+ } >+ >+ const segmentsUl = document.createElement("ul"); >+ segmentsUl.id = messageId; >+ segmentsUl.className = `collapse${shouldExpand ? " show" : ""}`; >+ >+ const groupedSegments = groupSegmentsByLineId(message.segments); >+ >+ groupedSegments.forEach((group, i) => { >+ if (group.isLineGroup) { >+ const lineGroupLi = buildLineGroup( >+ group, >+ settings, >+ `${messageId}_line_${i}` >+ ); >+ segmentsUl.appendChild(lineGroupLi); >+ } else { >+ const relatedGrouping = groupRelatedSegments(group.segments); >+ >+ relatedGrouping.forEach(relatedItem => { >+ if (relatedItem.type === "group") { >+ const groupLi = document.createElement("li"); >+ const groupDiv = document.createElement("div"); >+ groupDiv.className = "segment-group related-segments"; >+ groupDiv.setAttribute( >+ "data-relationship", >+ relatedItem.relationship >+ ); >+ >+ relatedItem.segments.forEach(segment => { >+ const segmentDiv = createSegmentDiv( >+ "content", >+ segment.raw, >+ false, >+ true >+ ); >+ if (segment.line_id) >+ segmentDiv.dataset.lineId = segment.line_id; >+ groupDiv.appendChild(segmentDiv); >+ }); >+ >+ groupLi.appendChild(groupDiv); >+ segmentsUl.appendChild(groupLi); >+ } else { >+ const segmentLi = buildSegmentElement( >+ relatedItem.segment, >+ settings >+ ); >+ segmentsUl.appendChild(segmentLi); >+ } >+ }); >+ } >+ }); >+ >+ const trailerDiv = createSegmentDiv( >+ "trailer", >+ message.trailer || "UNT", >+ false, >+ true >+ ); >+ >+ messageLi.append(segmentsUl, trailerDiv); >+ return messageLi; >+ }; >+ >+ const buildLineGroup = (group, settings, lineId) => { >+ const lineGroupLi = document.createElement("li"); >+ const linSegment = group.segments[0]; >+ >+ const headerDiv = createSegmentDiv( >+ "header", >+ linSegment.raw, >+ settings.expandByDefault, >+ true, >+ lineId >+ ); >+ lineGroupLi.appendChild(headerDiv); >+ >+ const lineSegmentsUl = document.createElement("ul"); >+ lineSegmentsUl.id = lineId; >+ lineSegmentsUl.className = `collapse${settings.expandByDefault ? " show" : ""}`; >+ >+ const lineSegments = group.segments.slice(1); >+ const groupedSegments = groupRelatedSegments(lineSegments); >+ >+ groupedSegments.forEach(groupItem => { >+ if (groupItem.type === "group") { >+ const groupLi = document.createElement("li"); >+ const groupDiv = document.createElement("div"); >+ groupDiv.className = "segment-group related-segments"; >+ groupDiv.setAttribute( >+ "data-relationship", >+ groupItem.relationship >+ ); >+ >+ groupItem.segments.forEach(segment => { >+ const segmentDiv = createSegmentDiv( >+ "content", >+ segment.raw, >+ false, >+ true >+ ); >+ if (segment.line_id) >+ segmentDiv.dataset.lineId = segment.line_id; >+ groupDiv.appendChild(segmentDiv); >+ }); >+ >+ groupLi.appendChild(groupDiv); >+ lineSegmentsUl.appendChild(groupLi); >+ } else { >+ const segmentLi = buildSegmentElement( >+ groupItem.segment, >+ settings >+ ); >+ lineSegmentsUl.appendChild(segmentLi); >+ } >+ }); >+ >+ lineGroupLi.appendChild(lineSegmentsUl); >+ return lineGroupLi; >+ }; >+ >+ const buildSegmentElement = (segment, settings) => { >+ const segmentLi = document.createElement("li"); >+ const segmentDiv = createSegmentDiv( >+ "content", >+ segment.raw, >+ false, >+ true >+ ); >+ >+ if (segment.line_id) { >+ segmentDiv.dataset.lineId = segment.line_id; >+ } >+ >+ if (settings.showElementDetails && segment.elements) { >+ const detailsDiv = document.createElement("div"); >+ detailsDiv.className = "segment-details"; >+ detailsDiv.innerHTML = `<small>Elements: ${JSON.stringify(segment.elements)}</small>`; >+ segmentDiv.appendChild(detailsDiv); >+ } >+ >+ segmentLi.appendChild(segmentDiv); >+ return segmentLi; >+ }; >+ >+ const groupRelatedSegments = segments => { >+ const grouped = []; >+ let i = 0; >+ >+ const relationships = { >+ ALC: ["MOA", "TAX"], >+ PRI: ["MOA"], >+ QTY: ["MOA"], >+ DTM: ["MOA"], >+ TAX: ["MOA"], >+ RFF: ["DTM"], >+ }; >+ >+ while (i < segments.length) { >+ const segment = segments[i]; >+ const relatedTags = relationships[segment.tag]; >+ >+ if (relatedTags) { >+ const relatedSegments = [segment]; >+ let j = i + 1; >+ >+ while ( >+ j < segments.length && >+ relatedTags.includes(segments[j].tag) >+ ) { >+ relatedSegments.push(segments[j]); >+ j++; >+ } >+ >+ if (relatedSegments.length > 1) { >+ grouped.push({ >+ type: "group", >+ relationship: `${segment.tag}_group`, >+ segments: relatedSegments, >+ }); >+ i = j; >+ } else { >+ grouped.push({ type: "single", segment }); >+ i++; >+ } >+ } else { >+ grouped.push({ type: "single", segment }); >+ i++; >+ } >+ } >+ >+ return grouped; >+ }; >+ >+ const createSegmentDiv = ( >+ type, >+ content, >+ expandByDefault, >+ boldTag, >+ targetId >+ ) => { >+ const div = document.createElement("div"); >+ div.className = `segment ${type}`; >+ >+ if (type === "header" && targetId) { >+ div.setAttribute("data-bs-toggle", "collapse"); >+ div.setAttribute("data-bs-target", `#${targetId}`); >+ div.setAttribute( >+ "aria-expanded", >+ expandByDefault ? "true" : "false" >+ ); >+ div.setAttribute("aria-controls", targetId); >+ div.style.cursor = "pointer"; >+ >+ if (boldTag && content && content.length >= 3) { >+ const tag = content.substring(0, 3); >+ const rest = content.substring(3); >+ div.innerHTML = `<i class="fa fa-chevron-down me-1"></i> <span class="segment-tag">${escapeHtml(tag)}</span>${escapeHtml(rest)}`; >+ } else { >+ div.innerHTML = `<i class="fa fa-chevron-down me-1"></i> ${escapeHtml(content)}`; >+ } >+ } else if (boldTag && content && content.length >= 3) { >+ const tag = content.substring(0, 3); >+ const rest = content.substring(3); >+ div.innerHTML = `<span class="segment-tag">${escapeHtml(tag)}</span>${escapeHtml(rest)}`; >+ } else { >+ div.textContent = content; >+ } >+ >+ return div; >+ }; >+ >+ const groupSegmentsByLineId = segments => { >+ const groups = []; >+ let currentGroup = null; >+ >+ segments.forEach(segment => { >+ if (segment.tag === "LIN") { >+ if (currentGroup) groups.push(currentGroup); >+ currentGroup = { >+ isLineGroup: true, >+ lineId: segment.line_id, >+ segments: [segment], >+ }; >+ } else if ( >+ currentGroup && >+ segment.line_id === currentGroup.lineId >+ ) { >+ currentGroup.segments.push(segment); >+ } else { >+ if (currentGroup) { >+ groups.push(currentGroup); >+ currentGroup = null; >+ } >+ groups.push({ isLineGroup: false, segments: [segment] }); >+ } >+ }); >+ >+ if (currentGroup) groups.push(currentGroup); >+ return groups; >+ }; >+ >+ const initializeModal = settings => { >+ const modal = $(settings.modalId); >+ >+ modal.on("hidden.bs.modal", function () { >+ const modalContent = $(this).find(".modal-content"); >+ const modalBody = $(this).find(settings.modalBodySelector); >+ >+ // Remove focus highlighting and navbar >+ modalContent >+ .find(".edi-focused-message") >+ .removeClass("edi-focused-message"); >+ modalContent.find(".edi-main-navbar").remove(); >+ >+ // Remove scrolling class and reset modal body content >+ modalBody.removeClass("edi-content"); >+ modalBody.html(` >+ <div class="edi-loading"> >+ <img src="/intranet-tmpl/${window.theme || "prog"}/img/spinner-small.gif" alt="" /> >+ Loading >+ </div> >+ `); >+ }); >+ }; >+ >+ const escapeHtml = str => { >+ if (!str) return ""; >+ const escapeMap = { >+ "&": "&", >+ "<": "<", >+ ">": ">", >+ '"': """, >+ "'": "'", >+ }; >+ return str.replace(/[&<>"']/g, match => escapeMap[match]); >+ }; >+ >+ return { >+ init, >+ showMessage, >+ showMessageFromUrl, >+ buildEdiTree, >+ }; >+})(); >+ >+if (typeof $ !== "undefined") { >+ $(document).ready(() => { >+ // Individual pages handle initialization >+ }); >+} >-- >2.50.1
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 40383
:
184185
|
184487
|
184488
| 184489 |
184490