Merge pull request #5280 from solgenomics/5714_phenotype_genotype_data_check
[sgn.git] / mason / breeders_toolbox / seedlot_maintenance / record.mas
blob21d00e187794c2dc79a8507e4e5414f77e4e1192
1 <%args>
2     $ontology => undef
3     $info_event_cvterms => undef
4     $operator => undef
5 </%args>
7 <%init>
8     if ( !$ontology ) {
9         print "<p><strong>ERROR:</strong> Seedlot Maintenance Event Ontology not loaded!</p>";
10         return;
11     }
12     use JSON;
13     my $ontology_str = encode_json($ontology);
14 </%init>
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=>"" &>
22     <br /><br />
23     <div class="well seedlot_event_well">
24         <form class="form-horizontal">
26             <!-- Seedlot Name -->
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="">
31                 </div>
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>
34                 </div>
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>
37                 </div>
38             </div>
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>
45                 </div>
46             </div>
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>
53                 </div>
54             </div>
56             <!-- Seedlot Box -->
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>
61                 </div>
62             </div>
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">
70                         <thead>
71                             <tr>
72                                 <th>Event</th>
73                                 <th>Value</th>
74                                 <th>Notes</th>
75                                 <th>Timestamp</th>
76                             </tr>
77                         </thead>
78                         <tbody>
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">&nbsp;</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>
85                             </tr>
86 % }
87                         </tbody>
88                     </table>
89                 </div>
90             </div>
91 % }
93         </form>
94     </div>
95 </&>
98 <!-- MAINTENANCE EVENTS -->
99 <&| /page/info_section.mas, title=>"Maintenance Events",  collapsible=>1, collapsed=>0, subtitle=>"" &>
100     <div id="seedlot_event_container"></div>
101 </&>
104 <!-- EVENT INFORMATION -->
105 <&| /page/info_section.mas, title=>"Username/Timestamp",  collapsible=>1, collapsed=>0, subtitle=>"" &>
106     <br /><br />
107     <div class="well seedlot_event_well">
108         <form class="form-horizontal">
110             <!-- Operator -->
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 %>">
115                 </div>
116             </div>
118             <!-- Timestamp -->
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="">
123                 </div>
124             </div>
126         </form>
127     </div>
128     <br /><br />
129 </&>
132 <!-- Submit Button -->
133 <div class="center">
134     <button id="seedlot_event_submit" class="btn btn-primary btn-block" style="max-width: 600px; margin: auto" disabled>Submit</button>
135 </div>
136 <br /><br />
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>
146         </div>
147     </div>
148 </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
161         parseArgs();
163         // Display the maintenance events, set intitial timestamp
164         resetUI();
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() {
172             MESSAGES = [];
173             jQuery('#seedlot_modal').modal('hide');
174         });
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',
181         });
183     });
186     /**
187      * Warn when leaving the page if there are any pending events
188      */
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?";
192         }
193     });
196     //
197     // SETUP FUNCTIONS
198     //
200     /**
201     * Parse the query parameters
202     * - Use `seedlot_name` to set the Seedlot Name input
203     */
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);
210             getSeedlotInfo();
211         }
212     }
214     /**
215      * Get the details of the Seedlot specified by name
216      * - Update the contents, location, and box fields
217      */
218     function getSeedlotInfo() {
219         let name = jQuery("#seedlot_event_name").val();
220         let contents = "";
221         let location = "";
222         let box = "";
223         SEEDLOT_NAME = undefined;
224         SEEDLOT_ID = undefined;
226         jQuery.ajax({
227             type: 'GET',
228             dataType: 'json',
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("");
238             },
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;
246                             box = sl.box;
247                             SEEDLOT_NAME = name;
248                             SEEDLOT_ID = sl.seedlot_stock_id;
249                             getSeedlotEvents();
250                         }
251                     }
252                 }
253             },
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);
261             }
262         });
264         return false;
265     }
267     /**
268      * Get the recent events for the current Seedlot
269      * - Populate the table of recent event info
270      */
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
281             jQuery.ajax({
282                 type: 'POST',
283                 url: '/ajax/breeders/seedlot/maintenance/search',
284                 data: JSON.stringify({
285                     filters: {
286                         names: [{comp: '=', value: SEEDLOT_NAME}],
287                         types: [{cvterm_id: cvterm}]
288                     }
289                 }),
290                 contentType: "application/json; charset=utf-8",
291                 dataType: "json",
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>");
296                         value.html(e.value);
297                         notes.html(e.notes ? e.notes : "");
298                         timestamp.html(e.timestamp);
299                     }
300                 }
301             });
303         });
304     }
306     /**
307      * Redirect to the Barcode Reader
308      */
309     function scanBarcode() {
310         window.location = "/barcode/read?return=/breeders/seedlot/maintenance/record&param=seedlot_name";
311         return false;
312     }
314     /**
315      * Reset the UI
316      * - reset the Event display
317      * - reset the timestamp
318      */
319     function resetUI() {
320         setEvents();
321         setTimestamp();
322         PENDING_CHANGES = false;
323         jQuery("#seedlot_event_submit").attr('disabled', true);
324     }
326     /**
327      * Build the display of each of the maintenace events and their values
328      */
329     function setEvents() {
330         let html = "";
331         if ( ONTOLOGY ) {
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 : [];
338                 // Category Title
339                 html += "<p class='seedlot_event_category'>" + category.name + "</p>";
341                 // Parse each event
342                 for ( let j = 0; j < events.length; j++ ) {
343                     let event = events[j];
344                     let values = event.children ? event.children : [];
346                     // Event Well
347                     html += "<div class='well seedlot_event_well'>";
349                     // Event Title
350                     html += "<p class='seedlot_event_title'>" + event.name + "</p>";
352                     // Event Definition
353                     if ( event.definition ) {
354                         html += "<p class='seedlot_event_definition'>" + jQuery('<div>').html(event.definition).text() + "</p>";
355                     }
357                     // Container Div
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>";
369                         }
370                     }
372                     // Values: Add input text field
373                     else {
374                         html += "<input type='text' class='form-control seedlot_event_value seedlot_event_value_input' value=''>";
375                     }
377                     // End Values Div
378                     html += "</div>";
380                     // Notes Button
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 + "'>&nbsp;<span class='glyphicon glyphicon-comment'></span>&nbsp;</button>";
383                     html += "</div>";
385                     // End Container Div
386                     html += "</div>";
388                     // Notes Input
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>";
391                     html += "</div>";
393                     // End Event Well
394                     html += "</div>";
395                 }
396             }
397         }
399         jQuery("#seedlot_event_container").html(html);
400     }
402     /**
403      * Change the highlighted button of the selected value
404      */
405     function updateEventValue() {
406         let el = jQuery(this);
407         let group = el.parent('.seedlot_event_values');
408         
409         group.find(".seedlot_event_value_btn").removeClass("btn-primary").addClass("btn-default");
410         el.removeClass("btn-default").addClass("btn-primary");
411         
412         PENDING_CHANGES = true;
413         jQuery("#seedlot_event_submit").attr('disabled', false);
414     }
416     /**
417      * Toggle the display of the event notes
418      */
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');
428     }
430     /**
431      * Set the value of the timestamp input to YYYY-MM-DD HH:MM:SS
432      */
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);
450     }
453     //
454     // SUBMIT EVENTS
455     //
457     /**
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
462      */
463     function submitEvents() {
465         // Check for Seedlot ID
466         if ( !SEEDLOT_ID ) {
467             displayError("Please enter a valid Seedlot name");
468             return;
469         }
471         // Build the event info to submit
472         let events = [];
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" ) {
484                 let event = {
485                     cvterm_id: cvterm_id,
486                     value: value,
487                     notes: notes !== "" ? notes : undefined,
488                     operator: operator !== "" ? operator : undefined,
489                     timestamp: timestamp !== "" ? timestamp : undefined
490                 }
491                 events.push(event);
492             }
493         }
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");
498             return;
499         }
501         // Disable Submit Button
502         jQuery("#seedlot_event_submit").attr('disabled', true);
503         jQuery("#seedlot_event_submit").html("Submitting...");
505         // Submit the events
506         jQuery.ajax({
507             type: "POST",
508             url: "/ajax/breeders/seedlot/" + SEEDLOT_ID + "/maintenance",
509             data: JSON.stringify({events: events}),
510             contentType: "application/json; charset=utf-8",
511             dataType: "json",
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);
516                 }
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>");
519                     return _finish();
520                 }
521                 else {
522                     displayError("The events could not be submitted due to an unknown database error");
523                     return _finish();
524                 }
525             },
526             error: function(msg) {
527                 displayError("The events could not be submitted due to a database error:<br /><br /><pre><code>" + msg + "</code></pre>");
528                 return _finish();
529             }
530         });
532         function _finish(complete) {
533             jQuery("#seedlot_event_submit").attr('disabled', false);
534             jQuery("#seedlot_event_submit").html("Submit");
535             if ( complete ) {
536                 window.scrollTo(0, 0);
537                 resetUI();
538             }
539         }
540     }
542     /**
543      * Display an error message in a bootstrap modal
544      * @param {string} msg Error message to display (can include HTML)
545      */
546     function displayError(msg) {
547         displayModal("Error", "#a94442", msg);
548     }
550     /**
551      * Display a success message in a bootstrap modal
552      * @param {string} msg Success message to display (can include HTML)
553      */
554     function displaySuccess(msg) {
555         displayModal("Success", "#3c763d", msg);
556     }
558     /**
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)
563      */
564     function displayModal(title, color, msg) {
565         let message = {
566             title: title,
567             color: color,
568             msg: msg
569         }
570         MESSAGES.push(message);
572         let html = [];
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>";
576             html.push(_html);
577         }
579         jQuery('#seedlot_modal_body').html(html.join("<hr />"));
580         jQuery('#seedlot_modal').modal({backdrop: 'static', keyboard: false});
581     }
583 </script>
585 <style>
586     .seedlot_event_info {
587         padding-top: 7px;
588         margin: 0;
589     }
590     .seedlot_event_well {
591         max-width: 800px;
592         margin: 0 auto 25px auto;
593     }
594     .seedlot_event_category {
595         font-weight: 700;
596         font-size: 120%;
597         margin: 25px 0 10px 0;
598         border-bottom: 1px solid #ccc;
599     }
600     .seedlot_event_title {
601         font-weight: 500;
602         font-size: 110%;
603         margin: 0;
604     }
605     .seedlot_event_definition {
606         font-size: 90%;
607         color: #999;
608         margin: 0;
609     }
610     .seedlot_event_values_div {
611         display: flex;
612         margin-top: 10px;
613     }
614     .seedlot_event_values {
615         flex-grow: 1;
616     }
617     .seedlot_event_notes_btn_div {
618         margin-left: 10px;
619     }
620     .seedlot_event_notes_input_div {
621         margin-top: 10px;
622         display: none;
623     }
624     .seedlot_event_value_btn {
625         min-width: 80px;
626     }
627     .center {
628         margin: 0 auto;
629     }
630 </style>