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.')
8 window.addEventListener('load', onload);
10 // Delay between API requests
11 var debounceDelay = 1000;
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;
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
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] = [];
42 supportedIdentifiers[idType].push(service);
46 // An object for when we're creating a new batch
55 // The object that holds the batch we're working with
56 // It's a proxy so we can update portions of the UI
58 var batch = new Proxy(
61 get: function (obj, prop) {
64 set: function (obj, prop, value) {
66 manageBatchItemsDisplay();
68 disableCardnumberInput();
70 updateStatusesSelect();
75 // The object that holds the contents of the table
76 // It's a proxy so we can make it automatically redraw the
78 var tableContent = new Proxy(
81 get: function (obj, prop) {
84 set: function (obj, prop, value) {
88 updateProcessTotals();
94 // The object that holds the contents of the table
95 // It's a proxy so we can update portions of the UI
97 var statuses = new Proxy(
100 get: function (obj, prop) {
103 set: function (obj, prop, value) {
105 updateStatusesSelect();
110 var progressTotals = new Proxy(
115 get: function (obj, prop) {
118 set: function (obj, prop, value) {
120 showCreateRequestsButton();
125 // Keep track of submission API calls that are in progress
126 // so we don't duplicate them
127 var submissionSent = {};
129 // Keep track of availability API calls that are in progress
130 // so we don't duplicate them
131 var availabilitySent = {};
133 // Are we updating an existing batch
134 var isUpdate = false;
138 var tableEl = document.getElementById('identifier-table');
140 // The element that potentially holds the ID of the batch
141 // we're working with
142 var idEl = document.getElementById('ill-batch-details');
147 $('#ill-batch-modal').on('shown.bs.modal', function () {
149 patronAutocomplete();
150 batchInputsEventListeners();
151 createButtonEventListener();
152 createRequestsButtonEventListener();
153 moreLessEventListener();
154 removeRowEventListener();
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;
162 tableEl.style.display = 'none';
163 tableContent.data = [];
164 progressTotals.data = {
171 cancelButton.innerHTML = cancelButtonOriginalText;
172 // Remove event listeners we created
173 removeEventListeners();
178 batchId = idEl.dataset.batchId;
179 backend = idEl.dataset.backend;
180 emptyBatch.backend = backend;
181 progressTotals.data = {
191 batch.data = emptyBatch;
195 finishButtonEventListener();
196 processButtonEventListener();
197 identifierTextareaEventListener();
198 displaySupportedIdentifiers();
199 createButtonEventListener();
203 function initPostCreate() {
204 disableCreateButton();
205 cancelButton.innerHTML = ill_batch_create_cancel_button;
208 function setFinishButton() {
209 if (batch.data.patron) {
210 finishButton.removeAttribute('disabled');
214 function setModalHeading() {
215 var heading = document.getElementById('ill-batch-modal-label');
216 heading.textContent = isUpdate ? ill_batch_update : ill_batch_add;
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);
225 var toCheck = tableContent.data;
226 toCheck.forEach(function (row) {
229 Object.keys(row.metadata).length > 0 &&
230 !submissionSent[row.value]
232 submissionSent[row.value] = 1;
233 makeLocalSubmission(row.value, row.metadata);
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) {
246 Object.keys(row.metadata).length > 0 &&
247 !availabilitySent[row.value]
249 availabilitySent[row.value] = 1;
250 getAvailability(row.value, row.metadata);
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];
267 service.endpoint + prepped
269 .then(function (response) {
270 return response.json();
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;
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);
296 return btoa(utf8Bytes);
299 // Create a local submission and update our local state
301 function makeLocalSubmission(identifier, metadata) {
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});
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
316 window.doCreateSubmission(payload)
317 .then(function (response) {
318 return response.json();
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;
330 window.handleApiError(ill_batch_api_request_fail);
334 function updateProcessTotals() {
340 progressTotals.data = init;
341 var toUpdate = progressTotals.data;
342 tableContent.data.forEach(function (row) {
344 if (Object.keys(row.metadata).length > 0 || row.failed.length > 0) {
347 if (Object.keys(row.failed).length > 0) {
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;
361 function displayPatronName() {
362 var span = document.getElementById('patron_link');
363 if (batch.data.patron) {
364 var link = createPatronLink();
365 span.appendChild(link);
367 if (span.children.length > 0) {
368 span.removeChild(span.firstChild);
373 function updateStatusesSelect() {
374 while (statusesSelect.options.length > 0) {
375 statusesSelect.remove(0);
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;
384 statusesSelect.add(option);
387 statusesSelect.parentElement.style.display = 'block';
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);
404 function finishButtonEventListener() {
405 finishButton.addEventListener('click', doFinish);
408 function identifierTextareaEventListener() {
409 textarea.addEventListener('paste', textareaUpdate);
410 textarea.addEventListener('keyup', textareaUpdate);
413 function processButtonEventListener() {
414 processButton.addEventListener('click', processIdentifiers);
417 function createRequestsButtonEventListener() {
418 createRequestsButton.addEventListener('click', requestRequestable);
421 function createButtonEventListener() {
422 createButton.addEventListener('click', createBatch);
425 function batchInputsEventListeners() {
426 nameInput.addEventListener('keyup', createButtonState);
427 cardnumberInput.addEventListener('keyup', createButtonState);
428 branchcodeSelect.addEventListener('change', createButtonState);
431 function moreLessEventListener() {
432 identifierTable.addEventListener('click', toggleMetadata);
435 function removeRowEventListener() {
436 identifierTable.addEventListener('click', removeRow);
439 function textareaUpdate() {
440 processButtonState();
444 function processButtonState() {
445 if (textarea.value.length > 0) {
446 processButton.removeAttribute('disabled');
447 processButton.removeAttribute('aria-disabled');
449 processButton.setAttribute('disabled', true);
450 processButton.setAttribute('aria-disabled', true);
454 function disableCardnumberInput() {
455 if (batch.data.patron) {
456 cardnumberInput.setAttribute('disabled', true);
457 cardnumberInput.setAttribute('aria-disabled', true);
459 cardnumberInput.removeAttribute('disabled');
460 cardnumberInput.removeAttribute('aria-disabled');
464 function createButtonState() {
466 nameInput.value.length > 0 &&
467 cardnumberInput.value.length > 0 &&
468 branchcodeSelect.selectedOptions.length === 1
470 createButton.removeAttribute('disabled');
471 createButton.setAttribute('display', 'inline-block');
473 createButton.setAttribute('disabled', 1);
474 createButton.setAttribute('display', 'none');
478 function doFinish() {
481 $('#ill-batch-modal').modal({ show: false });
482 location.href = '/cgi-bin/koha/ill/ill-requests.pl?batch_id=' + batch.data.ill_batch_id;
486 // Get all batch statuses
487 function fetchStatuses() {
488 window.doApiRequest('/api/v1/ill/batchstatuses')
489 .then(function (response) {
490 return response.json();
492 .then(function (jsoned) {
493 statuses.data = jsoned;
495 .catch(function (e) {
496 window.handleApiError(ill_batch_statuses_api_fail);
501 function fetchBatch() {
502 window.doBatchApiRequest("/" + batchId, {
504 'x-koha-embed': 'patron'
507 .then(function (response) {
508 return response.json();
510 .then(function (jsoned) {
512 ill_batch_id: jsoned.ill_batch_id,
514 backend: jsoned.backend,
515 cardnumber: jsoned.cardnumber,
516 library_id: jsoned.library_id,
517 status_code: jsoned.status_code
521 .then(function (data) {
525 window.handleApiError(ill_batch_api_fail);
529 function createBatch() {
530 var selectedBranchcode = branchcodeSelect.selectedOptions[0].value;
531 var selectedStatuscode = statusesSelect.selectedOptions[0].value;
532 return doBatchApiRequest('', {
535 'Content-type': 'application/json',
536 'x-koha-embed': 'patron'
538 body: JSON.stringify({
539 name: nameInput.value,
541 cardnumber: cardnumberInput.value,
542 library_id: selectedBranchcode,
543 status_code: selectedStatuscode
546 .then(function (response) {
548 return response.json();
550 return Promise.reject(response);
552 .then(function (body) {
553 batchId = body.ill_batch_id;
555 ill_batch_id: body.ill_batch_id,
557 backend: body.backend,
558 cardnumber: body.patron.cardnumber,
559 library_id: body.library_id,
560 status_code: body.status_code,
566 .catch(function (response) {
567 response.json().then((json) => {
569 handleApiError(json.error);
571 handleApiError(ill_batch_create_api_fail);
577 function updateBatch() {
578 var selectedBranchcode = branchcodeSelect.selectedOptions[0].value;
579 var selectedStatuscode = statusesSelect.selectedOptions[0].value;
581 return doBatchApiRequest('/' + batch.data.ill_batch_id, {
584 'Content-type': 'application/json'
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
595 handleApiError(ill_batch_update_api_fail);
599 function displaySupportedIdentifiers() {
600 var names = Object.keys(supportedIdentifiers).map(function (identifier) {
601 return window['ill_batch_' + identifier];
603 var displayEl = document.getElementById('supported_identifiers');
604 displayEl.textContent = names.length > 0 ? names.join(', ') : ill_batch_none;
607 function updateRowCount() {
608 var textEl = document.getElementById('row_count_value');
609 var val = textarea.value.trim();
611 if (val.length > 0) {
612 cnt = val.split(/\n/).length;
614 textEl.textContent = cnt;
617 function showProgress() {
618 var el = document.getElementById('create-progress');
619 el.style.display = 'block';
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';
628 async function processIdentifiers() {
629 var content = textarea.value;
631 if (content.length === 0) return;
633 disableProcessButton();
634 var label = document.getElementById('progress-label').firstChild;
635 label.innerHTML = ill_batch_retrieving_metadata;
638 // Errors encountered when processing
639 var processErrors = {};
641 // Prepare the content, including trimming each row
642 var contentArr = content.split(/\n/);
643 var trimmed = contentArr.map(function (row) {
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
654 if (match.length != 1) {
660 parsed.push(match[0]);
664 var unknownIdentifiers = parsed
665 .filter(function (parse) {
666 if (parse.type == 'unknown') {
670 .map(function (filtered) {
671 return filtered.value;
674 if (unknownIdentifiers.length > 0) {
675 processErrors.badidentifiers = {
677 values: unknownIdentifiers.join(', ')
684 parsed.forEach(function (row) {
685 var value = row.value;
686 var alreadyInDeduped = deduped.filter(function (d) {
687 return d.value === value;
689 if (alreadyInDeduped.length > 0 && !dupes[value]) {
691 } else if (alreadyInDeduped.length === 0) {
694 row.requestId = null;
698 // Update duplicate error if dupes were found
699 if (Object.keys(dupes).length > 0) {
700 processErrors.duplicates = {
702 values: Object.keys(dupes).join(', ')
706 // Display any errors
707 displayErrors(processErrors);
709 // Now build and display the table
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) {
719 var notInTable = deduped.filter(function (ded) {
720 if (!tabIdentifiers.includes(ded.value)) {
724 if (notInTable.length > 0) {
725 tableContent.data = tableContent.data.concat(notInTable);
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
734 Object.keys(tableContent.data[i].metadata).length > 0 ||
735 Object.keys(tableContent.data[i].failed).length > 0
737 var identifier = { type: row.type, value: row.value };
739 var populated = await populateMetadata(identifier);
740 row.metadata = populated.results.result || {};
742 row.failed = ill_populate_failed;
745 tableContent.data = newData;
749 function disableProcessButton() {
750 processButton.setAttribute('disabled', true);
751 processButton.setAttribute('aria-disabled', true);
754 function disableCreateButton() {
755 createButton.setAttribute('disabled', true);
756 createButton.setAttribute('aria-disabled', true);
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;
779 async function getMetadata(endpoint) {
780 var response = await debounce(doApiRequest)(endpoint);
781 return response.json();
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, {
789 'Content-type': 'application/json'
791 body: JSON.stringify(metadata)
793 return response.json();
796 // A render function for identifier type
797 function createIdentifierType(data) {
798 return window['ill_batch_' + data];
801 // Get an item's title
802 function getTitle(meta) {
803 if (meta.article_title && meta.article_title.length > 0) {
804 return 'article_title';
806 prop: 'article_title',
807 value: meta.article_title
809 } else if (meta.title && meta.title.length > 0) {
818 // Create a metadata row
819 function createMetadataRow(data, meta, prop) {
820 if (!meta[prop]) return;
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] + ': ';
828 // Add a link to the availability URL if appropriate
831 value = document.createElement('span');
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);
838 value.classList.add('metadata-value');
839 value.innerText = meta[prop];
840 div.appendChild(label);
841 div.appendChild(value);
846 // A render function for displaying metadata
847 function createMetadata(x, y, data) {
848 // If the fetch failed
849 if (data.failed.length > 0) {
853 // If we've not yet got any metadata back
854 if (Object.keys(data.metadata).length === 0) {
855 return ill_populate_waiting;
858 var core = ['doi', 'pmid', 'issn', 'title', 'year', 'issue', 'pages', 'publisher', 'article_title', 'article_author', 'volume'];
859 var meta = data.metadata;
861 var container = document.createElement('div');
862 container.classList.add('metadata-container');
864 // Create the title row
865 var title = getTitle(meta);
867 // Remove the title element from the props
868 // we're about to iterate
869 core = core.filter(function (i) {
872 var titleRow = createMetadataRow(data, meta, title);
873 container.appendChild(titleRow);
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);
883 remainder.appendChild(div);
886 container.appendChild(remainder);
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);
899 return container.outerHTML;
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;
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');
920 // Display or hide based on its current state
921 var display = window.getComputedStyle(meta).display;
923 meta.style.display = display === 'block' ? 'none' : 'block';
925 // Update the More / Less text
926 ev.target.innerText = ' [ ' + (display === 'none' ? ill_batch_metadata_less : ill_batch_metadata_more) + ' ]';
930 // A render function for the link to a request ID
931 function createRequestId(x, y, data) {
932 return data.requestId || '-';
935 function createRequestStatus(x, y, data) {
936 return data.requestStatus || '-';
939 function buildTable(identifiers) {
940 table = KohaTable('identifier-table', {
951 render: createIdentifierType
956 className: 'identifier'
960 render: createMetadata
965 render: createRequestId
968 data: 'requestStatus',
970 render: createRequestStatus
974 render: createActions,
975 className: 'action-column noExport'
978 createdRow: function (row, data) {
979 if (data.failed.length > 0) {
980 row.classList.add('fetch-failed');
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>';
991 function updateTable() {
993 tableEl.style.display = tableContent.data.length > 0 ? 'table' : 'none';
994 tableEl.style.width = '100%';
997 .rows.add(tableContent.data)
1001 function identifyIdentifier(identifier) {
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) {
1013 type: identifierType,
1014 value: match.groups.identifier
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';
1031 var el = document.getElementById('textarea-errors');
1032 el.style.display = 'flex';
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';
1047 function manageBatchItemsDisplay() {
1048 batchItemsDisplay.style.display = batch.data.ill_batch_id ? 'block' : 'none'
1051 function updateBatchInputs() {
1052 nameInput.value = batch.data.name || '';
1053 cardnumberInput.value = batch.data.cardnumber || '';
1054 branchcodeSelect.value = batch.data.library_id || '';
1057 function debounce(func) {
1059 return function (...args) {
1060 return new Promise(function (resolve) {
1062 clearTimeout(timeout);
1064 timeout = setTimeout(function () {
1065 return resolve(func(...args));
1071 function patronAutocomplete() {
1072 patron_autocomplete(
1073 $('#batch-form #batchcardnumber'),
1075 'on-select-callback': function( event, ui ) {
1076 $("#batch-form #batchcardnumber").val( ui.item.cardnumber );
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;