Bugzilla – Attachment 193020 Details for
Bug 30144
Add support for EDI GIR:LVT and/or GIR:LVC, Servicing instructions, segment
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 30144: Add UI for servicing instructions
c8c998d.patch (text/plain), 32.78 KB, created by
Martin Renvoize (ashimema)
on 2026-02-12 19:06:59 UTC
(
hide
)
Description:
Bug 30144: Add UI for servicing instructions
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2026-02-12 19:06:59 UTC
Size:
32.78 KB
patch
obsolete
>From c8c998d15424a9866518100165928a690383208c Mon Sep 17 00:00:00 2001 >From: Martin Renvoize <martin.renvoize@openfifth.co.uk> >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 <br> 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( '<br>', @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 @@ > <th data-colname="statistic1">Statistic 1</th> > <th data-colname="statistic2">Statistic 2</th> > <th data-colname="invoice">Invoice</th> >+ <th data-colname="servicing_instruction">Servicing instruction</th> > <th data-colname="supplier_report">Supplier report</th> > <th data-colname="place_hold" class="no-sort no-export">Place hold</th> > <th data-colname="modify" class="no-sort no-export">Modify</th> >@@ -777,6 +778,7 @@ > <th> </th> > <th> </th> > <th> </th> >+ <th> </th> > </tr> > [% END %] > <tr> >@@ -803,6 +805,7 @@ > <th> </th> > <th> </th> > <th> </th> >+ <th> </th> > </tr> > </tfoot> > <tbody> >@@ -943,7 +946,7 @@ > [% books_loo.invoice_object.invoicenumber | html %] > [% END %] > </td> >- >+ <td> [% books_loo.servicing_instruction_display | $raw %] </td> > [% IF Koha.Preference('EDIFACT') && ediaccount %] > <td>[% books_loo.suppliers_report | html %]</td> > [% 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('<div class="hint">No servicing instructions added. Click "Add servicing group" to add one.</div>'); >+ return; >+ } >+ >+ servicingInstructionGroups.forEach(function(group, groupIndex) { >+ var groupDiv = $('<div class="servicing_group"></div>'); >+ groupDiv.css({ >+ 'border': '1px solid #ddd', >+ 'padding': '10px', >+ 'margin-bottom': '10px', >+ 'background': '#f9f9f9', >+ 'border-radius': '4px' >+ }); >+ >+ var groupHeader = $('<div style="margin-bottom: 10px;"></div>'); >+ groupHeader.append('<strong>Service Group ' + (groupIndex + 1) + '</strong> '); >+ groupHeader.append( >+ $('<button type="button" class="btn btn-default btn-xs" style="margin-left: 10px;">' + >+ '<i class="fa fa-trash"></i> Remove group</button>') >+ .on('click', function() { removeServicingGroup(groupIndex); }) >+ ); >+ groupDiv.append(groupHeader); >+ >+ if (group.length === 0) { >+ groupDiv.append('<div class="hint" style="margin-left: 20px;">Empty group</div>'); >+ } >+ >+ group.forEach(function(instruction, instructionIndex) { >+ var instructionDiv = $('<div class="servicing_instruction" style="margin-left: 20px; margin-bottom: 5px;"></div>'); >+ >+ var typeSelect = $('<select style="width: 100px; margin-right: 5px;"></select>') >+ .append('<option value="LVC"' + (instruction.type === 'LVC' ? ' selected' : '') + '>LVC (Coded)</option>') >+ .append('<option value="LVT"' + (instruction.type === 'LVT' ? ' selected' : '') + '>LVT (Text)</option>') >+ .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 = $('<select style="width: 300px; margin-right: 5px;"></select>'); >+ valueInput.append('<option value="">Select...</option>'); >+ servicingInstructionAVs.forEach(function(av) { >+ valueInput.append( >+ $('<option></option>') >+ .attr('value', av.value) >+ .text(av.label) >+ .prop('selected', av.value === instruction.value) >+ ); >+ }); >+ } else { >+ valueInput = $('<input type="text" style="width: 300px; margin-right: 5px;" />') >+ .val(instruction.value); >+ } >+ >+ valueInput.on('change', function() { >+ updateInstruction(groupIndex, instructionIndex, typeSelect.val(), $(this).val()); >+ }); >+ >+ var removeBtn = $('<button type="button" class="btn btn-default btn-xs">' + >+ '<i class="fa fa-trash"></i> Remove</button>') >+ .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 = '<i class="fa fa-plus"></i> 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 = $('<button type="button" class="btn btn-default btn-xs" style="margin-left: 20px; margin-top: 5px;">' + btnText + '</button>') >+ .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 = $('<div class="hint" style="margin-left: 20px; margin-top: 5px; color: #666;">' + >+ 'Maximum 2 instructions per group (one LVC + one LVT)</div>'); >+ 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); >+ } > </script> > [% Asset.css("css/addbiblio.css") | $raw %] > </head> >@@ -716,6 +941,15 @@ > <label for="order_vendornote">Vendor note: </label> > <textarea id="order_vendornote" cols="30" rows="3" name="order_vendornote">[% IF ( order_vendornote ) %][% order_vendornote | html %][% END %]</textarea> > </li> >+ <li> >+ <label for="servicing_instructions">Servicing instructions: </label> >+ <div id="servicing_instructions_container"> >+ <!-- Servicing instruction groups will be populated here --> >+ </div> >+ <button type="button" id="add_servicing_group" class="btn btn-default btn-xs"> <i class="fa fa-plus"></i> Add servicing group </button> >+ <input type="hidden" id="servicing_instruction_groups_json" name="servicing_instruction_groups_json" value="" /> >+ <div class="hint">EDIFACT servicing instructions (GIR LVT/LVC). Each group can contain multiple coded (LVC) and/or free-text (LVT) instructions that work together.</div> >+ </li> > <li> > <label for="estimated_delivery_date">Estimated delivery date: </label> > <input type="text" id="estimated_delivery_date" size="10" name="estimated_delivery_date" class="flatpickr" value="[% estimated_delivery_date | html %]" /> >-- >2.53.0 >
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 30144
:
193017
|
193018
|
193019
|
193020
|
193021
|
193022
|
193093
|
193094
|
193095
|
193096
|
193097
|
193098
|
193099