Pedro Amorim
fb02affd57
Staging modal area had issues listing availability checks for each request in the batch creation process To test: 1) Run bash <(curl -s https://raw.githubusercontent.com/ammopt/koha-ill-dev/master/start-ill-dev-plus.sh) 2) Install a metadata enrichment plugin, e.g. https://github.com/PTFS-Europe/koha-plugin-api-pubmed/releases 3) Install and configure an availability plugin, e.g. eds https://github.com/PTFS-Europe/koha-plugin-ill-avail-eds/releases 4) Enable ILLCheckAvailability sys pref 5) Create a new ILL batch and input some pubmedids, i.e. 34898594, 31452466 6) Verify that the availability results show and are working, for each request in the batch Signed-off-by: Edith Speller <Edith.Speller@ukhsa.gov.uk> Sponsored-by: UKHSA (UK Health Security Agency) Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
1107 lines
38 KiB
JavaScript
1107 lines
38 KiB
JavaScript
(function () {
|
|
// Bail if there aren't any metadata enrichment plugins installed
|
|
if (typeof metadata_enrichment_services === 'undefined') {
|
|
console.log('No metadata enrichment plugins found.')
|
|
return;
|
|
}
|
|
|
|
window.addEventListener('load', onload);
|
|
|
|
// Delay between API requests
|
|
var debounceDelay = 1000;
|
|
|
|
// Elements we work frequently with
|
|
var textarea = document.getElementById("identifiers_input");
|
|
var nameInput = document.getElementById("name");
|
|
var cardnumberInput = document.getElementById("batchcardnumber");
|
|
var branchcodeSelect = document.getElementById("branchcode");
|
|
var processButton = document.getElementById("process_button");
|
|
var createButton = document.getElementById("button_create_batch");
|
|
var finishButton = document.getElementById("button_finish");
|
|
var batchItemsDisplay = document.getElementById("add_batch_items");
|
|
var createProgressTotal = document.getElementById("processed_total");
|
|
var createProgressCount = document.getElementById("processed_count");
|
|
var createProgressFailed = document.getElementById("processed_failed");
|
|
var createProgressBar = document.getElementById("processed_progress_bar");
|
|
var identifierTable = document.getElementById('identifier-table');
|
|
var createRequestsButton = document.getElementById('create-requests-button');
|
|
var statusesSelect = document.getElementById('status_code');
|
|
var cancelButton = document.getElementById('lhs').querySelector('button');
|
|
var cancelButtonOriginalText = cancelButton.innerHTML;
|
|
|
|
// We need a data structure keyed on identifier type, which tells us how to parse that
|
|
// identifier type and what services can get its metadata. We receive an array of
|
|
// available services
|
|
var supportedIdentifiers = {};
|
|
metadata_enrichment_services.forEach(function (service) {
|
|
// Iterate the identifiers that this service supports
|
|
Object.keys(service.identifiers_supported).forEach(function (idType) {
|
|
if (!supportedIdentifiers[idType]) {
|
|
supportedIdentifiers[idType] = [];
|
|
}
|
|
supportedIdentifiers[idType].push(service);
|
|
});
|
|
});
|
|
|
|
// An object for when we're creating a new batch
|
|
var emptyBatch = {
|
|
name: '',
|
|
backend: null,
|
|
cardnumber: '',
|
|
branchcode: '',
|
|
status_code: 'NEW'
|
|
};
|
|
|
|
// The object that holds the batch we're working with
|
|
// It's a proxy so we can update portions of the UI
|
|
// upon changes
|
|
var batch = new Proxy(
|
|
{ data: {} },
|
|
{
|
|
get: function (obj, prop) {
|
|
return obj[prop];
|
|
},
|
|
set: function (obj, prop, value) {
|
|
obj[prop] = value;
|
|
manageBatchItemsDisplay();
|
|
updateBatchInputs();
|
|
disableCardnumberInput();
|
|
displayPatronName();
|
|
updateStatusesSelect();
|
|
}
|
|
}
|
|
);
|
|
|
|
// The object that holds the contents of the table
|
|
// It's a proxy so we can make it automatically redraw the
|
|
// table upon changes
|
|
var tableContent = new Proxy(
|
|
{ data: [] },
|
|
{
|
|
get: function (obj, prop) {
|
|
return obj[prop];
|
|
},
|
|
set: function (obj, prop, value) {
|
|
obj[prop] = value;
|
|
updateTable();
|
|
updateRowCount();
|
|
updateProcessTotals();
|
|
}
|
|
}
|
|
);
|
|
|
|
// The object that holds the contents of the table
|
|
// It's a proxy so we can update portions of the UI
|
|
// upon changes
|
|
var statuses = new Proxy(
|
|
{ data: [] },
|
|
{
|
|
get: function (obj, prop) {
|
|
return obj[prop];
|
|
},
|
|
set: function (obj, prop, value) {
|
|
obj[prop] = value;
|
|
updateStatusesSelect();
|
|
}
|
|
}
|
|
);
|
|
|
|
var progressTotals = new Proxy(
|
|
{
|
|
data: {}
|
|
},
|
|
{
|
|
get: function (obj, prop) {
|
|
return obj[prop];
|
|
},
|
|
set: function (obj, prop, value) {
|
|
obj[prop] = value;
|
|
showCreateRequestsButton();
|
|
}
|
|
}
|
|
);
|
|
|
|
// Keep track of submission API calls that are in progress
|
|
// so we don't duplicate them
|
|
var submissionSent = {};
|
|
|
|
// Keep track of availability API calls that are in progress
|
|
// so we don't duplicate them
|
|
var availabilitySent = {};
|
|
|
|
// Are we updating an existing batch
|
|
var isUpdate = false;
|
|
|
|
// The datatable
|
|
var table;
|
|
var tableEl = document.getElementById('identifier-table');
|
|
|
|
// The element that potentially holds the ID of the batch
|
|
// we're working with
|
|
var idEl = document.getElementById('ill-batch-details');
|
|
var batchId = null;
|
|
var backend = null;
|
|
|
|
function onload() {
|
|
$('#ill-batch-modal').on('shown.bs.modal', function () {
|
|
init();
|
|
patronAutocomplete();
|
|
batchInputsEventListeners();
|
|
createButtonEventListener();
|
|
createRequestsButtonEventListener();
|
|
moreLessEventListener();
|
|
removeRowEventListener();
|
|
});
|
|
$('#ill-batch-modal').on('hidden.bs.modal', function () {
|
|
// Reset our state when we close the modal
|
|
// TODO: need to also reset progress bar and already processed identifiers
|
|
delete idEl.dataset.batchId;
|
|
delete idEl.dataset.backend;
|
|
batchId = null;
|
|
tableEl.style.display = 'none';
|
|
tableContent.data = [];
|
|
progressTotals.data = {
|
|
total: 0,
|
|
count: 0,
|
|
failed: 0
|
|
};
|
|
textarea.value = '';
|
|
batch.data = {};
|
|
cancelButton.innerHTML = cancelButtonOriginalText;
|
|
// Remove event listeners we created
|
|
removeEventListeners();
|
|
});
|
|
};
|
|
|
|
function init() {
|
|
batchId = idEl.dataset.batchId;
|
|
backend = idEl.dataset.backend;
|
|
emptyBatch.backend = backend;
|
|
progressTotals.data = {
|
|
total: 0,
|
|
count: 0,
|
|
failed: 0
|
|
};
|
|
if (batchId) {
|
|
fetchBatch();
|
|
isUpdate = true;
|
|
setModalHeading();
|
|
} else {
|
|
batch.data = emptyBatch;
|
|
setModalHeading();
|
|
}
|
|
fetchStatuses();
|
|
finishButtonEventListener();
|
|
processButtonEventListener();
|
|
identifierTextareaEventListener();
|
|
displaySupportedIdentifiers();
|
|
createButtonEventListener();
|
|
updateRowCount();
|
|
};
|
|
|
|
function initPostCreate() {
|
|
disableCreateButton();
|
|
cancelButton.innerHTML = ill_batch_create_cancel_button;
|
|
};
|
|
|
|
function setFinishButton() {
|
|
if (batch.data.patron) {
|
|
finishButton.removeAttribute('disabled');
|
|
}
|
|
};
|
|
|
|
function setModalHeading() {
|
|
var heading = document.getElementById('ill-batch-modal-label');
|
|
heading.textContent = isUpdate ? ill_batch_update : ill_batch_add;
|
|
}
|
|
|
|
// Identify items that have metadata and therefore can have a local request
|
|
// created, and do so
|
|
function requestRequestable() {
|
|
createRequestsButton.setAttribute('disabled', true);
|
|
createRequestsButton.setAttribute('aria-disabled', true);
|
|
setFinishButton();
|
|
var toCheck = tableContent.data;
|
|
toCheck.forEach(function (row) {
|
|
if (
|
|
!row.requestId &&
|
|
Object.keys(row.metadata).length > 0 &&
|
|
!submissionSent[row.value]
|
|
) {
|
|
submissionSent[row.value] = 1;
|
|
makeLocalSubmission(row.value, row.metadata);
|
|
}
|
|
});
|
|
};
|
|
|
|
async function populateAvailability(row) {
|
|
let metadata = row.metadata;
|
|
|
|
let availability_object = {};
|
|
if (metadata.issn) availability_object['issn'] = metadata.issn;
|
|
if (metadata.doi) availability_object['doi'] = metadata.doi;
|
|
if (metadata.pubmedid) availability_object['pubmedid'] = metadata.pubmedid;
|
|
|
|
// Check each service and use the first results we get, if any
|
|
var av_hits = [];
|
|
for (const service of batch_availability_services){
|
|
var prepped = encodeURIComponent(base64EncodeUnicode(JSON.stringify(availability_object)));
|
|
|
|
var endpoint = service.endpoint + prepped;
|
|
var availability = await getAvailability(endpoint);
|
|
if (availability.results.search_results && availability.results.search_results.length > 0) {
|
|
av_hits.push({name: service.name, url: availability.results.search_results[0].url});
|
|
}else{
|
|
av_hits.push({ name: service.name, empty:1 });
|
|
}
|
|
};
|
|
return av_hits;
|
|
};
|
|
|
|
// Help btoa with > 8 bit strings
|
|
// Shamelessly grabbed from: https://www.base64encoder.io/javascript/
|
|
function base64EncodeUnicode(str) {
|
|
// First we escape the string using encodeURIComponent to get the UTF-8 encoding of the characters,
|
|
// then we convert the percent encodings into raw bytes, and finally feed it to btoa() function.
|
|
utf8Bytes = encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
|
|
return String.fromCharCode('0x' + p1);
|
|
});
|
|
|
|
return btoa(utf8Bytes);
|
|
};
|
|
|
|
// Create a local submission and update our local state
|
|
// upon success
|
|
function makeLocalSubmission(identifier, metadata) {
|
|
|
|
// Prepare extended_attributes in array format for POST
|
|
var extended_attributes = [];
|
|
for (const [key, value] of Object.entries(metadata)) {
|
|
extended_attributes.push({"type":key, "value":value});
|
|
}
|
|
|
|
var payload = {
|
|
ill_batch_id: batchId,
|
|
ill_backend_id: batch.data.backend,
|
|
patron_id: batch.data.patron.patron_id,
|
|
library_id: batch.data.library_id,
|
|
extended_attributes: extended_attributes
|
|
};
|
|
window.doCreateSubmission(payload)
|
|
.then(function (response) {
|
|
return response.json();
|
|
})
|
|
.then(function (data) {
|
|
tableContent.data = tableContent.data.map(function (row) {
|
|
if (row.value === identifier) {
|
|
row.requestId = data.ill_request_id;
|
|
row.requestStatus = data.status;
|
|
}
|
|
return row;
|
|
});
|
|
})
|
|
.catch(function () {
|
|
window.handleApiError(ill_batch_api_request_fail);
|
|
});
|
|
};
|
|
|
|
function updateProcessTotals() {
|
|
var init = {
|
|
total: 0,
|
|
count: 0,
|
|
failed: 0
|
|
};
|
|
progressTotals.data = init;
|
|
var toUpdate = progressTotals.data;
|
|
tableContent.data.forEach(function (row) {
|
|
toUpdate.total++;
|
|
if (Object.keys(row.metadata).length > 0 || row.failed.length > 0) {
|
|
toUpdate.count++;
|
|
}
|
|
if (Object.keys(row.failed).length > 0) {
|
|
toUpdate.failed++;
|
|
}
|
|
});
|
|
createProgressTotal.innerHTML = toUpdate.total;
|
|
createProgressCount.innerHTML = toUpdate.count;
|
|
createProgressFailed.innerHTML = toUpdate.failed;
|
|
var percentDone = Math.ceil((toUpdate.count / toUpdate.total) * 100);
|
|
createProgressBar.setAttribute('aria-valuenow', percentDone);
|
|
createProgressBar.innerHTML = percentDone + '%';
|
|
createProgressBar.style.width = percentDone + '%';
|
|
progressTotals.data = toUpdate;
|
|
};
|
|
|
|
function displayPatronName() {
|
|
var span = document.getElementById('patron_link');
|
|
if (batch.data.patron) {
|
|
var link = createPatronLink();
|
|
span.appendChild(link);
|
|
} else {
|
|
if (span.children.length > 0) {
|
|
span.removeChild(span.firstChild);
|
|
}
|
|
}
|
|
};
|
|
|
|
function updateStatusesSelect() {
|
|
while (statusesSelect.options.length > 0) {
|
|
statusesSelect.remove(0);
|
|
}
|
|
statuses.data.forEach(function (status) {
|
|
var option = document.createElement('option')
|
|
option.value = status.code;
|
|
option.text = status.name;
|
|
if (batch.data.ill_batch_id && batch.data.status_code === status.code) {
|
|
option.selected = true;
|
|
}
|
|
statusesSelect.add(option);
|
|
});
|
|
if (isUpdate) {
|
|
statusesSelect.parentElement.style.display = 'block';
|
|
}
|
|
};
|
|
|
|
function removeEventListeners() {
|
|
textarea.removeEventListener('paste', processButtonState);
|
|
textarea.removeEventListener('keyup', processButtonState);
|
|
processButton.removeEventListener('click', processIdentifiers);
|
|
nameInput.removeEventListener('keyup', createButtonState);
|
|
cardnumberInput.removeEventListener('keyup', createButtonState);
|
|
branchcodeSelect.removeEventListener('change', createButtonState);
|
|
createButton.removeEventListener('click', createBatch);
|
|
identifierTable.removeEventListener('click', toggleMetadata);
|
|
identifierTable.removeEventListener('click', removeRow);
|
|
createRequestsButton.remove('click', requestRequestable);
|
|
};
|
|
|
|
function finishButtonEventListener() {
|
|
finishButton.addEventListener('click', doFinish);
|
|
};
|
|
|
|
function identifierTextareaEventListener() {
|
|
textarea.addEventListener('paste', textareaUpdate);
|
|
textarea.addEventListener('keyup', textareaUpdate);
|
|
};
|
|
|
|
function processButtonEventListener() {
|
|
processButton.addEventListener('click', processIdentifiers);
|
|
};
|
|
|
|
function createRequestsButtonEventListener() {
|
|
createRequestsButton.addEventListener('click', requestRequestable);
|
|
};
|
|
|
|
function createButtonEventListener() {
|
|
createButton.addEventListener('click', createBatch);
|
|
};
|
|
|
|
function batchInputsEventListeners() {
|
|
nameInput.addEventListener('keyup', createButtonState);
|
|
cardnumberInput.addEventListener('keyup', createButtonState);
|
|
branchcodeSelect.addEventListener('change', createButtonState);
|
|
};
|
|
|
|
function moreLessEventListener() {
|
|
identifierTable.addEventListener('click', toggleMetadata);
|
|
};
|
|
|
|
function removeRowEventListener() {
|
|
identifierTable.addEventListener('click', removeRow);
|
|
};
|
|
|
|
function textareaUpdate() {
|
|
processButtonState();
|
|
updateRowCount();
|
|
};
|
|
|
|
function processButtonState() {
|
|
if (textarea.value.length > 0) {
|
|
processButton.removeAttribute('disabled');
|
|
processButton.removeAttribute('aria-disabled');
|
|
} else {
|
|
processButton.setAttribute('disabled', true);
|
|
processButton.setAttribute('aria-disabled', true);
|
|
}
|
|
};
|
|
|
|
function disableCardnumberInput() {
|
|
if (batch.data.patron) {
|
|
cardnumberInput.setAttribute('disabled', true);
|
|
cardnumberInput.setAttribute('aria-disabled', true);
|
|
} else {
|
|
cardnumberInput.removeAttribute('disabled');
|
|
cardnumberInput.removeAttribute('aria-disabled');
|
|
}
|
|
};
|
|
|
|
function createButtonState() {
|
|
if (
|
|
nameInput.value.length > 0 &&
|
|
cardnumberInput.value.length > 0 &&
|
|
branchcodeSelect.selectedOptions.length === 1
|
|
) {
|
|
createButton.removeAttribute('disabled');
|
|
createButton.setAttribute('display', 'inline-block');
|
|
} else {
|
|
createButton.setAttribute('disabled', 1);
|
|
createButton.setAttribute('display', 'none');
|
|
}
|
|
};
|
|
|
|
function doFinish() {
|
|
updateBatch()
|
|
.then(function () {
|
|
$('#ill-batch-modal').modal({ show: false });
|
|
location.href = '/cgi-bin/koha/ill/ill-requests.pl?batch_id=' + batch.data.ill_batch_id;
|
|
});
|
|
};
|
|
|
|
// Get all batch statuses
|
|
function fetchStatuses() {
|
|
window.doApiRequest('/api/v1/ill/batchstatuses')
|
|
.then(function (response) {
|
|
return response.json();
|
|
})
|
|
.then(function (jsoned) {
|
|
statuses.data = jsoned;
|
|
})
|
|
.catch(function (e) {
|
|
window.handleApiError(ill_batch_statuses_api_fail);
|
|
});
|
|
};
|
|
|
|
// Get the batch
|
|
function fetchBatch() {
|
|
window.doBatchApiRequest("/" + batchId, {
|
|
headers: {
|
|
'x-koha-embed': 'patron'
|
|
}
|
|
})
|
|
.then(function (response) {
|
|
return response.json();
|
|
})
|
|
.then(function (jsoned) {
|
|
batch.data = {
|
|
ill_batch_id: jsoned.ill_batch_id,
|
|
name: jsoned.name,
|
|
backend: jsoned.backend,
|
|
cardnumber: jsoned.cardnumber,
|
|
library_id: jsoned.library_id,
|
|
status_code: jsoned.status_code
|
|
}
|
|
return jsoned;
|
|
})
|
|
.then(function (data) {
|
|
batch.data = data;
|
|
})
|
|
.catch(function () {
|
|
window.handleApiError(ill_batch_api_fail);
|
|
});
|
|
};
|
|
|
|
function createBatch() {
|
|
var selectedBranchcode = branchcodeSelect.selectedOptions[0].value;
|
|
var selectedStatuscode = statusesSelect.selectedOptions[0].value;
|
|
return doBatchApiRequest('', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-type': 'application/json',
|
|
'x-koha-embed': 'patron'
|
|
},
|
|
body: JSON.stringify({
|
|
name: nameInput.value,
|
|
backend: backend,
|
|
cardnumber: cardnumberInput.value,
|
|
library_id: selectedBranchcode,
|
|
status_code: selectedStatuscode
|
|
})
|
|
})
|
|
.then(function (response) {
|
|
if ( response.ok ) {
|
|
return response.json();
|
|
}
|
|
return Promise.reject(response);
|
|
})
|
|
.then(function (body) {
|
|
batchId = body.ill_batch_id;
|
|
batch.data = {
|
|
ill_batch_id: body.ill_batch_id,
|
|
name: body.name,
|
|
backend: body.backend,
|
|
cardnumber: body.patron.cardnumber,
|
|
library_id: body.library_id,
|
|
status_code: body.status_code,
|
|
patron: body.patron,
|
|
status: body.status
|
|
};
|
|
initPostCreate();
|
|
})
|
|
.catch(function (response) {
|
|
response.json().then((json) => {
|
|
if( json.error ) {
|
|
handleApiError(json.error);
|
|
} else {
|
|
handleApiError(ill_batch_create_api_fail);
|
|
}
|
|
})
|
|
});
|
|
};
|
|
|
|
function updateBatch() {
|
|
var selectedBranchcode = branchcodeSelect.selectedOptions[0].value;
|
|
var selectedStatuscode = statusesSelect.selectedOptions[0].value;
|
|
|
|
return doBatchApiRequest('/' + batch.data.ill_batch_id, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
name: nameInput.value,
|
|
backend: batch.data.backend,
|
|
cardnumber: batch.data.patron.cardnumber,
|
|
library_id: selectedBranchcode,
|
|
status_code: selectedStatuscode
|
|
})
|
|
})
|
|
.catch(function () {
|
|
handleApiError(ill_batch_update_api_fail);
|
|
});
|
|
};
|
|
|
|
function displaySupportedIdentifiers() {
|
|
var names = Object.keys(supportedIdentifiers).map(function (identifier) {
|
|
return window['ill_batch_' + identifier];
|
|
});
|
|
var displayEl = document.getElementById('supported_identifiers');
|
|
displayEl.textContent = names.length > 0 ? names.join(', ') : ill_batch_none;
|
|
}
|
|
|
|
function updateRowCount() {
|
|
var textEl = document.getElementById('row_count_value');
|
|
var val = textarea.value.trim();
|
|
var cnt = 0;
|
|
if (val.length > 0) {
|
|
cnt = val.split(/\n/).length;
|
|
}
|
|
textEl.textContent = cnt;
|
|
}
|
|
|
|
function showProgress() {
|
|
var el = document.getElementById('create-progress');
|
|
el.style.display = 'block';
|
|
}
|
|
|
|
function showCreateRequestsButton() {
|
|
var data = progressTotals.data;
|
|
var el = document.getElementById('create-requests');
|
|
el.style.display = (data.total > 0 && data.count === data.total) ? 'flex' : 'none';
|
|
}
|
|
|
|
async function processIdentifiers() {
|
|
var content = textarea.value;
|
|
hideErrors();
|
|
if (content.length === 0) return;
|
|
|
|
disableProcessButton();
|
|
var label = document.getElementById('progress-label').firstChild;
|
|
label.innerHTML = ill_batch_retrieving_metadata;
|
|
showProgress();
|
|
|
|
// Errors encountered when processing
|
|
var processErrors = {};
|
|
|
|
// Prepare the content, including trimming each row
|
|
var contentArr = content.split(/\n/);
|
|
var trimmed = contentArr.map(function (row) {
|
|
return row.trim();
|
|
});
|
|
|
|
var parsed = [];
|
|
|
|
trimmed.forEach(function (identifier) {
|
|
var match = identifyIdentifier(identifier);
|
|
// If this identifier is not identifiable or
|
|
// looks like more than one type, we can't be sure
|
|
// what it is
|
|
if (match.length != 1) {
|
|
parsed.push({
|
|
type: 'unknown',
|
|
value: identifier
|
|
});
|
|
} else {
|
|
parsed.push(match[0]);
|
|
}
|
|
});
|
|
|
|
var unknownIdentifiers = parsed
|
|
.filter(function (parse) {
|
|
if (parse.type == 'unknown') {
|
|
return parse;
|
|
}
|
|
})
|
|
.map(function (filtered) {
|
|
return filtered.value;
|
|
});
|
|
|
|
if (unknownIdentifiers.length > 0) {
|
|
processErrors.badidentifiers = {
|
|
element: 'badids',
|
|
values: unknownIdentifiers.join(', ')
|
|
};
|
|
};
|
|
|
|
// Deduping
|
|
var deduped = [];
|
|
var dupes = {};
|
|
parsed.forEach(function (row) {
|
|
var value = row.value;
|
|
var alreadyInDeduped = deduped.filter(function (d) {
|
|
return d.value === value;
|
|
});
|
|
if (alreadyInDeduped.length > 0 && !dupes[value]) {
|
|
dupes[value] = 1;
|
|
} else if (alreadyInDeduped.length === 0) {
|
|
row.metadata = {};
|
|
row.failed = {};
|
|
row.availability_hits = {};
|
|
row.requestId = null;
|
|
deduped.push(row);
|
|
}
|
|
});
|
|
// Update duplicate error if dupes were found
|
|
if (Object.keys(dupes).length > 0) {
|
|
processErrors.duplicates = {
|
|
element: 'dupelist',
|
|
values: Object.keys(dupes).join(', ')
|
|
};
|
|
}
|
|
|
|
// Display any errors
|
|
displayErrors(processErrors);
|
|
|
|
// Now build and display the table
|
|
if (!table) {
|
|
buildTable();
|
|
}
|
|
|
|
// We may be appending new values to an existing table,
|
|
// in which case, ensure we don't create duplicates
|
|
var tabIdentifiers = tableContent.data.map(function (tabId) {
|
|
return tabId.value;
|
|
});
|
|
var notInTable = deduped.filter(function (ded) {
|
|
if (!tabIdentifiers.includes(ded.value)) {
|
|
return ded;
|
|
}
|
|
});
|
|
if (notInTable.length > 0) {
|
|
tableContent.data = tableContent.data.concat(notInTable);
|
|
}
|
|
|
|
// Populate metadata for those records that need it
|
|
var newData = tableContent.data;
|
|
for (var i = 0; i < tableContent.data.length; i++) {
|
|
var row = tableContent.data[i];
|
|
// Skip rows that don't need populating
|
|
if (
|
|
Object.keys(tableContent.data[i].metadata).length > 0 ||
|
|
Object.keys(tableContent.data[i].failed).length > 0
|
|
) continue;
|
|
var identifier = { type: row.type, value: row.value };
|
|
try {
|
|
var populated = await populateMetadata(identifier);
|
|
row.metadata = populated.results.result || {};
|
|
} catch (e) {
|
|
row.failed = ill_populate_failed;
|
|
}
|
|
|
|
if (ill_check_availability_syspref == 1){
|
|
try {
|
|
var availability = await populateAvailability(row);
|
|
row.availability_hits = availability || {};
|
|
} catch (e) {
|
|
//do nothing
|
|
}
|
|
}
|
|
|
|
newData[i] = row;
|
|
tableContent.data = newData;
|
|
}
|
|
}
|
|
|
|
function disableProcessButton() {
|
|
processButton.setAttribute('disabled', true);
|
|
processButton.setAttribute('aria-disabled', true);
|
|
}
|
|
|
|
function disableCreateButton() {
|
|
createButton.setAttribute('disabled', true);
|
|
createButton.setAttribute('aria-disabled', true);
|
|
}
|
|
|
|
async function populateMetadata(identifier) {
|
|
// All services that support this identifier type
|
|
var services = supportedIdentifiers[identifier.type];
|
|
// Check each service and use the first results we get, if any
|
|
for (var i = 0; i < services.length; i++) {
|
|
var service = services[i];
|
|
var endpoint = '/api/v1/contrib/' + service.api_namespace + service.search_endpoint + '?' + identifier.type + '=' + identifier.value;
|
|
var metadata = await getMetadata(endpoint);
|
|
if (metadata.errors.length === 0) {
|
|
var parsed = await parseMetadata(metadata, service);
|
|
if (parsed.errors.length > 0) {
|
|
throw Error(metadata.errors.map(function (error) {
|
|
return error.message;
|
|
}).join(', '));
|
|
}
|
|
return parsed;
|
|
}
|
|
}
|
|
};
|
|
|
|
async function getAvailability(endpoint) {
|
|
var response = await debounce(doApiRequest)(endpoint);
|
|
return response.json();
|
|
};
|
|
|
|
async function getMetadata(endpoint) {
|
|
var response = await debounce(doApiRequest)(endpoint);
|
|
return response.json();
|
|
};
|
|
|
|
async function parseMetadata(metadata, service) {
|
|
var endpoint = '/api/v1/contrib/' + service.api_namespace + service.ill_parse_endpoint;
|
|
var response = await doApiRequest(endpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-type': 'application/json'
|
|
},
|
|
body: JSON.stringify(metadata)
|
|
});
|
|
return response.json();
|
|
}
|
|
|
|
// A render function for identifier type
|
|
function createIdentifierType(data) {
|
|
return window['ill_batch_' + data];
|
|
};
|
|
|
|
// Get an item's title
|
|
function getTitle(meta) {
|
|
if (meta.article_title && meta.article_title.length > 0) {
|
|
return 'article_title';
|
|
return {
|
|
prop: 'article_title',
|
|
value: meta.article_title
|
|
};
|
|
} else if (meta.title && meta.title.length > 0) {
|
|
return 'title';
|
|
return {
|
|
prop: 'title',
|
|
value: meta.title
|
|
};
|
|
}
|
|
};
|
|
|
|
// Create a metadata row
|
|
function createMetadataRow(data, meta, prop) {
|
|
if (!meta[prop]) return;
|
|
|
|
var div = document.createElement('div');
|
|
div.classList.add('metadata-row');
|
|
var label = document.createElement('span');
|
|
label.classList.add('metadata-label');
|
|
label.innerText = ill_batch_metadata[prop] + ': ';
|
|
|
|
// Add a link to the availability URL if appropriate
|
|
var value = document.createElement('span');
|
|
value.classList.add('metadata-value');
|
|
value.innerText = meta[prop];
|
|
div.appendChild(label);
|
|
div.appendChild(value);
|
|
|
|
return div;
|
|
}
|
|
|
|
// A render function for displaying metadata
|
|
function createMetadata(x, y, data) {
|
|
// If the fetch failed
|
|
if (data.failed.length > 0) {
|
|
return data.failed;
|
|
}
|
|
|
|
// If we've not yet got any metadata back
|
|
if (Object.keys(data.metadata).length === 0) {
|
|
return ill_populate_waiting;
|
|
}
|
|
|
|
var core = ['doi', 'pmid', 'issn', 'title', 'year', 'issue', 'pages', 'publisher', 'article_title', 'article_author', 'volume'];
|
|
var meta = data.metadata;
|
|
|
|
var container = document.createElement('div');
|
|
container.classList.add('metadata-container');
|
|
|
|
// Create the title row
|
|
var title = getTitle(meta);
|
|
if (title) {
|
|
// Remove the title element from the props
|
|
// we're about to iterate
|
|
core = core.filter(function (i) {
|
|
return i !== title;
|
|
});
|
|
var titleRow = createMetadataRow(data, meta, title);
|
|
container.appendChild(titleRow);
|
|
}
|
|
|
|
var remainder = document.createElement('div');
|
|
remainder.classList.add('metadata-remainder');
|
|
remainder.style.display = 'none';
|
|
// Create the remaining rows
|
|
core.sort().forEach(function (prop) {
|
|
var div = createMetadataRow(data, meta, prop);
|
|
if (div) {
|
|
remainder.appendChild(div);
|
|
}
|
|
});
|
|
container.appendChild(remainder);
|
|
|
|
// Add a more/less toggle
|
|
var firstField = container.firstChild;
|
|
var moreLess = document.createElement('div');
|
|
moreLess.classList.add('more-less');
|
|
var moreLessLink = document.createElement('a');
|
|
moreLessLink.setAttribute('href', '#');
|
|
moreLessLink.classList.add('more-less-link');
|
|
moreLessLink.innerText = ' [' + ill_batch_metadata_more + ']';
|
|
moreLess.appendChild(moreLessLink);
|
|
firstField.appendChild(moreLess);
|
|
|
|
return container.outerHTML;
|
|
};
|
|
|
|
function removeRow(ev) {
|
|
if (ev.target.className.includes('remove-row')) {
|
|
if (!confirm(ill_batch_item_remove)) return;
|
|
// Find the parent row
|
|
var ancestor = ev.target.closest('tr');
|
|
var identifier = ancestor.querySelector('.identifier').innerText;
|
|
tableContent.data = tableContent.data.filter(function (row) {
|
|
return row.value !== identifier;
|
|
});
|
|
}
|
|
}
|
|
|
|
function toggleMetadata(ev) {
|
|
if (ev.target.className === 'more-less-link') {
|
|
// Find the element we need to show
|
|
var ancestor = ev.target.closest('.metadata-container');
|
|
var meta = ancestor.querySelector('.metadata-remainder');
|
|
|
|
// Display or hide based on its current state
|
|
var display = window.getComputedStyle(meta).display;
|
|
|
|
meta.style.display = display === 'block' ? 'none' : 'block';
|
|
|
|
// Update the More / Less text
|
|
ev.target.innerText = ' [ ' + (display === 'none' ? ill_batch_metadata_less : ill_batch_metadata_more) + ' ]';
|
|
}
|
|
}
|
|
|
|
// A render function for the link to a request ID
|
|
function createRequestId(x, y, data) {
|
|
return data.requestId || '-';
|
|
}
|
|
|
|
function createRequestStatus(x, y, data) {
|
|
return data.requestStatus || '-';
|
|
}
|
|
|
|
function createRequestAvailability(x, y, data) {
|
|
|
|
// If the fetch failed
|
|
if (data.failed.length > 0) {
|
|
return data.failed;
|
|
}
|
|
|
|
if (Object.keys(data.availability_hits).length === 0){
|
|
return ill_populate_waiting;
|
|
}
|
|
|
|
let str = '';
|
|
let has_some = false;
|
|
for (i = 0; i < data.availability_hits.length; i++) {
|
|
if (!data.availability_hits[i].empty){
|
|
has_some = true;
|
|
str += "<li><a href=" + data.availability_hits[i].url + " target=\"_blank\">" + data.availability_hits[i].name + "</a></li>"
|
|
}
|
|
}
|
|
if(!has_some){
|
|
str = ill_batch_none;
|
|
}
|
|
return str;
|
|
};
|
|
|
|
function buildTable(identifiers) {
|
|
table = KohaTable('identifier-table', {
|
|
processing: true,
|
|
deferRender: true,
|
|
ordering: false,
|
|
paging: false,
|
|
searching: false,
|
|
autoWidth: false,
|
|
columns: [
|
|
{
|
|
data: 'type',
|
|
width: '13%',
|
|
render: createIdentifierType
|
|
},
|
|
{
|
|
data: 'value',
|
|
width: '25%',
|
|
className: 'identifier'
|
|
},
|
|
{
|
|
data: 'metadata',
|
|
render: createMetadata
|
|
},
|
|
{
|
|
data: 'requestId',
|
|
width: '6.5%',
|
|
render: createRequestId
|
|
},
|
|
{
|
|
data: 'requestStatus',
|
|
width: '6.5%',
|
|
render: createRequestStatus
|
|
},
|
|
...( ill_check_availability_syspref == 1 ? [{
|
|
data: '',
|
|
width: '13%',
|
|
render: createRequestAvailability, }] : []
|
|
),
|
|
{
|
|
width: '6.5%',
|
|
render: createActions,
|
|
className: 'action-column noExport'
|
|
}
|
|
],
|
|
createdRow: function (row, data) {
|
|
if (data.failed.length > 0) {
|
|
row.classList.add('fetch-failed');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function createActions(x, y, data) {
|
|
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>';
|
|
}
|
|
|
|
// Redraw the table
|
|
function updateTable() {
|
|
if (!table) return;
|
|
tableEl.style.display = tableContent.data.length > 0 ? 'table' : 'none';
|
|
tableEl.style.width = '100%';
|
|
table.api()
|
|
.clear()
|
|
.rows.add(tableContent.data)
|
|
.draw();
|
|
};
|
|
|
|
function identifyIdentifier(identifier) {
|
|
var matches = [];
|
|
|
|
// Iterate our available services to see if any can identify this identifier
|
|
Object.keys(supportedIdentifiers).forEach(function (identifierType) {
|
|
// Since all the services supporting this identifier type should use the same
|
|
// regex to identify it, we can just use the first
|
|
var service = supportedIdentifiers[identifierType][0];
|
|
var regex = new RegExp(service.identifiers_supported[identifierType].regex);
|
|
var match = identifier.match(regex);
|
|
if (match && match.groups && match.groups.identifier) {
|
|
matches.push({
|
|
type: identifierType,
|
|
value: match.groups.identifier
|
|
});
|
|
}
|
|
});
|
|
return matches;
|
|
}
|
|
|
|
function displayErrors(errors) {
|
|
var keys = Object.keys(errors);
|
|
if (keys.length > 0) {
|
|
keys.forEach(function (key) {
|
|
var el = document.getElementById(errors[key].element);
|
|
el.textContent = errors[key].values;
|
|
el.style.display = 'inline';
|
|
var container = document.getElementById(key);
|
|
container.style.display = 'block';
|
|
});
|
|
var el = document.getElementById('textarea-errors');
|
|
el.style.display = 'flex';
|
|
}
|
|
}
|
|
|
|
function hideErrors() {
|
|
var dupelist = document.getElementById('dupelist');
|
|
var badids = document.getElementById('badids');
|
|
dupelist.textContent = '';
|
|
dupelist.parentElement.style.display = 'none';
|
|
badids.textContent = '';
|
|
badids.parentElement.style.display = 'none';
|
|
var tae = document.getElementById('textarea-errors');
|
|
tae.style.display = 'none';
|
|
}
|
|
|
|
function manageBatchItemsDisplay() {
|
|
batchItemsDisplay.style.display = batch.data.ill_batch_id ? 'block' : 'none'
|
|
};
|
|
|
|
function updateBatchInputs() {
|
|
nameInput.value = batch.data.name || '';
|
|
cardnumberInput.value = batch.data.cardnumber || '';
|
|
branchcodeSelect.value = batch.data.library_id || '';
|
|
}
|
|
|
|
function debounce(func) {
|
|
var timeout;
|
|
return function (...args) {
|
|
return new Promise(function (resolve) {
|
|
if (timeout) {
|
|
clearTimeout(timeout);
|
|
}
|
|
timeout = setTimeout(function () {
|
|
return resolve(func(...args));
|
|
}, debounceDelay);
|
|
});
|
|
}
|
|
}
|
|
|
|
function patronAutocomplete() {
|
|
patron_autocomplete(
|
|
$('#batch-form #batchcardnumber'),
|
|
{
|
|
'on-select-callback': function( event, ui ) {
|
|
$("#batch-form #batchcardnumber").val( ui.item.cardnumber );
|
|
return false;
|
|
}
|
|
}
|
|
);
|
|
};
|
|
|
|
function createPatronLink() {
|
|
if (!batch.data.patron) return;
|
|
var patron = batch.data.patron;
|
|
var a = document.createElement('a');
|
|
var href = '/cgi-bin/koha/members/moremember.pl?borrowernumber=' + patron.borrowernumber;
|
|
var text = patron.surname + ' (' + patron.cardnumber + ')';
|
|
a.setAttribute('title', ill_borrower_details);
|
|
a.setAttribute('href', href);
|
|
a.textContent = text;
|
|
return a;
|
|
};
|
|
|
|
})();
|