From ff46ef449fc1b4501da3430c535080849eebbb5e Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Thu, 12 Feb 2026 18:24:37 +0000 Subject: [PATCH] Bug 30144: Add UI for servicing instructions This patch adds the user interface for managing and displaying EDIFACT servicing instructions in the acquisitions module. Changes to acqui/neworderempty.pl: - Parses servicing_instruction JSON for editing existing orders - Fetches EDIFACT_SI authorized values for dropdown - Passes data to template in both array and JSON formats Changes to acqui/addorder.pl: - Accepts servicing_instruction_groups_json from form submission - Validates and stores JSON in database Changes to acqui/basket.pl: - Fetches EDIFACT_SI authorized values - Formats servicing instructions for display in get_order_infos() - Creates human-readable display strings with: * LVC codes replaced with descriptions * Groups separated by
tags * Format: "Description" or "Description: free text" Changes to neworderempty.tt: - Dynamic UI for adding/removing servicing instruction groups - Each group can contain: * One LVC (coded) instruction only, OR * One LVT (free-text) instruction only, OR * One LVC + one LVT combination - Validation prevents: * More than 2 instructions per group * Duplicate LVC or LVT in same group - LVC instructions use dropdown populated from EDIFACT_SI authorized values - LVT instructions use free-text input - Groups serialized to JSON hidden field for form submission - Smart button labels show what can be added to each group Changes to basket.tt: - Displays servicing instructions in basket table - Pre-formatted in Perl (no JavaScript parsing needed) - Shows LVC descriptions instead of codes - Multiple groups displayed on separate lines Changes to columns_settings.yml: - Adds servicing_instruction column to basket table configuration Display format examples: - LVC only: "Barcode labelling" - LVT only: "Handle with care" - Combined: "Binding: Apply to spine" - Multiple groups (on separate lines): Barcode labelling Binding: Special instructions Test plan: 1. Create/edit order 2. Add servicing instruction groups 3. Try to add more than 2 instructions to a group (should prevent) 4. Try to add duplicate LVC or LVT to group (should prevent) 5. Save order 6. View basket and verify instructions display correctly 7. Edit order and verify instructions load into form --- acqui/addorder.pl | 24 +- acqui/basket.pl | 69 +++++- acqui/neworderempty.pl | 121 +++++---- admin/columns_settings.yml | 2 + .../prog/en/modules/acqui/basket.tt | 14 +- .../prog/en/modules/acqui/neworderempty.tt | 234 ++++++++++++++++++ 6 files changed, 410 insertions(+), 54 deletions(-) diff --git a/acqui/addorder.pl b/acqui/addorder.pl index 42309ea2ceb..46fa5fd11fd 100755 --- a/acqui/addorder.pl +++ b/acqui/addorder.pl @@ -118,7 +118,8 @@ if it is an order from an existing suggestion : the id of this suggestion. use Modern::Perl; use CGI qw ( -utf8 ); -use JSON qw ( to_json encode_json ); +use JSON qw ( to_json encode_json decode_json ); +use Carp qw( carp ); use C4::Acquisition qw( FillWithDefaultValues ModOrderUsers ); use C4::Auth qw( get_template_and_user ); @@ -266,6 +267,7 @@ if ( $op eq 'cud-order' ) { unitprice => scalar $input->param('unitprice'), order_internalnote => scalar $input->param('order_internalnote'), order_vendornote => scalar $input->param('order_vendornote'), + servicing_instruction => build_servicing_instruction_json($input), sort1 => scalar $input->param('sort1'), sort2 => scalar $input->param('sort2'), subscriptionid => scalar $input->param('subscriptionid'), @@ -510,3 +512,23 @@ if ( $op eq 'cud-order' ) { print $input->redirect("/cgi-bin/koha/acqui/basket.pl?basketno=$basketno"); exit; } + +# Helper function to build servicing instruction JSON from form parameters +# Expects form parameters in the format: +# servicing_instruction_groups_json (JSON string) +# Returns JSON string or undef +sub build_servicing_instruction_json { + my ($input) = @_; + + my $json_string = $input->param('servicing_instruction_groups_json'); + return unless $json_string; + + # Validate JSON + eval { decode_json($json_string); }; + if ($@) { + carp "Invalid servicing instruction JSON: $@"; + return; + } + + return $json_string; +} diff --git a/acqui/basket.pl b/acqui/basket.pl index 9d00d40f880..8b72075a10c 100755 --- a/acqui/basket.pl +++ b/acqui/basket.pl @@ -24,6 +24,7 @@ use Modern::Perl; use C4::Auth qw( get_template_and_user haspermission ); use C4::Output qw( output_html_with_http_headers output_and_exit ); use CGI qw ( -utf8 ); +use JSON qw( decode_json ); use C4::Acquisition qw( GetBasket CanUserManageBasket GetBasketAsCSV NewBasket NewBasketgroup ModBasket ReopenBasket ModBasketUsers GetBasketgroup GetBasketgroups GetBasketUsers GetOrders GetOrder get_rounded_price ); use C4::Budgets qw( GetBudgetHierarchy GetBudget CanUserUseBudget ); @@ -33,6 +34,7 @@ use Koha::Biblios; use Koha::Acquisition::Baskets; use Koha::Acquisition::Booksellers; use Koha::Acquisition::Orders; +use Koha::AuthorisedValues; use Koha::Libraries; use C4::Letters qw( SendAlerts ); use Date::Calc qw( Add_Delta_Days ); @@ -473,6 +475,13 @@ if ( $op eq 'list' ) { my $active_currency = Koha::Acquisition::Currencies->get_active; + # Fetch EDIFACT servicing instruction authorized values for display + my @edifact_si_avs = Koha::AuthorisedValues->search( + { category => 'EDIFACT_SI' }, + { order_by => 'lib' } + )->as_list; + my %edifact_si_map = map { $_->authorised_value => $_->lib } @edifact_si_avs; + my @orders = GetOrders($basketno); my @books_loop; @@ -483,7 +492,7 @@ if ( $op eq 'list' ) { my $total_tax_included = 0; my $total_tax_value = 0; for my $order (@orders) { - my $line = get_order_infos( $order, $bookseller ); + my $line = get_order_infos( $order, $bookseller, \%edifact_si_map ); if ( $line->{uncertainprice} ) { $template->param( uncertainprices => 1 ); } @@ -510,7 +519,7 @@ if ( $op eq 'list' ) { my @cancelledorders = GetOrders( $basketno, { cancelled => 1 } ); my @cancelledorders_loop; for my $order (@cancelledorders) { - my $line = get_order_infos( $order, $bookseller ); + my $line = get_order_infos( $order, $bookseller, \%edifact_si_map ); push @cancelledorders_loop, $line; } @@ -587,9 +596,10 @@ $template->param( messages => \@messages ); output_html_with_http_headers $query, $cookie, $template->output; sub get_order_infos { - my $order = shift; - my $bookseller = shift; - my $qty = $order->{'quantity'} || 0; + my $order = shift; + my $bookseller = shift; + my $edifact_si_map = shift; + my $qty = $order->{'quantity'} || 0; if ( !defined $order->{quantityreceived} ) { $order->{quantityreceived} = 0; } @@ -679,6 +689,55 @@ sub get_order_infos { } } + # Format servicing instruction for display + if ( $line{servicing_instruction} && $edifact_si_map ) { + my $si_display = ''; + eval { + my $groups = decode_json( $line{servicing_instruction} ); + if ( ref $groups eq 'ARRAY' && @$groups ) { + my @display_parts; + foreach my $group (@$groups) { + if ( ref $group eq 'ARRAY' && @$group && @$group <= 2 ) { + my $lvc; + my $lvt; + + # Extract LVC and LVT from group + foreach my $instruction (@$group) { + if ( ref $instruction eq 'HASH' + && $instruction->{type} + && $instruction->{value} ) + { + if ( $instruction->{type} eq 'LVC' ) { + $lvc = $instruction->{value}; + } elsif ( $instruction->{type} eq 'LVT' ) { + $lvt = $instruction->{value}; + } + } + } + + # Format based on what's present + if ( $lvc && $lvt ) { + + # Both: "LVC description: LVT text" + my $lvc_desc = $edifact_si_map->{$lvc} || $lvc; + push @display_parts, $lvc_desc . ': ' . $lvt; + } elsif ($lvc) { + + # LVC only: show description + push @display_parts, $edifact_si_map->{$lvc} || $lvc; + } elsif ($lvt) { + + # LVT only: show text + push @display_parts, $lvt; + } + } + } + $si_display = join( '
', @display_parts ) if @display_parts; + } + }; + $line{servicing_instruction_display} = $si_display; + } + return \%line; } diff --git a/acqui/neworderempty.pl b/acqui/neworderempty.pl index 307c48509e2..5b25274fdd4 100755 --- a/acqui/neworderempty.pl +++ b/acqui/neworderempty.pl @@ -64,7 +64,9 @@ the item's id in the breeding reservoir =cut use Modern::Perl; -use CGI qw ( -utf8 ); +use CGI qw ( -utf8 ); +use JSON qw( decode_json encode_json ); +use Carp qw( carp ); use C4::Context; use C4::Auth qw( get_template_and_user ); @@ -100,6 +102,7 @@ use Koha::RecordProcessor; use Koha::Subscriptions; use Koha::UI::Form::Builder::Biblio; use Koha::AdditionalFields; +use Koha::AuthorisedValues; our $input = CGI->new; our $biblionumber = $input->param('biblionumber'); @@ -381,17 +384,18 @@ if ( defined $from_subscriptionid ) { $budget_id = $lastOrderReceived->{budgetid}; $data->{listprice} = $lastOrderReceived->{listprice}; $data->{uncertainprice} = $lastOrderReceived->{uncertainprice}; - $data->{tax_rate} = $lastOrderReceived->{tax_rate_on_ordering}; - $data->{discount} = $lastOrderReceived->{discount}; - $data->{rrp} = $lastOrderReceived->{rrp}; - $data->{replacementprice} = $lastOrderReceived->{replacementprice}; - $data->{ecost} = $lastOrderReceived->{ecost}; - $data->{quantity} = $lastOrderReceived->{quantity}; - $data->{unitprice} = $lastOrderReceived->{unitprice}; - $data->{order_internalnote} = $lastOrderReceived->{order_internalnote}; - $data->{order_vendornote} = $lastOrderReceived->{order_vendornote}; - $data->{sort1} = $lastOrderReceived->{sort1}; - $data->{sort2} = $lastOrderReceived->{sort2}; + $data->{tax_rate} = $lastOrderReceived->{tax_rate_on_ordering}; + $data->{discount} = $lastOrderReceived->{discount}; + $data->{rrp} = $lastOrderReceived->{rrp}; + $data->{replacementprice} = $lastOrderReceived->{replacementprice}; + $data->{ecost} = $lastOrderReceived->{ecost}; + $data->{quantity} = $lastOrderReceived->{quantity}; + $data->{unitprice} = $lastOrderReceived->{unitprice}; + $data->{order_internalnote} = $lastOrderReceived->{order_internalnote}; + $data->{order_vendornote} = $lastOrderReceived->{order_vendornote}; + $data->{servicing_instruction} = $lastOrderReceived->{servicing_instruction}; + $data->{sort1} = $lastOrderReceived->{sort1}; + $data->{sort2} = $lastOrderReceived->{sort2}; $basket = GetBasket( $input->param('basketno') ); } @@ -444,6 +448,26 @@ $template->param( items => $items, ); +# Get servicing instruction authorized values for EDIFACT_SI category +my @servicing_instruction_authorised_values = Koha::AuthorisedValues->search( + { category => 'EDIFACT_SI' }, + { order_by => 'lib' } +)->as_list; + +# Parse servicing instruction JSON into array of groups +my $servicing_instruction_groups = []; +my $servicing_instruction_groups_json = '[]'; +if ( $data->{'servicing_instruction'} ) { + eval { $servicing_instruction_groups = decode_json( $data->{'servicing_instruction'} ); }; + if ($@) { + carp "Failed to parse servicing_instruction JSON: $@"; + } else { + + # Re-encode for JavaScript (already validated JSON) + $servicing_instruction_groups_json = $data->{'servicing_instruction'}; + } +} + # fill template $template->param( existing => $biblionumber, @@ -461,41 +485,44 @@ $template->param( closedate => $basket->{'closedate'}, # order details - suggestionid => $suggestion->{suggestionid}, - surnamesuggestedby => $suggestion->{surnamesuggestedby}, - firstnamesuggestedby => $suggestion->{firstnamesuggestedby}, - biblionumber => $biblionumber, - uncertainprice => $data->{'uncertainprice'}, - discount_2dp => sprintf( "%.2f", $bookseller->discount ), # for display - discount => $bookseller->discount, - orderdiscount_2dp => sprintf( "%.2f", $data->{'discount'} || 0 ), - orderdiscount => $data->{'discount'}, - order_internalnote => $data->{'order_internalnote'}, - order_vendornote => $data->{'order_vendornote'}, - listincgst => $bookseller->listincgst, - invoiceincgst => $bookseller->invoiceincgst, - cur_active_sym => $active_currency->symbol, - cur_active => $active_currency->currency, - currencies => Koha::Acquisition::Currencies->search, - currency => $data->{currency}, - vendor_currency => $bookseller->listprice, - orderexists => ( $new eq 'yes' ) ? 0 : 1, - title => $data->{'title'}, - author => $data->{'author'}, - publicationyear => $data->{'publicationyear'} ? $data->{'publicationyear'} : $data->{'copyrightdate'}, - editionstatement => $data->{'editionstatement'}, - budget_loop => $budget_loop, - isbn => $data->{'isbn'}, - ean => $data->{'ean'}, - seriestitle => $data->{'seriestitle'}, - itemtypeloop => \@itemtypes, - quantity => $quantity, - quantityrec => $quantity, - quantitysugg => $data->{quantitysugg}, - rrp => $data->{'rrp'}, - replacementprice => $data->{'replacementprice'}, - gst_values => \@gst_values, - tax_rate => $data->{tax_rate_on_ordering} ? $data->{tax_rate_on_ordering} + 0.0 + suggestionid => $suggestion->{suggestionid}, + surnamesuggestedby => $suggestion->{surnamesuggestedby}, + firstnamesuggestedby => $suggestion->{firstnamesuggestedby}, + biblionumber => $biblionumber, + uncertainprice => $data->{'uncertainprice'}, + discount_2dp => sprintf( "%.2f", $bookseller->discount ), # for display + discount => $bookseller->discount, + orderdiscount_2dp => sprintf( "%.2f", $data->{'discount'} || 0 ), + orderdiscount => $data->{'discount'}, + order_internalnote => $data->{'order_internalnote'}, + order_vendornote => $data->{'order_vendornote'}, + servicing_instruction_groups => $servicing_instruction_groups, + servicing_instruction_groups_json => $servicing_instruction_groups_json, + servicing_instruction_authorised_values => \@servicing_instruction_authorised_values, + listincgst => $bookseller->listincgst, + invoiceincgst => $bookseller->invoiceincgst, + cur_active_sym => $active_currency->symbol, + cur_active => $active_currency->currency, + currencies => Koha::Acquisition::Currencies->search, + currency => $data->{currency}, + vendor_currency => $bookseller->listprice, + orderexists => ( $new eq 'yes' ) ? 0 : 1, + title => $data->{'title'}, + author => $data->{'author'}, + publicationyear => $data->{'publicationyear'} ? $data->{'publicationyear'} : $data->{'copyrightdate'}, + editionstatement => $data->{'editionstatement'}, + budget_loop => $budget_loop, + isbn => $data->{'isbn'}, + ean => $data->{'ean'}, + seriestitle => $data->{'seriestitle'}, + itemtypeloop => \@itemtypes, + quantity => $quantity, + quantityrec => $quantity, + quantitysugg => $data->{quantitysugg}, + rrp => $data->{'rrp'}, + replacementprice => $data->{'replacementprice'}, + gst_values => \@gst_values, + tax_rate => $data->{tax_rate_on_ordering} ? $data->{tax_rate_on_ordering} + 0.0 : $bookseller->tax_rate ? $bookseller->tax_rate + 0.0 : 0, listprice => sprintf( "%.2f", $data->{listprice} || $data->{price} || $listprice ), diff --git a/admin/columns_settings.yml b/admin/columns_settings.yml index c249e0d0fda..57c371d6ca5 100644 --- a/admin/columns_settings.yml +++ b/admin/columns_settings.yml @@ -201,6 +201,8 @@ modules: is_hidden: 1 - columnname: invoice + - + columnname: servicing_instruction - columnname: supplier_report - 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 163fa5f323e..9544d1581d0 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/basket.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/basket.tt @@ -745,6 +745,7 @@ Statistic 1 Statistic 2 Invoice + Servicing instruction Supplier report Place hold Modify @@ -777,6 +778,7 @@       +   [% END %] @@ -803,6 +805,7 @@       +   @@ -943,7 +946,7 @@ [% books_loo.invoice_object.invoicenumber | html %] [% END %] - + [% books_loo.servicing_instruction_display | $raw %] [% IF Koha.Preference('EDIFACT') && ediaccount %] [% books_loo.suppliers_report | html %] [% ELSE %] @@ -1416,6 +1419,15 @@ [% END %] supplier_report_column.force_visibility = 1; + let servicing_instruction_column = orders_table_settings.columns.find(c => c.columnname == 'servicing_instruction'); + [% IF !(Koha.Preference('EDIFACT') && ediaccount) %] + servicing_instruction_column.is_hidden = 1; + servicing_instruction_column.cannot_be_toggled = 1; + [% ELSE %] + servicing_instruction_column.is_hidden = 0; + [% END %] + servicing_instruction_column.force_visibility = 1; + let modify_column = orders_table_settings.columns.find(c => c.columnname == 'modify'); [% IF ( active && !closedate ) %] modify_column.is_hidden = 0; diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/neworderempty.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/neworderempty.tt index 782f0dc2d94..01557db4425 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/neworderempty.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/neworderempty.tt @@ -198,6 +198,9 @@ getAuthValueDropbox( 'sort2', sort2_authcat, destination_sort2, sort2 ); }); $("#budget_id").change(); + + // Servicing Instructions Management + initServicingInstructions(); }); function add_user(borrowernumber, borrowername) { @@ -225,6 +228,228 @@ ids.splice(ids.indexOf(borrowernumber.toString()), 1); $("#users_ids").val(ids.join(':')); } + + // Servicing Instructions Management + var servicingInstructionGroups = []; + var servicingInstructionAVs = [ + [% FOREACH av IN servicing_instruction_authorised_values %] + { value: '[% av.authorised_value | html %]', label: '[% av.lib | html %]' }, + [% END %] + ]; + + function initServicingInstructions() { + // Load existing data from template + try { + var existingData = JSON.parse('[% servicing_instruction_groups_json | $raw %]'); + servicingInstructionGroups = Array.isArray(existingData) ? existingData : []; + } catch (e) { + console.error('Failed to parse servicing instruction groups:', e); + servicingInstructionGroups = []; + } + + renderServicingInstructions(); + updateJSON(); + + // Add group button handler + $('#add_servicing_group').on('click', function() { + addServicingGroup(); + }); + + // Form submission handler - update JSON before submit + $('form').on('submit', function() { + updateJSON(); + }); + } + + function addServicingGroup() { + servicingInstructionGroups.push([]); + renderServicingInstructions(); + updateJSON(); + } + + function removeServicingGroup(groupIndex) { + servicingInstructionGroups.splice(groupIndex, 1); + renderServicingInstructions(); + updateJSON(); + } + + function addInstructionToGroup(groupIndex, type, value) { + var group = servicingInstructionGroups[groupIndex]; + + // Validate: max 2 instructions per group + if (group.length >= 2) { + alert('Each service group can contain at most 2 instructions (one LVC and/or one LVT).'); + return; + } + + if (!type) type = 'LVC'; // Default to LVC (coded) + if (!value) value = ''; + + // Check if this type already exists in the group + var hasLVC = group.some(function(inst) { return inst.type === 'LVC'; }); + var hasLVT = group.some(function(inst) { return inst.type === 'LVT'; }); + + if (type === 'LVC' && hasLVC) { + alert('This group already has an LVC instruction. Each group can have only one LVC.'); + return; + } + if (type === 'LVT' && hasLVT) { + alert('This group already has an LVT instruction. Each group can have only one LVT.'); + return; + } + + servicingInstructionGroups[groupIndex].push({ type: type, value: value }); + renderServicingInstructions(); + updateJSON(); + } + + function removeInstructionFromGroup(groupIndex, instructionIndex) { + servicingInstructionGroups[groupIndex].splice(instructionIndex, 1); + renderServicingInstructions(); + updateJSON(); + } + + function updateInstruction(groupIndex, instructionIndex, type, value) { + var group = servicingInstructionGroups[groupIndex]; + + // Check if changing type would create a duplicate + for (var i = 0; i < group.length; i++) { + if (i !== instructionIndex && group[i].type === type) { + alert('This group already has a ' + type + ' instruction. Each group can have only one of each type.'); + renderServicingInstructions(); // Re-render to reset the select + return; + } + } + + servicingInstructionGroups[groupIndex][instructionIndex] = { type: type, value: value }; + updateJSON(); + } + + function renderServicingInstructions() { + var container = $('#servicing_instructions_container'); + container.empty(); + + if (servicingInstructionGroups.length === 0) { + container.append('
No servicing instructions added. Click "Add servicing group" to add one.
'); + return; + } + + servicingInstructionGroups.forEach(function(group, groupIndex) { + var groupDiv = $('
'); + groupDiv.css({ + 'border': '1px solid #ddd', + 'padding': '10px', + 'margin-bottom': '10px', + 'background': '#f9f9f9', + 'border-radius': '4px' + }); + + var groupHeader = $('
'); + groupHeader.append('Service Group ' + (groupIndex + 1) + ' '); + groupHeader.append( + $('') + .on('click', function() { removeServicingGroup(groupIndex); }) + ); + groupDiv.append(groupHeader); + + if (group.length === 0) { + groupDiv.append('
Empty group
'); + } + + group.forEach(function(instruction, instructionIndex) { + var instructionDiv = $('
'); + + var typeSelect = $('') + .append('') + .append('') + .on('change', function() { + var newType = $(this).val(); + var valueInput = $(this).next(); + updateInstruction(groupIndex, instructionIndex, newType, valueInput.val()); + renderServicingInstructions(); // Re-render to update input type + }); + + var valueInput; + if (instruction.type === 'LVC' && servicingInstructionAVs.length > 0) { + valueInput = $(''); + valueInput.append(''); + servicingInstructionAVs.forEach(function(av) { + valueInput.append( + $('') + .attr('value', av.value) + .text(av.label) + .prop('selected', av.value === instruction.value) + ); + }); + } else { + valueInput = $('') + .val(instruction.value); + } + + valueInput.on('change', function() { + updateInstruction(groupIndex, instructionIndex, typeSelect.val(), $(this).val()); + }); + + var removeBtn = $('') + .on('click', function() { removeInstructionFromGroup(groupIndex, instructionIndex); }); + + instructionDiv.append(typeSelect); + instructionDiv.append(valueInput); + instructionDiv.append(removeBtn); + groupDiv.append(instructionDiv); + }); + + // Determine what can be added to this group + var hasLVC = group.some(function(inst) { return inst.type === 'LVC'; }); + var hasLVT = group.some(function(inst) { return inst.type === 'LVT'; }); + var canAddMore = group.length < 2; + + if (canAddMore) { + var btnText = ' Add instruction'; + if (hasLVC && !hasLVT) { + btnText += ' (LVT only)'; + } else if (hasLVT && !hasLVC) { + btnText += ' (LVC only)'; + } else if (group.length === 0) { + btnText += ' to this group'; + } + + var addInstructionBtn = $('') + .on('click', function() { + // Determine default type based on what's already in the group + var defaultType = 'LVC'; + if (hasLVC && !hasLVT) defaultType = 'LVT'; + else if (hasLVT && !hasLVC) defaultType = 'LVC'; + addInstructionToGroup(groupIndex, defaultType); + }); + groupDiv.append(addInstructionBtn); + } else { + var maxMsg = $('
' + + 'Maximum 2 instructions per group (one LVC + one LVT)
'); + groupDiv.append(maxMsg); + } + + container.append(groupDiv); + }); + } + + function updateJSON() { + // Filter out empty groups and instructions with empty values + var filtered = servicingInstructionGroups + .map(function(group) { + return group.filter(function(instruction) { + return instruction.value && instruction.value.trim() !== ''; + }); + }) + .filter(function(group) { + return group.length > 0; + }); + + var json = JSON.stringify(filtered); + $('#servicing_instruction_groups_json').val(json); + } [% Asset.css("css/addbiblio.css") | $raw %] @@ -716,6 +941,15 @@ +
  • + +
    + +
    + + +
    EDIFACT servicing instructions (GIR LVT/LVC). Each group can contain multiple coded (LVC) and/or free-text (LVT) instructions that work together.
    +
  • -- 2.53.0