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('statuscode');
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});
311 ill_backend_id: batch.data.backend,
312 patron_id: batch.data.patron.borrowernumber,
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.batch_id && batch.data.statuscode === 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.batch_id;
486 // Get all batch statuses
487 function fetchStatuses() {
488 window.doApiRequest('/api/v1/illbatchstatuses')
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)
503 .then(function (response) {
504 return response.json();
506 .then(function (jsoned) {
508 batch_id: jsoned.batch_id,
510 backend: jsoned.backend,
511 cardnumber: jsoned.cardnumber,
512 library_id: jsoned.library_id,
513 statuscode: jsoned.statuscode
517 .then(function (data) {
521 window.handleApiError(ill_batch_api_fail);
525 function createBatch() {
526 var selectedBranchcode = branchcodeSelect.selectedOptions[0].value;
527 var selectedStatuscode = statusesSelect.selectedOptions[0].value;
528 return doBatchApiRequest('', {
531 'Content-type': 'application/json'
533 body: JSON.stringify({
534 name: nameInput.value,
536 cardnumber: cardnumberInput.value,
537 library_id: selectedBranchcode,
538 statuscode: selectedStatuscode
541 .then(function (response) {
543 return response.json();
545 return Promise.reject(response);
547 .then(function (body) {
548 batchId = body.batch_id;
550 batch_id: body.batch_id,
552 backend: body.backend,
553 cardnumber: body.patron.cardnumber,
554 library_id: body.library_id,
555 statuscode: body.statuscode,
561 .catch(function (response) {
562 response.json().then((json) => {
564 handleApiError(json.error);
566 handleApiError(ill_batch_create_api_fail);
572 function updateBatch() {
573 var selectedBranchcode = branchcodeSelect.selectedOptions[0].value;
574 var selectedStatuscode = statusesSelect.selectedOptions[0].value;
575 return doBatchApiRequest('/' + batch.data.batch_id, {
578 'Content-type': 'application/json'
580 body: JSON.stringify({
581 name: nameInput.value,
582 backend: batch.data.backend,
583 cardnumber: batch.data.patron.cardnumber,
584 library_id: selectedBranchcode,
585 statuscode: selectedStatuscode
589 handleApiError(ill_batch_update_api_fail);
593 function displaySupportedIdentifiers() {
594 var names = Object.keys(supportedIdentifiers).map(function (identifier) {
595 return window['ill_batch_' + identifier];
597 var displayEl = document.getElementById('supported_identifiers');
598 displayEl.textContent = names.length > 0 ? names.join(', ') : ill_batch_none;
601 function updateRowCount() {
602 var textEl = document.getElementById('row_count_value');
603 var val = textarea.value.trim();
605 if (val.length > 0) {
606 cnt = val.split(/\n/).length;
608 textEl.textContent = cnt;
611 function showProgress() {
612 var el = document.getElementById('create-progress');
613 el.style.display = 'block';
616 function showCreateRequestsButton() {
617 var data = progressTotals.data;
618 var el = document.getElementById('create-requests');
619 el.style.display = (data.total > 0 && data.count === data.total) ? 'flex' : 'none';
622 async function processIdentifiers() {
623 var content = textarea.value;
625 if (content.length === 0) return;
627 disableProcessButton();
628 var label = document.getElementById('progress-label').firstChild;
629 label.innerHTML = ill_batch_retrieving_metadata;
632 // Errors encountered when processing
633 var processErrors = {};
635 // Prepare the content, including trimming each row
636 var contentArr = content.split(/\n/);
637 var trimmed = contentArr.map(function (row) {
643 trimmed.forEach(function (identifier) {
644 var match = identifyIdentifier(identifier);
645 // If this identifier is not identifiable or
646 // looks like more than one type, we can't be sure
648 if (match.length != 1) {
654 parsed.push(match[0]);
658 var unknownIdentifiers = parsed
659 .filter(function (parse) {
660 if (parse.type == 'unknown') {
664 .map(function (filtered) {
665 return filtered.value;
668 if (unknownIdentifiers.length > 0) {
669 processErrors.badidentifiers = {
671 values: unknownIdentifiers.join(', ')
678 parsed.forEach(function (row) {
679 var value = row.value;
680 var alreadyInDeduped = deduped.filter(function (d) {
681 return d.value === value;
683 if (alreadyInDeduped.length > 0 && !dupes[value]) {
685 } else if (alreadyInDeduped.length === 0) {
688 row.requestId = null;
692 // Update duplicate error if dupes were found
693 if (Object.keys(dupes).length > 0) {
694 processErrors.duplicates = {
696 values: Object.keys(dupes).join(', ')
700 // Display any errors
701 displayErrors(processErrors);
703 // Now build and display the table
708 // We may be appending new values to an existing table,
709 // in which case, ensure we don't create duplicates
710 var tabIdentifiers = tableContent.data.map(function (tabId) {
713 var notInTable = deduped.filter(function (ded) {
714 if (!tabIdentifiers.includes(ded.value)) {
718 if (notInTable.length > 0) {
719 tableContent.data = tableContent.data.concat(notInTable);
722 // Populate metadata for those records that need it
723 var newData = tableContent.data;
724 for (var i = 0; i < tableContent.data.length; i++) {
725 var row = tableContent.data[i];
726 // Skip rows that don't need populating
728 Object.keys(tableContent.data[i].metadata).length > 0 ||
729 Object.keys(tableContent.data[i].failed).length > 0
731 var identifier = { type: row.type, value: row.value };
733 var populated = await populateMetadata(identifier);
734 row.metadata = populated.results.result || {};
736 row.failed = ill_populate_failed;
739 tableContent.data = newData;
743 function disableProcessButton() {
744 processButton.setAttribute('disabled', true);
745 processButton.setAttribute('aria-disabled', true);
748 function disableCreateButton() {
749 createButton.setAttribute('disabled', true);
750 createButton.setAttribute('aria-disabled', true);
753 async function populateMetadata(identifier) {
754 // All services that support this identifier type
755 var services = supportedIdentifiers[identifier.type];
756 // Check each service and use the first results we get, if any
757 for (var i = 0; i < services.length; i++) {
758 var service = services[i];
759 var endpoint = '/api/v1/contrib/' + service.api_namespace + service.search_endpoint + '?' + identifier.type + '=' + identifier.value;
760 var metadata = await getMetadata(endpoint);
761 if (metadata.errors.length === 0) {
762 var parsed = await parseMetadata(metadata, service);
763 if (parsed.errors.length > 0) {
764 throw Error(metadata.errors.map(function (error) {
765 return error.message;
773 async function getMetadata(endpoint) {
774 var response = await debounce(doApiRequest)(endpoint);
775 return response.json();
778 async function parseMetadata(metadata, service) {
779 var endpoint = '/api/v1/contrib/' + service.api_namespace + service.ill_parse_endpoint;
780 var response = await doApiRequest(endpoint, {
783 'Content-type': 'application/json'
785 body: JSON.stringify(metadata)
787 return response.json();
790 // A render function for identifier type
791 function createIdentifierType(data) {
792 return window['ill_batch_' + data];
795 // Get an item's title
796 function getTitle(meta) {
797 if (meta.article_title && meta.article_title.length > 0) {
798 return 'article_title';
800 prop: 'article_title',
801 value: meta.article_title
803 } else if (meta.title && meta.title.length > 0) {
812 // Create a metadata row
813 function createMetadataRow(data, meta, prop) {
814 if (!meta[prop]) return;
816 var div = document.createElement('div');
817 div.classList.add('metadata-row');
818 var label = document.createElement('span');
819 label.classList.add('metadata-label');
820 label.innerText = ill_batch_metadata[prop] + ': ';
822 // Add a link to the availability URL if appropriate
825 value = document.createElement('span');
827 value = document.createElement('a');
828 value.setAttribute('href', data.url);
829 value.setAttribute('target', '_blank');
830 value.setAttribute('title', ill_batch_available_via + ' ' + data.availabilitySupplier);
832 value.classList.add('metadata-value');
833 value.innerText = meta[prop];
834 div.appendChild(label);
835 div.appendChild(value);
840 // A render function for displaying metadata
841 function createMetadata(x, y, data) {
842 // If the fetch failed
843 if (data.failed.length > 0) {
847 // If we've not yet got any metadata back
848 if (Object.keys(data.metadata).length === 0) {
849 return ill_populate_waiting;
852 var core = ['doi', 'pmid', 'issn', 'title', 'year', 'issue', 'pages', 'publisher', 'article_title', 'article_author', 'volume'];
853 var meta = data.metadata;
855 var container = document.createElement('div');
856 container.classList.add('metadata-container');
858 // Create the title row
859 var title = getTitle(meta);
861 // Remove the title element from the props
862 // we're about to iterate
863 core = core.filter(function (i) {
866 var titleRow = createMetadataRow(data, meta, title);
867 container.appendChild(titleRow);
870 var remainder = document.createElement('div');
871 remainder.classList.add('metadata-remainder');
872 remainder.style.display = 'none';
873 // Create the remaining rows
874 core.sort().forEach(function (prop) {
875 var div = createMetadataRow(data, meta, prop);
877 remainder.appendChild(div);
880 container.appendChild(remainder);
882 // Add a more/less toggle
883 var firstField = container.firstChild;
884 var moreLess = document.createElement('div');
885 moreLess.classList.add('more-less');
886 var moreLessLink = document.createElement('a');
887 moreLessLink.setAttribute('href', '#');
888 moreLessLink.classList.add('more-less-link');
889 moreLessLink.innerText = ' [' + ill_batch_metadata_more + ']';
890 moreLess.appendChild(moreLessLink);
891 firstField.appendChild(moreLess);
893 return container.outerHTML;
896 function removeRow(ev) {
897 if (ev.target.className.includes('remove-row')) {
898 if (!confirm(ill_batch_item_remove)) return;
899 // Find the parent row
900 var ancestor = ev.target.closest('tr');
901 var identifier = ancestor.querySelector('.identifier').innerText;
902 tableContent.data = tableContent.data.filter(function (row) {
903 return row.value !== identifier;
908 function toggleMetadata(ev) {
909 if (ev.target.className === 'more-less-link') {
910 // Find the element we need to show
911 var ancestor = ev.target.closest('.metadata-container');
912 var meta = ancestor.querySelector('.metadata-remainder');
914 // Display or hide based on its current state
915 var display = window.getComputedStyle(meta).display;
917 meta.style.display = display === 'block' ? 'none' : 'block';
919 // Update the More / Less text
920 ev.target.innerText = ' [ ' + (display === 'none' ? ill_batch_metadata_less : ill_batch_metadata_more) + ' ]';
924 // A render function for the link to a request ID
925 function createRequestId(x, y, data) {
926 return data.requestId || '-';
929 function createRequestStatus(x, y, data) {
930 return data.requestStatus || '-';
933 function buildTable(identifiers) {
934 table = KohaTable('identifier-table', {
945 render: createIdentifierType
950 className: 'identifier'
954 render: createMetadata
959 render: createRequestId
962 data: 'requestStatus',
964 render: createRequestStatus
968 render: createActions,
969 className: 'action-column noExport'
972 createdRow: function (row, data) {
973 if (data.failed.length > 0) {
974 row.classList.add('fetch-failed');
980 function createActions(x, y, data) {
981 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>';
985 function updateTable() {
987 tableEl.style.display = tableContent.data.length > 0 ? 'table' : 'none';
988 tableEl.style.width = '100%';
991 .rows.add(tableContent.data)
995 function identifyIdentifier(identifier) {
998 // Iterate our available services to see if any can identify this identifier
999 Object.keys(supportedIdentifiers).forEach(function (identifierType) {
1000 // Since all the services supporting this identifier type should use the same
1001 // regex to identify it, we can just use the first
1002 var service = supportedIdentifiers[identifierType][0];
1003 var regex = new RegExp(service.identifiers_supported[identifierType].regex);
1004 var match = identifier.match(regex);
1005 if (match && match.groups && match.groups.identifier) {
1007 type: identifierType,
1008 value: match.groups.identifier
1015 function displayErrors(errors) {
1016 var keys = Object.keys(errors);
1017 if (keys.length > 0) {
1018 keys.forEach(function (key) {
1019 var el = document.getElementById(errors[key].element);
1020 el.textContent = errors[key].values;
1021 el.style.display = 'inline';
1022 var container = document.getElementById(key);
1023 container.style.display = 'block';
1025 var el = document.getElementById('textarea-errors');
1026 el.style.display = 'flex';
1030 function hideErrors() {
1031 var dupelist = document.getElementById('dupelist');
1032 var badids = document.getElementById('badids');
1033 dupelist.textContent = '';
1034 dupelist.parentElement.style.display = 'none';
1035 badids.textContent = '';
1036 badids.parentElement.style.display = 'none';
1037 var tae = document.getElementById('textarea-errors');
1038 tae.style.display = 'none';
1041 function manageBatchItemsDisplay() {
1042 batchItemsDisplay.style.display = batch.data.batch_id ? 'block' : 'none'
1045 function updateBatchInputs() {
1046 nameInput.value = batch.data.name || '';
1047 cardnumberInput.value = batch.data.cardnumber || '';
1048 branchcodeSelect.value = batch.data.library_id || '';
1051 function debounce(func) {
1053 return function (...args) {
1054 return new Promise(function (resolve) {
1056 clearTimeout(timeout);
1058 timeout = setTimeout(function () {
1059 return resolve(func(...args));
1065 function patronAutocomplete() {
1066 patron_autocomplete(
1067 $('#batch-form #batchcardnumber'),
1069 'on-select-callback': function( event, ui ) {
1070 $("#batch-form #batchcardnumber").val( ui.item.cardnumber );
1077 function createPatronLink() {
1078 if (!batch.data.patron) return;
1079 var patron = batch.data.patron;
1080 var a = document.createElement('a');
1081 var href = '/cgi-bin/koha/members/moremember.pl?borrowernumber=' + patron.borrowernumber;
1082 var text = patron.surname + ' (' + patron.cardnumber + ')';
1083 a.setAttribute('title', ill_borrower_details);
1084 a.setAttribute('href', href);
1085 a.textContent = text;