3 $info_event_cvterms => undef
9 print "<p><strong>ERROR:</strong> Seedlot Maintenance Event Ontology not loaded!</p>";
13 my $ontology_str = encode_json($ontology);
16 <& /util/import_javascript.mas, classes => [ 'bootstrap_min.js', 'jquery' ] &>
18 <& /page/page_title.mas, title => 'Record Seedlot Maintenance' &>
20 <!-- SEEDLOT INFORMATION -->
21 <&| /page/info_section.mas, title=>"Seedlot", collapsible=>1, collapsed=>0, subtitle=>"" &>
23 <div class="well seedlot_event_well">
24 <form class="form-horizontal">
27 <div class="form-group">
28 <label class="col-sm-3 control-label">Name: </label>
29 <div class="col-sm-5">
30 <input class="form-control" id="seedlot_event_name" type="text" value="">
32 <div class="col-sm-2">
33 <button id="seedlot_event_name_update" class="btn btn-block btn-info"><span class="glyphicon glyphicon-refresh"></span> Update</button>
35 <div class="col-sm-2">
36 <button id="seedlot_event_name_barcode" class="btn btn-block btn-default"><span class="glyphicon glyphicon-qrcode"></span> Barcode</button>
40 <!-- Seedlot Contents -->
41 <div class="form-group">
42 <label class="col-sm-3 control-label">Contents: </label>
43 <div class="col-sm-9">
44 <p class="seedlot_event_info" id="seedlot_event_contents"></p>
48 <!-- Seedlot Location -->
49 <div class="form-group">
50 <label class="col-sm-3 control-label">Location: </label>
51 <div class="col-sm-9">
52 <p class="seedlot_event_info" id="seedlot_event_location"></p>
57 <div class="form-group">
58 <label class="col-sm-3 control-label">Box: </label>
59 <div class="col-sm-9">
60 <p class="seedlot_event_info" id="seedlot_event_box"></p>
64 % if ( defined $info_event_cvterms && scalar(@{$info_event_cvterms}) > 0 ) {
65 <!-- Seedlot Events -->
66 <div class="form-group">
67 <label class="col-sm-3 control-label">Recent Events: </label>
68 <div class="col-sm-9">
69 <table class="table table-hover">
79 % foreach my $cvterm (@$info_event_cvterms) {
80 <tr class="seedlot_event_info_cvterm_row" data-cvterm="<% $cvterm %>">
81 <td class="seedlot_event_info_cvterm_event"> </td>
82 <td class="seedlot_event_info_cvterm_value"></td>
83 <td class="seedlot_event_info_cvterm_notes"></td>
84 <td class="seedlot_event_info_cvterm_timestamp"></td>
98 <!-- MAINTENANCE EVENTS -->
99 <&| /page/info_section.mas, title=>"Maintenance Events", collapsible=>1, collapsed=>0, subtitle=>"" &>
100 <div id="seedlot_event_container"></div>
104 <!-- EVENT INFORMATION -->
105 <&| /page/info_section.mas, title=>"Username/Timestamp", collapsible=>1, collapsed=>0, subtitle=>"" &>
107 <div class="well seedlot_event_well">
108 <form class="form-horizontal">
111 <div class="form-group">
112 <label class="col-sm-3 control-label">Operator: </label>
113 <div class="col-sm-9">
114 <input class="form-control" id="seedlot_event_operator" name="seedlot_event_operator" type="text" value="<% $operator %>">
119 <div class="form-group">
120 <label class="col-sm-3 control-label">Timestamp: </label>
121 <div class="col-sm-9">
122 <input class="form-control" id="seedlot_event_timestamp" name="seedlot_event_timestamp" type="text" value="">
132 <!-- Submit Button -->
134 <button id="seedlot_event_submit" class="btn btn-primary btn-block" style="max-width: 600px; margin: auto" disabled>Submit</button>
140 <!-- Message Modal -->
141 <div id="seedlot_modal" class="modal fade" tabindex="-1" role="dialog">
142 <div class="modal-dialog" role="document">
143 <div class="modal-content">
144 <div id="seedlot_modal_body" class="modal-body"></div>
145 <div class="modal-footer"><button id="seedlot_modal_close" type="button" class="btn btn-default">Close</button></div>
151 <script type="text/javascript">
152 let ONTOLOGY = JSON.parse('<% $ontology_str %>'); // Seedlot Event Ontology representation (array of categories with events and values)
153 let SEEDLOT_NAME; // Name of current Seedlot
154 let SEEDLOT_ID; // ID of current Seedlot
155 let PENDING_CHANGES = false; // Flag set to true when event selections have been made
156 let MESSAGES = []; // Array of messages to display in the message modal
158 jQuery(document).ready(function () {
160 // Parse query arguments
163 // Display the maintenance events, set intitial timestamp
166 // Event change / Click listeners
167 jQuery('#seedlot_event_name').change(getSeedlotInfo);
168 jQuery('#seedlot_event_name_update').click(getSeedlotInfo);
169 jQuery('#seedlot_event_name_barcode').click(scanBarcode);
170 jQuery('#seedlot_event_submit').click(submitEvents);
171 jQuery('#seedlot_modal_close').click(function() {
173 jQuery('#seedlot_modal').modal('hide');
175 jQuery(document).on('click', '.seedlot_event_value_btn', updateEventValue);
176 jQuery(document).on('click', '.seedlot_event_notes_btn', toggleEventNotes);
178 // Autocomplete for Seedlot Name input
179 jQuery("#seedlot_event_name").autocomplete({
180 source: '/ajax/stock/seedlot_name_autocomplete',
187 * Warn when leaving the page if there are any pending events
189 jQuery(window).bind('beforeunload', function(e) {
190 if ( PENDING_CHANGES ) {
191 return e.originalEvent.returnValue = "There are pending changes that have not yet been submitted to the database! Are you sure you want to leave the page?";
201 * Parse the query parameters
202 * - Use `seedlot_name` to set the Seedlot Name input
204 function parseArgs() {
205 const urlSearchParams = new URLSearchParams(window.location.search);
206 if ( urlSearchParams.has('seedlot_name') ) {
207 let seedlot_name = decodeURIComponent(urlSearchParams.get('seedlot_name'));
208 seedlot_name = seedlot_name.includes('seedlot_name=') ? seedlot_name.match(/.*seedlot_name=(.*)/)[1] : seedlot_name;
209 jQuery('input[name="seedlot_event_name"]').val(seedlot_name);
215 * Get the details of the Seedlot specified by name
216 * - Update the contents, location, and box fields
218 function getSeedlotInfo() {
219 let name = jQuery("#seedlot_event_name").val();
223 SEEDLOT_NAME = undefined;
224 SEEDLOT_ID = undefined;
229 url: '/ajax/breeders/seedlots?seedlot_name=' + name,
230 beforeSend: function() {
231 jQuery('#seedlot_event_name').attr('disabled', true);
232 jQuery('#seedlot_event_name_update').attr('disabled', true);
233 jQuery('#seedlot_event_name_barcode').attr('disabled', true);
234 jQuery(".seedlot_event_info").html("Loading...");
235 jQuery(".seedlot_event_info_cvterm_value").html("");
236 jQuery(".seedlot_event_info_cvterm_notes").html("");
237 jQuery(".seedlot_event_info_cvterm_timestamp").html("");
239 success: function(response) {
240 if ( response && response.data ) {
241 for ( let i = 0; i < response.data.length; i++ ) {
242 if ( response.data[i].seedlot_stock_uniquename.toUpperCase() === name.toUpperCase() ) {
243 let sl = response.data[i];
244 contents = sl.contents_html;
245 location = sl.location;
248 SEEDLOT_ID = sl.seedlot_stock_id;
254 complete: function() {
255 jQuery("#seedlot_event_contents").html(contents);
256 jQuery("#seedlot_event_location").html(location);
257 jQuery("#seedlot_event_box").html(box);
258 jQuery('#seedlot_event_name').attr('disabled', false);
259 jQuery('#seedlot_event_name_update').attr('disabled', false);
260 jQuery('#seedlot_event_name_barcode').attr('disabled', false);
268 * Get the recent events for the current Seedlot
269 * - Populate the table of recent event info
271 function getSeedlotEvents() {
272 jQuery(".seedlot_event_info_cvterm_row").each(function() {
273 let row = jQuery(this);
274 let event = jQuery(row.find(".seedlot_event_info_cvterm_event")[0]);
275 let value = jQuery(row.find(".seedlot_event_info_cvterm_value")[0]);
276 let notes = jQuery(row.find(".seedlot_event_info_cvterm_notes")[0]);
277 let timestamp = jQuery(row.find(".seedlot_event_info_cvterm_timestamp")[0]);
278 let cvterm = row.data("cvterm");
280 // Get events for seedlot
283 url: '/ajax/breeders/seedlot/maintenance/search',
284 data: JSON.stringify({
286 names: [{comp: '=', value: SEEDLOT_NAME}],
287 types: [{cvterm_id: cvterm}]
290 contentType: "application/json; charset=utf-8",
292 success: function(data) {
293 if ( data && data.results && data.results.length > 0 ) {
294 let e = data.results[0];
295 event.html("<strong>" + e.cvterm_name + "</strong>");
297 notes.html(e.notes ? e.notes : "");
298 timestamp.html(e.timestamp);
307 * Redirect to the Barcode Reader
309 function scanBarcode() {
310 window.location = "/barcode/read?return=/breeders/seedlot/maintenance/record¶m=seedlot_name";
316 * - reset the Event display
317 * - reset the timestamp
322 PENDING_CHANGES = false;
323 jQuery("#seedlot_event_submit").attr('disabled', true);
327 * Build the display of each of the maintenace events and their values
329 function setEvents() {
333 // Parse each category
334 for ( let i = 0; i < ONTOLOGY.length; i++ ) {
335 let category = ONTOLOGY[i];
336 let events = category.children ? category.children : [];
339 html += "<p class='seedlot_event_category'>" + category.name + "</p>";
342 for ( let j = 0; j < events.length; j++ ) {
343 let event = events[j];
344 let values = event.children ? event.children : [];
347 html += "<div class='well seedlot_event_well'>";
350 html += "<p class='seedlot_event_title'>" + event.name + "</p>";
353 if ( event.definition ) {
354 html += "<p class='seedlot_event_definition'>" + jQuery('<div>').html(event.definition).text() + "</p>";
358 html += "<div class='seedlot_event_values_div'>";
360 // Event Values (Buttons/Text) Div
361 html += "<div class='btn-group seedlot_event_values' data-cvterm='" + event.cvterm_id + "' role='group'>";
363 // Values: Add buttons for pre-determined values
364 if ( values && values.length > 0 ) {
365 html += "<button type='button' class='btn btn-primary seedlot_event_value seedlot_event_value_btn'>Not Recorded</button>";
366 for ( let k = 0; k < values.length; k++ ) {
367 let value = values[k];
368 html += "<button type='button' class='btn btn-default seedlot_event_value seedlot_event_value_btn'>" + value.name + "</button>";
372 // Values: Add input text field
374 html += "<input type='text' class='form-control seedlot_event_value seedlot_event_value_input' value=''>";
381 html += "<div class='seedlot_event_notes_btn_div'>";
382 html += "<button type='button' class='btn btn-default seedlot_event_notes_btn' data-cvterm='" + event.cvterm_id + "'> <span class='glyphicon glyphicon-comment'></span> </button>";
389 html += "<div id='seedlot_event_notes_input_div_" + event.cvterm_id + "' class='seedlot_event_notes_input_div'>";
390 html += "<textarea class='form-control seedlot_event_notes_input' rows='3' placeholder='Notes about the Event'></textarea>";
399 jQuery("#seedlot_event_container").html(html);
403 * Change the highlighted button of the selected value
405 function updateEventValue() {
406 let el = jQuery(this);
407 let group = el.parent('.seedlot_event_values');
409 group.find(".seedlot_event_value_btn").removeClass("btn-primary").addClass("btn-default");
410 el.removeClass("btn-default").addClass("btn-primary");
412 PENDING_CHANGES = true;
413 jQuery("#seedlot_event_submit").attr('disabled', false);
417 * Toggle the display of the event notes
419 function toggleEventNotes() {
420 let el = jQuery(this);
421 let cvterm = el.data("cvterm");
422 let displayed = jQuery("#seedlot_event_notes_input_div_" + cvterm).css("display") !== 'none';
424 jQuery("#seedlot_event_notes_input_div_" + cvterm).css("display", displayed ? 'none' : 'block');
425 if ( displayed ) jQuery("#seedlot_event_notes_input_" + cvterm).val("");
426 el.removeClass(displayed ? 'btn-primary' : 'btn-default');
427 el.addClass(displayed ? 'btn-default' : 'btn-primary');
431 * Set the value of the timestamp input to YYYY-MM-DD HH:MM:SS
433 function setTimestamp() {
434 let now = new Date();
435 let y = now.getFullYear();
436 let m = now.getMonth() + 1;
437 let d = now.getDate();
438 let h = now.getHours();
439 let i = now.getMinutes();
440 let s = now.getSeconds();
442 if ( m <= 9 ) m = '0' + m;
443 if ( d <= 9 ) d = '0' + d;
444 if ( h <= 9 ) h = '0' + h;
445 if ( i <= 9 ) i = '0' + i;
446 if ( s <= 9 ) s = '0' + s;
448 let ts = y + '-' + m + '-' + d + ' ' + h + ':' + i + ':' + s;
449 jQuery("input[name='seedlot_event_timestamp']").val(ts);
458 * Submit the pending events to the database
459 * - Get the IDs of the Seedlots (by name) in PENDING_EVENTS
460 * - Submit the events for each Seedlot
461 * - If submitted, remove the events from PENDING_EVENTS
463 function submitEvents() {
465 // Check for Seedlot ID
467 displayError("Please enter a valid Seedlot name");
471 // Build the event info to submit
473 let event_groups = jQuery(".seedlot_event_values");
474 let operator = jQuery("#seedlot_event_operator").val();
475 let timestamp = jQuery("#seedlot_event_timestamp").val();
476 for ( let i = 0; i < event_groups.length; i++ ) {
477 let event_group = jQuery(event_groups[i]);
478 let cvterm_id = event_group.data("cvterm");
479 let event_value = event_group.find('.seedlot_event_value_btn.btn-primary,.seedlot_event_value_input');
480 let value = event_value.hasClass('seedlot_event_value_btn') ? event_value.html() : event_value.val();
481 let notes = jQuery("#seedlot_event_notes_input_div_" + cvterm_id).find(".seedlot_event_notes_input").val();
483 if ( value && value !== "" && value !== "Not Recorded" ) {
485 cvterm_id: cvterm_id,
487 notes: notes !== "" ? notes : undefined,
488 operator: operator !== "" ? operator : undefined,
489 timestamp: timestamp !== "" ? timestamp : undefined
495 // Make sure there's at least one recorded event
496 if ( !events || events.length === 0 ) {
497 displayError("One or more Events must be recorded");
501 // Disable Submit Button
502 jQuery("#seedlot_event_submit").attr('disabled', true);
503 jQuery("#seedlot_event_submit").html("Submitting...");
508 url: "/ajax/breeders/seedlot/" + SEEDLOT_ID + "/maintenance",
509 data: JSON.stringify({events: events}),
510 contentType: "application/json; charset=utf-8",
512 success: function(data) {
513 if ( data && data.events && data.events.length === events.length ) {
514 displaySuccess(data.events.length + " events successfully stored for Seedlot " + SEEDLOT_NAME);
515 return _finish(true);
517 else if ( data && data.error ) {
518 displayError("The events could not be submitted due to a database error:<br /><br /><pre><code>" + data.error + "</code></pre>");
522 displayError("The events could not be submitted due to an unknown database error");
526 error: function(msg) {
527 displayError("The events could not be submitted due to a database error:<br /><br /><pre><code>" + msg + "</code></pre>");
532 function _finish(complete) {
533 jQuery("#seedlot_event_submit").attr('disabled', false);
534 jQuery("#seedlot_event_submit").html("Submit");
536 window.scrollTo(0, 0);
543 * Display an error message in a bootstrap modal
544 * @param {string} msg Error message to display (can include HTML)
546 function displayError(msg) {
547 displayModal("Error", "#a94442", msg);
551 * Display a success message in a bootstrap modal
552 * @param {string} msg Success message to display (can include HTML)
554 function displaySuccess(msg) {
555 displayModal("Success", "#3c763d", msg);
559 * Display a message (and any existing messages) in a bootstrap modal
560 * @param {string} title Message title
561 * @param {string} color Message title color
562 * @param {string} msg Message to display (can include HTML)
564 function displayModal(title, color, msg) {
570 MESSAGES.push(message);
573 for ( let i = 0; i < MESSAGES.length; i++ ) {
574 let _html = "<h1 style='color: " + MESSAGES[i].color + "'>" + MESSAGES[i].title + "</h1>";
575 _html += "<p style='margin: 15px 5px; font-size: 110%;'>" + MESSAGES[i].msg + "</p>";
579 jQuery('#seedlot_modal_body').html(html.join("<hr />"));
580 jQuery('#seedlot_modal').modal({backdrop: 'static', keyboard: false});
586 .seedlot_event_info {
590 .seedlot_event_well {
592 margin: 0 auto 25px auto;
594 .seedlot_event_category {
597 margin: 25px 0 10px 0;
598 border-bottom: 1px solid #ccc;
600 .seedlot_event_title {
605 .seedlot_event_definition {
610 .seedlot_event_values_div {
614 .seedlot_event_values {
617 .seedlot_event_notes_btn_div {
620 .seedlot_event_notes_input_div {
624 .seedlot_event_value_btn {