Bug 30708: Rebase - Use name instead of url for router-links
[koha.git] / koha-tmpl / intranet-tmpl / prog / js / ill-batch-modal.js
1 (function () {
2     // Bail if there aren't any metadata enrichment plugins installed
3     if (typeof metadata_enrichment_services === 'undefined') {
4         console.log('No metadata enrichment plugins found.')
5         return;
6     }
7
8     window.addEventListener('load', onload);
9
10     // Delay between API requests
11     var debounceDelay = 1000;
12
13     // Elements we work frequently with
14     var textarea = document.getElementById("identifiers_input");
15     var nameInput = document.getElementById("name");
16     var cardnumberInput = document.getElementById("batchcardnumber");
17     var branchcodeSelect = document.getElementById("branchcode");
18     var processButton = document.getElementById("process_button");
19     var createButton = document.getElementById("button_create_batch");
20     var finishButton = document.getElementById("button_finish");
21     var batchItemsDisplay = document.getElementById("add_batch_items");
22     var createProgressTotal = document.getElementById("processed_total");
23     var createProgressCount = document.getElementById("processed_count");
24     var createProgressFailed = document.getElementById("processed_failed");
25     var createProgressBar = document.getElementById("processed_progress_bar");
26     var identifierTable = document.getElementById('identifier-table');
27     var createRequestsButton = document.getElementById('create-requests-button');
28     var statusesSelect = document.getElementById('status_code');
29     var cancelButton = document.getElementById('lhs').querySelector('button');
30     var cancelButtonOriginalText = cancelButton.innerHTML;
31
32     // We need a data structure keyed on identifier type, which tells us how to parse that
33     // identifier type and what services can get its metadata. We receive an array of
34     // available services
35     var supportedIdentifiers = {};
36     metadata_enrichment_services.forEach(function (service) {
37         // Iterate the identifiers that this service supports
38         Object.keys(service.identifiers_supported).forEach(function (idType) {
39             if (!supportedIdentifiers[idType]) {
40                 supportedIdentifiers[idType] = [];
41             }
42             supportedIdentifiers[idType].push(service);
43         });
44     });
45
46     // An object for when we're creating a new batch
47     var emptyBatch = {
48         name: '',
49         backend: null,
50         cardnumber: '',
51         branchcode: '',
52         status_code: 'NEW'
53     };
54
55     // The object that holds the batch we're working with
56     // It's a proxy so we can update portions of the UI
57     // upon changes
58     var batch = new Proxy(
59         { data: {} },
60         {
61             get: function (obj, prop) {
62                 return obj[prop];
63             },
64             set: function (obj, prop, value) {
65                 obj[prop] = value;
66                 manageBatchItemsDisplay();
67                 updateBatchInputs();
68                 disableCardnumberInput();
69                 displayPatronName();
70                 updateStatusesSelect();
71             }
72         }
73     );
74
75     // The object that holds the contents of the table
76     // It's a proxy so we can make it automatically redraw the
77     // table upon changes
78     var tableContent = new Proxy(
79         { data: [] },
80         {
81             get: function (obj, prop) {
82                 return obj[prop];
83             },
84             set: function (obj, prop, value) {
85                 obj[prop] = value;
86                 updateTable();
87                 updateRowCount();
88                 updateProcessTotals();
89                 checkAvailability();
90             }
91         }
92     );
93
94     // The object that holds the contents of the table
95     // It's a proxy so we can update portions of the UI
96     // upon changes
97     var statuses = new Proxy(
98         { data: [] },
99         {
100             get: function (obj, prop) {
101                 return obj[prop];
102             },
103             set: function (obj, prop, value) {
104                 obj[prop] = value;
105                 updateStatusesSelect();
106             }
107         }
108     );
109
110     var progressTotals = new Proxy(
111         {
112             data: {}
113         },
114         {
115             get: function (obj, prop) {
116                 return obj[prop];
117             },
118             set: function (obj, prop, value) {
119                 obj[prop] = value;
120                 showCreateRequestsButton();
121             }
122         }
123     );
124
125     // Keep track of submission API calls that are in progress
126     // so we don't duplicate them
127     var submissionSent = {};
128
129     // Keep track of availability API calls that are in progress
130     // so we don't duplicate them
131     var availabilitySent = {};
132
133     // Are we updating an existing batch
134     var isUpdate = false;
135
136     // The datatable
137     var table;
138     var tableEl = document.getElementById('identifier-table');
139
140     // The element that potentially holds the ID of the batch
141     // we're working with
142     var idEl = document.getElementById('ill-batch-details');
143     var batchId = null;
144     var backend = null;
145
146     function onload() {
147         $('#ill-batch-modal').on('shown.bs.modal', function () {
148             init();
149             patronAutocomplete();
150             batchInputsEventListeners();
151             createButtonEventListener();
152             createRequestsButtonEventListener();
153             moreLessEventListener();
154             removeRowEventListener();
155         });
156         $('#ill-batch-modal').on('hidden.bs.modal', function () {
157             // Reset our state when we close the modal
158             // TODO: need to also reset progress bar and already processed identifiers
159             delete idEl.dataset.batchId;
160             delete idEl.dataset.backend;
161             batchId = null;
162             tableEl.style.display = 'none';
163             tableContent.data = [];
164             progressTotals.data = {
165                 total: 0,
166                 count: 0,
167                 failed: 0
168             };
169             textarea.value = '';
170             batch.data = {};
171             cancelButton.innerHTML = cancelButtonOriginalText;
172             // Remove event listeners we created
173             removeEventListeners();
174         });
175     };
176
177     function init() {
178         batchId = idEl.dataset.batchId;
179         backend = idEl.dataset.backend;
180         emptyBatch.backend = backend;
181         progressTotals.data = {
182             total: 0,
183             count: 0,
184             failed: 0
185         };
186         if (batchId) {
187             fetchBatch();
188             isUpdate = true;
189             setModalHeading();
190         } else {
191             batch.data = emptyBatch;
192             setModalHeading();
193         }
194         fetchStatuses();
195         finishButtonEventListener();
196         processButtonEventListener();
197         identifierTextareaEventListener();
198         displaySupportedIdentifiers();
199         createButtonEventListener();
200         updateRowCount();
201     };
202
203     function initPostCreate() {
204         disableCreateButton();
205         cancelButton.innerHTML = ill_batch_create_cancel_button;
206     };
207
208     function setFinishButton() {
209         if (batch.data.patron) {
210             finishButton.removeAttribute('disabled');
211         }
212     };
213
214     function setModalHeading() {
215         var heading = document.getElementById('ill-batch-modal-label');
216         heading.textContent = isUpdate ? ill_batch_update : ill_batch_add;
217     }
218
219     // Identify items that have metadata and therefore can have a local request
220     // created, and do so
221     function requestRequestable() {
222         createRequestsButton.setAttribute('disabled', true);
223         createRequestsButton.setAttribute('aria-disabled', true);
224         setFinishButton();
225         var toCheck = tableContent.data;
226         toCheck.forEach(function (row) {
227             if (
228                 !row.requestId &&
229                 Object.keys(row.metadata).length > 0 &&
230                 !submissionSent[row.value]
231             ) {
232                 submissionSent[row.value] = 1;
233                 makeLocalSubmission(row.value, row.metadata);
234             }
235         });
236     };
237
238     // Identify items that can have their availability checked, and do it
239     function checkAvailability() {
240         // Only proceed if we've got services that can check availability
241         if (!batch_availability_services || batch_availability_services.length === 0) return;
242         var toCheck = tableContent.data;
243         toCheck.forEach(function (row) {
244             if (
245                 !row.url &&
246                 Object.keys(row.metadata).length > 0 &&
247                 !availabilitySent[row.value]
248             ) {
249                 availabilitySent[row.value] = 1;
250                 getAvailability(row.value, row.metadata);
251             }
252         });
253     };
254
255     // Check availability services for immediate availability, if found,
256     // create a link in the table linking to the item
257     function getAvailability(identifier, metadata) {
258         // Prep the metadata for passing to the availability plugins
259         let availability_object = {};
260         if (metadata.issn) availability_object['issn'] = metadata.issn;
261         if (metadata.doi) availability_object['doi'] = metadata.doi;
262         if (metadata.pubmedid) availability_object['pubmedid'] = metadata.pubmedid;
263         var prepped = encodeURIComponent(base64EncodeUnicode(JSON.stringify(availability_object)));
264         for (i = 0; i < batch_availability_services.length; i++) {
265             var service = batch_availability_services[i];
266             window.doApiRequest(
267                 service.endpoint + prepped
268             )
269                 .then(function (response) {
270                     return response.json();
271                 })
272                 .then(function (data) {
273                     if (data.results.search_results && data.results.search_results.length > 0) {
274                         var result = data.results.search_results[0];
275                         tableContent.data = tableContent.data.map(function (row) {
276                             if (row.value === identifier) {
277                                 row.url = result.url;
278                                 row.availabilitySupplier = service.name;
279                             }
280                             return row;
281                         });
282                     }
283                 });
284         }
285     };
286
287     // Help btoa with > 8 bit strings
288     // Shamelessly grabbed from: https://www.base64encoder.io/javascript/
289     function base64EncodeUnicode(str) {
290         // First we escape the string using encodeURIComponent to get the UTF-8 encoding of the characters,
291         // then we convert the percent encodings into raw bytes, and finally feed it to btoa() function.
292         utf8Bytes = encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
293                 return String.fromCharCode('0x' + p1);
294         });
295
296         return btoa(utf8Bytes);
297     };
298
299     // Create a local submission and update our local state
300     // upon success
301     function makeLocalSubmission(identifier, metadata) {
302
303         // Prepare extended_attributes in array format for POST
304         var extended_attributes = [];
305         for (const [key, value] of Object.entries(metadata)) {
306             extended_attributes.push({"type":key, "value":value});
307         }
308
309         var payload = {
310             ill_batch_id: batchId,
311             ill_backend_id: batch.data.backend,
312             patron_id: batch.data.patron.patron_id,
313             library_id: batch.data.library_id,
314             extended_attributes: extended_attributes
315         };
316         window.doCreateSubmission(payload)
317             .then(function (response) {
318                 return response.json();
319             })
320             .then(function (data) {
321                 tableContent.data = tableContent.data.map(function (row) {
322                     if (row.value === identifier) {
323                         row.requestId = data.ill_request_id;
324                         row.requestStatus = data.status;
325                     }
326                     return row;
327                 });
328             })
329             .catch(function () {
330                 window.handleApiError(ill_batch_api_request_fail);
331             });
332     };
333
334     function updateProcessTotals() {
335         var init = {
336             total: 0,
337             count: 0,
338             failed: 0
339         };
340         progressTotals.data = init;
341         var toUpdate = progressTotals.data;
342         tableContent.data.forEach(function (row) {
343             toUpdate.total++;
344             if (Object.keys(row.metadata).length > 0 || row.failed.length > 0) {
345                 toUpdate.count++;
346             }
347             if (Object.keys(row.failed).length > 0) {
348                 toUpdate.failed++;
349             }
350         });
351         createProgressTotal.innerHTML = toUpdate.total;
352         createProgressCount.innerHTML = toUpdate.count;
353         createProgressFailed.innerHTML = toUpdate.failed;
354         var percentDone = Math.ceil((toUpdate.count / toUpdate.total) * 100);
355         createProgressBar.setAttribute('aria-valuenow', percentDone);
356         createProgressBar.innerHTML = percentDone + '%';
357         createProgressBar.style.width = percentDone + '%';
358         progressTotals.data = toUpdate;
359     };
360
361     function displayPatronName() {
362         var span = document.getElementById('patron_link');
363         if (batch.data.patron) {
364             var link = createPatronLink();
365             span.appendChild(link);
366         } else {
367             if (span.children.length > 0) {
368                 span.removeChild(span.firstChild);
369             }
370         }
371     };
372
373     function updateStatusesSelect() {
374         while (statusesSelect.options.length > 0) {
375             statusesSelect.remove(0);
376         }
377         statuses.data.forEach(function (status) {
378             var option = document.createElement('option')
379             option.value = status.code;
380             option.text = status.name;
381             if (batch.data.ill_batch_id && batch.data.status_code === status.code) {
382                 option.selected = true;
383             }
384             statusesSelect.add(option);
385         });
386         if (isUpdate) {
387             statusesSelect.parentElement.style.display = 'block';
388         }
389     };
390
391     function removeEventListeners() {
392         textarea.removeEventListener('paste', processButtonState);
393         textarea.removeEventListener('keyup', processButtonState);
394         processButton.removeEventListener('click', processIdentifiers);
395         nameInput.removeEventListener('keyup', createButtonState);
396         cardnumberInput.removeEventListener('keyup', createButtonState);
397         branchcodeSelect.removeEventListener('change', createButtonState);
398         createButton.removeEventListener('click', createBatch);
399         identifierTable.removeEventListener('click', toggleMetadata);
400         identifierTable.removeEventListener('click', removeRow);
401         createRequestsButton.remove('click', requestRequestable);
402     };
403
404     function finishButtonEventListener() {
405         finishButton.addEventListener('click', doFinish);
406     };
407
408     function identifierTextareaEventListener() {
409         textarea.addEventListener('paste', textareaUpdate);
410         textarea.addEventListener('keyup', textareaUpdate);
411     };
412
413     function processButtonEventListener() {
414         processButton.addEventListener('click', processIdentifiers);
415     };
416
417     function createRequestsButtonEventListener() {
418         createRequestsButton.addEventListener('click', requestRequestable);
419     };
420
421     function createButtonEventListener() {
422         createButton.addEventListener('click', createBatch);
423     };
424
425     function batchInputsEventListeners() {
426         nameInput.addEventListener('keyup', createButtonState);
427         cardnumberInput.addEventListener('keyup', createButtonState);
428         branchcodeSelect.addEventListener('change', createButtonState);
429     };
430
431     function moreLessEventListener() {
432         identifierTable.addEventListener('click', toggleMetadata);
433     };
434
435     function removeRowEventListener() {
436         identifierTable.addEventListener('click', removeRow);
437     };
438
439     function textareaUpdate() {
440         processButtonState();
441         updateRowCount();
442     };
443
444     function processButtonState() {
445         if (textarea.value.length > 0) {
446             processButton.removeAttribute('disabled');
447             processButton.removeAttribute('aria-disabled');
448         } else {
449             processButton.setAttribute('disabled', true);
450             processButton.setAttribute('aria-disabled', true);
451         }
452     };
453
454     function disableCardnumberInput() {
455         if (batch.data.patron) {
456             cardnumberInput.setAttribute('disabled', true);
457             cardnumberInput.setAttribute('aria-disabled', true);
458         } else {
459             cardnumberInput.removeAttribute('disabled');
460             cardnumberInput.removeAttribute('aria-disabled');
461         }
462     };
463
464     function createButtonState() {
465         if (
466             nameInput.value.length > 0 &&
467             cardnumberInput.value.length > 0 &&
468             branchcodeSelect.selectedOptions.length === 1
469         ) {
470             createButton.removeAttribute('disabled');
471             createButton.setAttribute('display', 'inline-block');
472         } else {
473             createButton.setAttribute('disabled', 1);
474             createButton.setAttribute('display', 'none');
475         }
476     };
477
478     function doFinish() {
479         updateBatch()
480             .then(function () {
481                 $('#ill-batch-modal').modal({ show: false });
482                 location.href = '/cgi-bin/koha/ill/ill-requests.pl?batch_id=' + batch.data.ill_batch_id;
483             });
484     };
485
486     // Get all batch statuses
487     function fetchStatuses() {
488         window.doApiRequest('/api/v1/ill/batchstatuses')
489             .then(function (response) {
490                 return response.json();
491             })
492             .then(function (jsoned) {
493                 statuses.data = jsoned;
494             })
495             .catch(function (e) {
496                 window.handleApiError(ill_batch_statuses_api_fail);
497             });
498     };
499
500     // Get the batch
501     function fetchBatch() {
502         window.doBatchApiRequest("/" + batchId, {
503                 headers: {
504                     'x-koha-embed': 'patron'
505                 }
506             })
507             .then(function (response) {
508                 return response.json();
509             })
510             .then(function (jsoned) {
511                 batch.data = {
512                     ill_batch_id: jsoned.ill_batch_id,
513                     name: jsoned.name,
514                     backend: jsoned.backend,
515                     cardnumber: jsoned.cardnumber,
516                     library_id: jsoned.library_id,
517                     status_code: jsoned.status_code
518                 }
519                 return jsoned;
520             })
521             .then(function (data) {
522                 batch.data = data;
523             })
524             .catch(function () {
525                 window.handleApiError(ill_batch_api_fail);
526             });
527     };
528
529     function createBatch() {
530         var selectedBranchcode = branchcodeSelect.selectedOptions[0].value;
531         var selectedStatuscode = statusesSelect.selectedOptions[0].value;
532         return doBatchApiRequest('', {
533             method: 'POST',
534             headers: {
535                 'Content-type': 'application/json',
536                 'x-koha-embed': 'patron'
537             },
538             body: JSON.stringify({
539                 name: nameInput.value,
540                 backend: backend,
541                 cardnumber: cardnumberInput.value,
542                 library_id: selectedBranchcode,
543                 status_code: selectedStatuscode
544             })
545         })
546             .then(function (response) {
547                 if ( response.ok ) {
548                     return response.json();
549                 }
550                 return Promise.reject(response);
551             })
552             .then(function (body) {
553                 batchId = body.ill_batch_id;
554                 batch.data = {
555                     ill_batch_id: body.ill_batch_id,
556                     name: body.name,
557                     backend: body.backend,
558                     cardnumber: body.patron.cardnumber,
559                     library_id: body.library_id,
560                     status_code: body.status_code,
561                     patron: body.patron,
562                     status: body.status
563                 };
564                 initPostCreate();
565             })
566             .catch(function (response) {
567                 response.json().then((json) => {
568                     if( json.error ) {
569                         handleApiError(json.error);
570                     } else {
571                         handleApiError(ill_batch_create_api_fail);
572                     }
573                 })
574             });
575     };
576
577     function updateBatch() {
578         var selectedBranchcode = branchcodeSelect.selectedOptions[0].value;
579         var selectedStatuscode = statusesSelect.selectedOptions[0].value;
580
581         return doBatchApiRequest('/' + batch.data.ill_batch_id, {
582             method: 'PUT',
583             headers: {
584                 'Content-type': 'application/json'
585             },
586             body: JSON.stringify({
587                 name: nameInput.value,
588                 backend: batch.data.backend,
589                 cardnumber: batch.data.patron.cardnumber,
590                 library_id: selectedBranchcode,
591                 status_code: selectedStatuscode
592             })
593         })
594             .catch(function () {
595                 handleApiError(ill_batch_update_api_fail);
596             });
597     };
598
599     function displaySupportedIdentifiers() {
600         var names = Object.keys(supportedIdentifiers).map(function (identifier) {
601             return window['ill_batch_' + identifier];
602         });
603         var displayEl = document.getElementById('supported_identifiers');
604         displayEl.textContent = names.length > 0 ? names.join(', ') : ill_batch_none;
605     }
606
607     function updateRowCount() {
608         var textEl = document.getElementById('row_count_value');
609         var val = textarea.value.trim();
610         var cnt = 0;
611         if (val.length > 0) {
612             cnt = val.split(/\n/).length;
613         }
614         textEl.textContent = cnt;
615     }
616
617     function showProgress() {
618         var el = document.getElementById('create-progress');
619         el.style.display = 'block';
620     }
621
622     function showCreateRequestsButton() {
623         var data = progressTotals.data;
624         var el = document.getElementById('create-requests');
625         el.style.display = (data.total > 0 && data.count === data.total) ? 'flex' : 'none';
626     }
627
628     async function processIdentifiers() {
629         var content = textarea.value;
630         hideErrors();
631         if (content.length === 0) return;
632
633         disableProcessButton();
634         var label = document.getElementById('progress-label').firstChild;
635         label.innerHTML = ill_batch_retrieving_metadata;
636         showProgress();
637
638         // Errors encountered when processing
639         var processErrors = {};
640
641         // Prepare the content, including trimming each row
642         var contentArr = content.split(/\n/);
643         var trimmed = contentArr.map(function (row) {
644             return row.trim();
645         });
646
647         var parsed = [];
648
649         trimmed.forEach(function (identifier) {
650             var match = identifyIdentifier(identifier);
651             // If this identifier is not identifiable or
652             // looks like more than one type, we can't be sure
653             // what it is
654             if (match.length != 1) {
655                 parsed.push({
656                     type: 'unknown',
657                     value: identifier
658                 });
659             } else {
660                 parsed.push(match[0]);
661             }
662         });
663
664         var unknownIdentifiers = parsed
665             .filter(function (parse) {
666                 if (parse.type == 'unknown') {
667                     return parse;
668                 }
669             })
670             .map(function (filtered) {
671                 return filtered.value;
672             });
673
674         if (unknownIdentifiers.length > 0) {
675             processErrors.badidentifiers = {
676                 element: 'badids',
677                 values: unknownIdentifiers.join(', ')
678             };
679         };
680
681         // Deduping
682         var deduped = [];
683         var dupes = {};
684         parsed.forEach(function (row) {
685             var value = row.value;
686             var alreadyInDeduped = deduped.filter(function (d) {
687                 return d.value === value;
688             });
689             if (alreadyInDeduped.length > 0 && !dupes[value]) {
690                 dupes[value] = 1;
691             } else if (alreadyInDeduped.length === 0) {
692                 row.metadata = {};
693                 row.failed = {};
694                 row.requestId = null;
695                 deduped.push(row);
696             }
697         });
698         // Update duplicate error if dupes were found
699         if (Object.keys(dupes).length > 0) {
700             processErrors.duplicates = {
701                 element: 'dupelist',
702                 values: Object.keys(dupes).join(', ')
703             };
704         }
705
706         // Display any errors
707         displayErrors(processErrors);
708
709         // Now build and display the table
710         if (!table) {
711             buildTable();
712         }
713
714         // We may be appending new values to an existing table,
715         // in which case, ensure we don't create duplicates
716         var tabIdentifiers = tableContent.data.map(function (tabId) {
717             return tabId.value;
718         });
719         var notInTable = deduped.filter(function (ded) {
720             if (!tabIdentifiers.includes(ded.value)) {
721                 return ded;
722            }
723         });
724         if (notInTable.length > 0) {
725             tableContent.data = tableContent.data.concat(notInTable);
726         }
727
728         // Populate metadata for those records that need it
729         var newData = tableContent.data;
730         for (var i = 0; i < tableContent.data.length; i++) {
731             var row = tableContent.data[i];
732             // Skip rows that don't need populating
733             if (
734                 Object.keys(tableContent.data[i].metadata).length > 0 ||
735                 Object.keys(tableContent.data[i].failed).length > 0
736             ) continue;
737             var identifier = { type: row.type, value: row.value };
738             try {
739                 var populated = await populateMetadata(identifier);
740                 row.metadata = populated.results.result || {};
741             } catch (e) {
742                 row.failed = ill_populate_failed;
743             }
744             newData[i] = row;
745             tableContent.data = newData;
746         }
747     }
748
749     function disableProcessButton() {
750         processButton.setAttribute('disabled', true);
751         processButton.setAttribute('aria-disabled', true);
752     }
753
754     function disableCreateButton() {
755         createButton.setAttribute('disabled', true);
756         createButton.setAttribute('aria-disabled', true);
757     }
758
759     async function populateMetadata(identifier) {
760         // All services that support this identifier type
761         var services = supportedIdentifiers[identifier.type];
762         // Check each service and use the first results we get, if any
763         for (var i = 0; i < services.length; i++) {
764             var service = services[i];
765             var endpoint = '/api/v1/contrib/' + service.api_namespace + service.search_endpoint + '?' + identifier.type + '=' + identifier.value;
766             var metadata = await getMetadata(endpoint);
767             if (metadata.errors.length === 0) {
768                 var parsed = await parseMetadata(metadata, service);
769                 if (parsed.errors.length > 0) {
770                     throw Error(metadata.errors.map(function (error) {
771                         return error.message;
772                     }).join(', '));
773                 }
774                 return parsed;
775             }
776         }
777     };
778
779     async function getMetadata(endpoint) {
780         var response = await debounce(doApiRequest)(endpoint);
781         return response.json();
782     };
783
784     async function parseMetadata(metadata, service) {
785         var endpoint = '/api/v1/contrib/' + service.api_namespace + service.ill_parse_endpoint;
786         var response = await doApiRequest(endpoint, {
787             method: 'POST',
788             headers: {
789                 'Content-type': 'application/json'
790             },
791             body: JSON.stringify(metadata)
792         });
793         return response.json();
794     }
795
796     // A render function for identifier type
797     function createIdentifierType(data) {
798         return window['ill_batch_' + data];
799     };
800
801     // Get an item's title
802     function getTitle(meta) {
803         if (meta.article_title && meta.article_title.length > 0) {
804             return 'article_title';
805             return {
806                 prop: 'article_title',
807                 value: meta.article_title
808             };
809         } else if (meta.title && meta.title.length > 0) {
810             return 'title';
811             return {
812                 prop: 'title',
813                 value: meta.title
814             };
815         }
816     };
817
818     // Create a metadata row
819     function createMetadataRow(data, meta, prop) {
820         if (!meta[prop]) return;
821
822         var div = document.createElement('div');
823         div.classList.add('metadata-row');
824         var label = document.createElement('span');
825         label.classList.add('metadata-label');
826         label.innerText = ill_batch_metadata[prop] + ': ';
827
828         // Add a link to the availability URL if appropriate
829         var value;
830         if (!data.url) {
831             value = document.createElement('span');
832         } else {
833             value = document.createElement('a');
834             value.setAttribute('href', data.url);
835             value.setAttribute('target', '_blank');
836             value.setAttribute('title', ill_batch_available_via + ' ' + data.availabilitySupplier);
837         }
838         value.classList.add('metadata-value');
839         value.innerText = meta[prop];
840         div.appendChild(label);
841         div.appendChild(value);
842
843         return div;
844     }
845
846     // A render function for displaying metadata
847     function createMetadata(x, y, data) {
848         // If the fetch failed
849         if (data.failed.length > 0) {
850             return data.failed;
851         }
852
853         // If we've not yet got any metadata back
854         if (Object.keys(data.metadata).length === 0) {
855             return ill_populate_waiting;
856         }
857
858         var core = ['doi', 'pmid', 'issn', 'title', 'year', 'issue', 'pages', 'publisher', 'article_title', 'article_author', 'volume'];
859         var meta = data.metadata;
860
861         var container = document.createElement('div');
862         container.classList.add('metadata-container');
863
864         // Create the title row
865         var title = getTitle(meta);
866         if (title) {
867             // Remove the title element from the props
868             // we're about to iterate
869             core = core.filter(function (i) {
870                 return i !== title;
871             });
872             var titleRow = createMetadataRow(data, meta, title);
873             container.appendChild(titleRow);
874         }
875
876         var remainder = document.createElement('div');
877         remainder.classList.add('metadata-remainder');
878         remainder.style.display = 'none';
879         // Create the remaining rows
880         core.sort().forEach(function (prop) {
881             var div = createMetadataRow(data, meta, prop);
882             if (div) {
883                 remainder.appendChild(div);
884             }
885         });
886         container.appendChild(remainder);
887
888         // Add a more/less toggle
889         var firstField = container.firstChild;
890         var moreLess = document.createElement('div');
891         moreLess.classList.add('more-less');
892         var moreLessLink = document.createElement('a');
893         moreLessLink.setAttribute('href', '#');
894         moreLessLink.classList.add('more-less-link');
895         moreLessLink.innerText = ' [' + ill_batch_metadata_more + ']';
896         moreLess.appendChild(moreLessLink);
897         firstField.appendChild(moreLess);
898
899         return container.outerHTML;
900     };
901
902     function removeRow(ev) {
903         if (ev.target.className.includes('remove-row')) {
904             if (!confirm(ill_batch_item_remove)) return;
905             // Find the parent row
906             var ancestor = ev.target.closest('tr');
907             var identifier = ancestor.querySelector('.identifier').innerText;
908             tableContent.data = tableContent.data.filter(function (row) {
909                 return row.value !== identifier;
910             });
911         }
912     }
913
914     function toggleMetadata(ev) {
915         if (ev.target.className === 'more-less-link') {
916             // Find the element we need to show
917             var ancestor = ev.target.closest('.metadata-container');
918             var meta = ancestor.querySelector('.metadata-remainder');
919
920             // Display or hide based on its current state
921             var display = window.getComputedStyle(meta).display;
922
923             meta.style.display = display === 'block' ? 'none' : 'block';
924
925             // Update the More / Less text
926             ev.target.innerText = ' [ ' + (display === 'none' ? ill_batch_metadata_less : ill_batch_metadata_more) + ' ]';
927         }
928     }
929
930     // A render function for the link to a request ID
931     function createRequestId(x, y, data) {
932         return data.requestId || '-';
933     }
934
935     function createRequestStatus(x, y, data) {
936         return data.requestStatus || '-';
937     }
938
939     function buildTable(identifiers) {
940         table = KohaTable('identifier-table', {
941             processing: true,
942             deferRender: true,
943             ordering: false,
944             paging: false,
945             searching: false,
946             autoWidth: false,
947             columns: [
948                 {
949                     data: 'type',
950                     width: '13%',
951                     render: createIdentifierType
952                 },
953                 {
954                     data: 'value',
955                     width: '25%',
956                     className: 'identifier'
957                 },
958                 {
959                     data: 'metadata',
960                     render: createMetadata
961                 },
962                 {
963                     data: 'requestId',
964                     width: '6.5%',
965                     render: createRequestId
966                 },
967                 {
968                     data: 'requestStatus',
969                     width: '6.5%',
970                     render: createRequestStatus
971                 },
972                 {
973                     width: '18%',
974                     render: createActions,
975                     className: 'action-column noExport'
976                 }
977             ],
978             createdRow: function (row, data) {
979                 if (data.failed.length > 0) {
980                     row.classList.add('fetch-failed');
981                 }
982             }
983         });
984     }
985
986     function createActions(x, y, data) {
987         return '<button type="button" aria-label='+ ill_button_remove + (data.requestId ? ' disabled aria-disabled="true"' : '') + ' class="btn btn-xs btn-danger remove-row">' + ill_button_remove + '</button>';
988     }
989
990     // Redraw the table
991     function updateTable() {
992         if (!table) return;
993         tableEl.style.display = tableContent.data.length > 0 ? 'table' : 'none';
994         tableEl.style.width = '100%';
995         table.api()
996             .clear()
997             .rows.add(tableContent.data)
998             .draw();
999     };
1000
1001     function identifyIdentifier(identifier) {
1002         var matches = [];
1003
1004         // Iterate our available services to see if any can identify this identifier
1005         Object.keys(supportedIdentifiers).forEach(function (identifierType) {
1006             // Since all the services supporting this identifier type should use the same
1007             // regex to identify it, we can just use the first
1008             var service = supportedIdentifiers[identifierType][0];
1009             var regex = new RegExp(service.identifiers_supported[identifierType].regex);
1010             var match = identifier.match(regex);
1011             if (match && match.groups && match.groups.identifier) {
1012                 matches.push({
1013                     type: identifierType,
1014                     value: match.groups.identifier
1015                 });
1016             }
1017         });
1018         return matches;
1019     }
1020
1021     function displayErrors(errors) {
1022         var keys = Object.keys(errors);
1023         if (keys.length > 0) {
1024             keys.forEach(function (key) {
1025                 var el = document.getElementById(errors[key].element);
1026                 el.textContent = errors[key].values;
1027                 el.style.display = 'inline';
1028                 var container = document.getElementById(key);
1029                 container.style.display = 'block';
1030             });
1031             var el = document.getElementById('textarea-errors');
1032             el.style.display = 'flex';
1033         }
1034     }
1035
1036     function hideErrors() {
1037         var dupelist = document.getElementById('dupelist');
1038         var badids = document.getElementById('badids');
1039         dupelist.textContent = '';
1040         dupelist.parentElement.style.display = 'none';
1041         badids.textContent = '';
1042         badids.parentElement.style.display = 'none';
1043         var tae = document.getElementById('textarea-errors');
1044         tae.style.display = 'none';
1045     }
1046
1047     function manageBatchItemsDisplay() {
1048         batchItemsDisplay.style.display = batch.data.ill_batch_id ? 'block' : 'none'
1049     };
1050
1051     function updateBatchInputs() {
1052         nameInput.value = batch.data.name || '';
1053         cardnumberInput.value = batch.data.cardnumber || '';
1054         branchcodeSelect.value = batch.data.library_id || '';
1055     }
1056
1057     function debounce(func) {
1058         var timeout;
1059         return function (...args) {
1060             return new Promise(function (resolve) {
1061                 if (timeout) {
1062                     clearTimeout(timeout);
1063                 }
1064                 timeout = setTimeout(function () {
1065                     return resolve(func(...args));
1066                 }, debounceDelay);
1067             });
1068         }
1069     }
1070
1071     function patronAutocomplete() {
1072         patron_autocomplete(
1073             $('#batch-form #batchcardnumber'),
1074             {
1075               'on-select-callback': function( event, ui ) {
1076                 $("#batch-form #batchcardnumber").val( ui.item.cardnumber );
1077                 return false;
1078               }
1079             }
1080           );
1081     };
1082
1083     function createPatronLink() {
1084         if (!batch.data.patron) return;
1085         var patron = batch.data.patron;
1086         var a = document.createElement('a');
1087         var href = '/cgi-bin/koha/members/moremember.pl?borrowernumber=' + patron.borrowernumber;
1088         var text = patron.surname + ' (' + patron.cardnumber + ')';
1089         a.setAttribute('title', ill_borrower_details);
1090         a.setAttribute('href', href);
1091         a.textContent = text;
1092         return a;
1093     };
1094
1095 })();