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

Sponsored-by: Martin Renvoize <martin.renvoize@gmail.com>
Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
This commit is contained in:
Martin Renvoize 2026-02-12 18:24:37 +00:00 committed by Pedro Amorim
parent 4f2829c639
commit c215f2dd43
No known key found for this signature in database
GPG Key ID: 06B75BA7F3894D55
6 changed files with 412 additions and 52 deletions

View File

@ -118,7 +118,7 @@ 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 C4::Acquisition qw( FillWithDefaultValues ModOrderUsers );
use C4::Auth qw( get_template_and_user );
@ -135,6 +135,7 @@ use Koha::Acquisition::Currencies;
use Koha::Acquisition::Orders;
use Koha::AdditionalFields;
use Koha::DateUtils qw( dt_from_string );
use Koha::Logger;
### "-------------------- addorder.pl ----------"
@ -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,29 @@ if ( $op eq 'cud-order' ) {
print $input->redirect("/cgi-bin/koha/acqui/basket.pl?basketno=$basketno");
exit;
}
=head1 build_servicing_instruction_json
my $json_string = build_servicing_instruction_json($input);
Builds the servicing instruction JSON string from form parameters. Expects
a C<servicing_instruction_groups_json> form parameter containing a JSON
string. Returns the JSON string if valid, or undef if missing or invalid.
=cut
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 ($@) {
Koha::Logger->get->warn("Invalid servicing instruction JSON: $@");
return;
}
return $json_string;
}

View File

@ -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 );
@ -32,12 +33,14 @@ 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 );
use Koha::Database;
use Koha::EDI qw( create_edi_order );
use Koha::CsvProfiles;
use Koha::Logger;
use Koha::Patrons;
use Koha::Edifact::Files;
@ -472,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;
@ -482,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 );
}
@ -509,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;
}
@ -586,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;
}
@ -676,6 +687,58 @@ 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;
}
};
if ($@) {
Koha::Logger->get->warn("Failed to parse servicing_instruction JSON: $@");
}
$line{servicing_instruction_display} = $si_display;
}
return \%line;
}

View File

@ -64,7 +64,8 @@ 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 C4::Context;
use C4::Auth qw( get_template_and_user );
@ -99,6 +100,8 @@ use Koha::RecordProcessor;
use Koha::Subscriptions;
use Koha::UI::Form::Builder::Biblio;
use Koha::AdditionalFields;
use Koha::AuthorisedValues;
use Koha::Logger;
our $input = CGI->new;
our $biblionumber = $input->param('biblionumber');
@ -379,17 +382,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') );
}
@ -442,6 +446,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 ($@) {
Koha::Logger->get->warn("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,
@ -459,39 +483,42 @@ $template->param(
closedate => $basket->{'closedate'},
# order details
suggestion => $suggestion,
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
suggestion => $suggestion,
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 ),

View File

@ -201,6 +201,8 @@ modules:
is_hidden: 1
-
columnname: invoice
-
columnname: servicing_instruction
-
columnname: supplier_report
-

View File

@ -789,6 +789,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>
@ -931,7 +932,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 %]
@ -1033,6 +1034,7 @@
<th>&nbsp;</th>
<th>&nbsp;</th>
<th>&nbsp;</th>
<th>&nbsp;</th>
</tr>
[% END %]
<tr>
@ -1059,6 +1061,7 @@
<th>&nbsp;</th>
<th>&nbsp;</th>
<th>&nbsp;</th>
<th>&nbsp;</th>
</tr>
</tfoot>
</table>
@ -1324,6 +1327,9 @@
const edifact_available = [% Koha.Preference('EDIFACT') && ediaccount ? 'true' : 'false' | html %];
supplier_report_column.visibility_condition = edifact_available;
let servicing_instruction_column = orders_table_settings.columns.find(c => c.columnname == 'servicing_instruction');
servicing_instruction_column.visibility_condition = edifact_available;
let modify_column = orders_table_settings.columns.find(c => c.columnname == 'modify');
const modify_column_available = [% active && !closedate ? 'true' : 'false' | html %];
modify_column.visibility_condition = modify_column_available;

View File

@ -208,6 +208,9 @@
getAuthValueDropbox("sort2", sort2_authcat, destination_sort2, sort2);
});
$("#budget_id").change();
// Servicing Instructions Management
initServicingInstructions();
});
function add_user(borrowernumber, borrowername) {
@ -233,6 +236,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>
@ -725,6 +950,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 %]" />