|
Line 0
Link Here
|
|
|
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('statuscode'); |
| 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 |
statuscode: '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 |
batch_id: batchId, |
| 311 |
ill_backend_id: batch.data.backend, |
| 312 |
patron_id: batch.data.patron.borrowernumber, |
| 313 |
library_id: batch.data.branchcode, |
| 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.id && batch.data.statuscode === 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.id; |
| 483 |
}); |
| 484 |
}; |
| 485 |
|
| 486 |
// Get all batch statuses |
| 487 |
function fetchStatuses() { |
| 488 |
window.doApiRequest('/api/v1/illbatchstatuses') |
| 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 |
.then(function (response) { |
| 504 |
return response.json(); |
| 505 |
}) |
| 506 |
.then(function (jsoned) { |
| 507 |
batch.data = { |
| 508 |
id: jsoned.id, |
| 509 |
name: jsoned.name, |
| 510 |
backend: jsoned.backend, |
| 511 |
cardnumber: jsoned.cardnumber, |
| 512 |
branchcode: jsoned.branchcode, |
| 513 |
statuscode: jsoned.statuscode |
| 514 |
} |
| 515 |
return jsoned; |
| 516 |
}) |
| 517 |
.then(function (data) { |
| 518 |
batch.data = data; |
| 519 |
}) |
| 520 |
.catch(function () { |
| 521 |
window.handleApiError(ill_batch_api_fail); |
| 522 |
}); |
| 523 |
}; |
| 524 |
|
| 525 |
function createBatch() { |
| 526 |
var selectedBranchcode = branchcodeSelect.selectedOptions[0].value; |
| 527 |
var selectedStatuscode = statusesSelect.selectedOptions[0].value; |
| 528 |
return doBatchApiRequest('', { |
| 529 |
method: 'POST', |
| 530 |
headers: { |
| 531 |
'Content-type': 'application/json' |
| 532 |
}, |
| 533 |
body: JSON.stringify({ |
| 534 |
name: nameInput.value, |
| 535 |
backend: backend, |
| 536 |
cardnumber: cardnumberInput.value, |
| 537 |
branchcode: selectedBranchcode, |
| 538 |
statuscode: selectedStatuscode |
| 539 |
}) |
| 540 |
}) |
| 541 |
.then(function (response) { |
| 542 |
if ( response.ok ) { |
| 543 |
return response.json(); |
| 544 |
} |
| 545 |
return Promise.reject(response); |
| 546 |
}) |
| 547 |
.then(function (body) { |
| 548 |
batchId = body.id; |
| 549 |
batch.data = { |
| 550 |
id: body.id, |
| 551 |
name: body.name, |
| 552 |
backend: body.backend, |
| 553 |
cardnumber: body.patron.cardnumber, |
| 554 |
branchcode: body.branchcode, |
| 555 |
statuscode: body.statuscode, |
| 556 |
patron: body.patron, |
| 557 |
status: body.status |
| 558 |
}; |
| 559 |
initPostCreate(); |
| 560 |
}) |
| 561 |
.catch(function (response) { |
| 562 |
response.json().then((json) => { |
| 563 |
if( json.error ) { |
| 564 |
handleApiError(json.error); |
| 565 |
} else { |
| 566 |
handleApiError(ill_batch_create_api_fail); |
| 567 |
} |
| 568 |
}) |
| 569 |
}); |
| 570 |
}; |
| 571 |
|
| 572 |
function updateBatch() { |
| 573 |
var selectedBranchcode = branchcodeSelect.selectedOptions[0].value; |
| 574 |
var selectedStatuscode = statusesSelect.selectedOptions[0].value; |
| 575 |
return doBatchApiRequest('/' + batch.data.id, { |
| 576 |
method: 'PUT', |
| 577 |
headers: { |
| 578 |
'Content-type': 'application/json' |
| 579 |
}, |
| 580 |
body: JSON.stringify({ |
| 581 |
name: nameInput.value, |
| 582 |
backend: batch.data.backend, |
| 583 |
cardnumber: batch.data.patron.cardnumber, |
| 584 |
branchcode: selectedBranchcode, |
| 585 |
statuscode: selectedStatuscode |
| 586 |
}) |
| 587 |
}) |
| 588 |
.catch(function () { |
| 589 |
handleApiError(ill_batch_update_api_fail); |
| 590 |
}); |
| 591 |
}; |
| 592 |
|
| 593 |
function displaySupportedIdentifiers() { |
| 594 |
var names = Object.keys(supportedIdentifiers).map(function (identifier) { |
| 595 |
return window['ill_batch_' + identifier]; |
| 596 |
}); |
| 597 |
var displayEl = document.getElementById('supported_identifiers'); |
| 598 |
displayEl.textContent = names.length > 0 ? names.join(', ') : ill_batch_none; |
| 599 |
} |
| 600 |
|
| 601 |
function updateRowCount() { |
| 602 |
var textEl = document.getElementById('row_count_value'); |
| 603 |
var val = textarea.value.trim(); |
| 604 |
var cnt = 0; |
| 605 |
if (val.length > 0) { |
| 606 |
cnt = val.split(/\n/).length; |
| 607 |
} |
| 608 |
textEl.textContent = cnt; |
| 609 |
} |
| 610 |
|
| 611 |
function showProgress() { |
| 612 |
var el = document.getElementById('create-progress'); |
| 613 |
el.style.display = 'block'; |
| 614 |
} |
| 615 |
|
| 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'; |
| 620 |
} |
| 621 |
|
| 622 |
async function processIdentifiers() { |
| 623 |
var content = textarea.value; |
| 624 |
hideErrors(); |
| 625 |
if (content.length === 0) return; |
| 626 |
|
| 627 |
disableProcessButton(); |
| 628 |
var label = document.getElementById('progress-label').firstChild; |
| 629 |
label.innerHTML = ill_batch_retrieving_metadata; |
| 630 |
showProgress(); |
| 631 |
|
| 632 |
// Errors encountered when processing |
| 633 |
var processErrors = {}; |
| 634 |
|
| 635 |
// Prepare the content, including trimming each row |
| 636 |
var contentArr = content.split(/\n/); |
| 637 |
var trimmed = contentArr.map(function (row) { |
| 638 |
return row.trim(); |
| 639 |
}); |
| 640 |
|
| 641 |
var parsed = []; |
| 642 |
|
| 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 |
| 647 |
// what it is |
| 648 |
if (match.length != 1) { |
| 649 |
parsed.push({ |
| 650 |
type: 'unknown', |
| 651 |
value: identifier |
| 652 |
}); |
| 653 |
} else { |
| 654 |
parsed.push(match[0]); |
| 655 |
} |
| 656 |
}); |
| 657 |
|
| 658 |
var unknownIdentifiers = parsed |
| 659 |
.filter(function (parse) { |
| 660 |
if (parse.type == 'unknown') { |
| 661 |
return parse; |
| 662 |
} |
| 663 |
}) |
| 664 |
.map(function (filtered) { |
| 665 |
return filtered.value; |
| 666 |
}); |
| 667 |
|
| 668 |
if (unknownIdentifiers.length > 0) { |
| 669 |
processErrors.badidentifiers = { |
| 670 |
element: 'badids', |
| 671 |
values: unknownIdentifiers.join(', ') |
| 672 |
}; |
| 673 |
}; |
| 674 |
|
| 675 |
// Deduping |
| 676 |
var deduped = []; |
| 677 |
var dupes = {}; |
| 678 |
parsed.forEach(function (row) { |
| 679 |
var value = row.value; |
| 680 |
var alreadyInDeduped = deduped.filter(function (d) { |
| 681 |
return d.value === value; |
| 682 |
}); |
| 683 |
if (alreadyInDeduped.length > 0 && !dupes[value]) { |
| 684 |
dupes[value] = 1; |
| 685 |
} else if (alreadyInDeduped.length === 0) { |
| 686 |
row.metadata = {}; |
| 687 |
row.failed = {}; |
| 688 |
row.requestId = null; |
| 689 |
deduped.push(row); |
| 690 |
} |
| 691 |
}); |
| 692 |
// Update duplicate error if dupes were found |
| 693 |
if (Object.keys(dupes).length > 0) { |
| 694 |
processErrors.duplicates = { |
| 695 |
element: 'dupelist', |
| 696 |
values: Object.keys(dupes).join(', ') |
| 697 |
}; |
| 698 |
} |
| 699 |
|
| 700 |
// Display any errors |
| 701 |
displayErrors(processErrors); |
| 702 |
|
| 703 |
// Now build and display the table |
| 704 |
if (!table) { |
| 705 |
buildTable(); |
| 706 |
} |
| 707 |
|
| 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) { |
| 711 |
return tabId.value; |
| 712 |
}); |
| 713 |
var notInTable = deduped.filter(function (ded) { |
| 714 |
if (!tabIdentifiers.includes(ded.value)) { |
| 715 |
return ded; |
| 716 |
} |
| 717 |
}); |
| 718 |
if (notInTable.length > 0) { |
| 719 |
tableContent.data = tableContent.data.concat(notInTable); |
| 720 |
} |
| 721 |
|
| 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 |
| 727 |
if ( |
| 728 |
Object.keys(tableContent.data[i].metadata).length > 0 || |
| 729 |
Object.keys(tableContent.data[i].failed).length > 0 |
| 730 |
) continue; |
| 731 |
var identifier = { type: row.type, value: row.value }; |
| 732 |
try { |
| 733 |
var populated = await populateMetadata(identifier); |
| 734 |
row.metadata = populated.results.result || {}; |
| 735 |
} catch (e) { |
| 736 |
row.failed = ill_populate_failed; |
| 737 |
} |
| 738 |
newData[i] = row; |
| 739 |
tableContent.data = newData; |
| 740 |
} |
| 741 |
} |
| 742 |
|
| 743 |
function disableProcessButton() { |
| 744 |
processButton.setAttribute('disabled', true); |
| 745 |
processButton.setAttribute('aria-disabled', true); |
| 746 |
} |
| 747 |
|
| 748 |
function disableCreateButton() { |
| 749 |
createButton.setAttribute('disabled', true); |
| 750 |
createButton.setAttribute('aria-disabled', true); |
| 751 |
} |
| 752 |
|
| 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; |
| 766 |
}).join(', ')); |
| 767 |
} |
| 768 |
return parsed; |
| 769 |
} |
| 770 |
} |
| 771 |
}; |
| 772 |
|
| 773 |
async function getMetadata(endpoint) { |
| 774 |
var response = await debounce(doApiRequest)(endpoint); |
| 775 |
return response.json(); |
| 776 |
}; |
| 777 |
|
| 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, { |
| 781 |
method: 'POST', |
| 782 |
headers: { |
| 783 |
'Content-type': 'application/json' |
| 784 |
}, |
| 785 |
body: JSON.stringify(metadata) |
| 786 |
}); |
| 787 |
return response.json(); |
| 788 |
} |
| 789 |
|
| 790 |
// A render function for identifier type |
| 791 |
function createIdentifierType(data) { |
| 792 |
return window['ill_batch_' + data]; |
| 793 |
}; |
| 794 |
|
| 795 |
// Get an item's title |
| 796 |
function getTitle(meta) { |
| 797 |
if (meta.article_title && meta.article_title.length > 0) { |
| 798 |
return 'article_title'; |
| 799 |
return { |
| 800 |
prop: 'article_title', |
| 801 |
value: meta.article_title |
| 802 |
}; |
| 803 |
} else if (meta.title && meta.title.length > 0) { |
| 804 |
return 'title'; |
| 805 |
return { |
| 806 |
prop: 'title', |
| 807 |
value: meta.title |
| 808 |
}; |
| 809 |
} |
| 810 |
}; |
| 811 |
|
| 812 |
// Create a metadata row |
| 813 |
function createMetadataRow(data, meta, prop) { |
| 814 |
if (!meta[prop]) return; |
| 815 |
|
| 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] + ': '; |
| 821 |
|
| 822 |
// Add a link to the availability URL if appropriate |
| 823 |
var value; |
| 824 |
if (!data.url) { |
| 825 |
value = document.createElement('span'); |
| 826 |
} else { |
| 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); |
| 831 |
} |
| 832 |
value.classList.add('metadata-value'); |
| 833 |
value.innerText = meta[prop]; |
| 834 |
div.appendChild(label); |
| 835 |
div.appendChild(value); |
| 836 |
|
| 837 |
return div; |
| 838 |
} |
| 839 |
|
| 840 |
// A render function for displaying metadata |
| 841 |
function createMetadata(x, y, data) { |
| 842 |
// If the fetch failed |
| 843 |
if (data.failed.length > 0) { |
| 844 |
return data.failed; |
| 845 |
} |
| 846 |
|
| 847 |
// If we've not yet got any metadata back |
| 848 |
if (Object.keys(data.metadata).length === 0) { |
| 849 |
return ill_populate_waiting; |
| 850 |
} |
| 851 |
|
| 852 |
var core = ['doi', 'pmid', 'issn', 'title', 'year', 'issue', 'pages', 'publisher', 'article_title', 'article_author', 'volume']; |
| 853 |
var meta = data.metadata; |
| 854 |
|
| 855 |
var container = document.createElement('div'); |
| 856 |
container.classList.add('metadata-container'); |
| 857 |
|
| 858 |
// Create the title row |
| 859 |
var title = getTitle(meta); |
| 860 |
if (title) { |
| 861 |
// Remove the title element from the props |
| 862 |
// we're about to iterate |
| 863 |
core = core.filter(function (i) { |
| 864 |
return i !== title; |
| 865 |
}); |
| 866 |
var titleRow = createMetadataRow(data, meta, title); |
| 867 |
container.appendChild(titleRow); |
| 868 |
} |
| 869 |
|
| 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); |
| 876 |
if (div) { |
| 877 |
remainder.appendChild(div); |
| 878 |
} |
| 879 |
}); |
| 880 |
container.appendChild(remainder); |
| 881 |
|
| 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); |
| 892 |
|
| 893 |
return container.outerHTML; |
| 894 |
}; |
| 895 |
|
| 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; |
| 904 |
}); |
| 905 |
} |
| 906 |
} |
| 907 |
|
| 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'); |
| 913 |
|
| 914 |
// Display or hide based on its current state |
| 915 |
var display = window.getComputedStyle(meta).display; |
| 916 |
|
| 917 |
meta.style.display = display === 'block' ? 'none' : 'block'; |
| 918 |
|
| 919 |
// Update the More / Less text |
| 920 |
ev.target.innerText = ' [ ' + (display === 'none' ? ill_batch_metadata_less : ill_batch_metadata_more) + ' ]'; |
| 921 |
} |
| 922 |
} |
| 923 |
|
| 924 |
// A render function for the link to a request ID |
| 925 |
function createRequestId(x, y, data) { |
| 926 |
return data.requestId || '-'; |
| 927 |
} |
| 928 |
|
| 929 |
function createRequestStatus(x, y, data) { |
| 930 |
return data.requestStatus || '-'; |
| 931 |
} |
| 932 |
|
| 933 |
function buildTable(identifiers) { |
| 934 |
table = KohaTable('identifier-table', { |
| 935 |
processing: true, |
| 936 |
deferRender: true, |
| 937 |
ordering: false, |
| 938 |
paging: false, |
| 939 |
searching: false, |
| 940 |
autoWidth: false, |
| 941 |
columns: [ |
| 942 |
{ |
| 943 |
data: 'type', |
| 944 |
width: '13%', |
| 945 |
render: createIdentifierType |
| 946 |
}, |
| 947 |
{ |
| 948 |
data: 'value', |
| 949 |
width: '25%', |
| 950 |
className: 'identifier' |
| 951 |
}, |
| 952 |
{ |
| 953 |
data: 'metadata', |
| 954 |
render: createMetadata |
| 955 |
}, |
| 956 |
{ |
| 957 |
data: 'requestId', |
| 958 |
width: '6.5%', |
| 959 |
render: createRequestId |
| 960 |
}, |
| 961 |
{ |
| 962 |
data: 'requestStatus', |
| 963 |
width: '6.5%', |
| 964 |
render: createRequestStatus |
| 965 |
}, |
| 966 |
{ |
| 967 |
width: '18%', |
| 968 |
render: createActions, |
| 969 |
className: 'action-column' |
| 970 |
} |
| 971 |
], |
| 972 |
createdRow: function (row, data) { |
| 973 |
if (data.failed.length > 0) { |
| 974 |
row.classList.add('fetch-failed'); |
| 975 |
} |
| 976 |
} |
| 977 |
}); |
| 978 |
} |
| 979 |
|
| 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>'; |
| 982 |
} |
| 983 |
|
| 984 |
// Redraw the table |
| 985 |
function updateTable() { |
| 986 |
if (!table) return; |
| 987 |
tableEl.style.display = tableContent.data.length > 0 ? 'table' : 'none'; |
| 988 |
tableEl.style.width = '100%'; |
| 989 |
table.api() |
| 990 |
.clear() |
| 991 |
.rows.add(tableContent.data) |
| 992 |
.draw(); |
| 993 |
}; |
| 994 |
|
| 995 |
function identifyIdentifier(identifier) { |
| 996 |
var matches = []; |
| 997 |
|
| 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) { |
| 1006 |
matches.push({ |
| 1007 |
type: identifierType, |
| 1008 |
value: match.groups.identifier |
| 1009 |
}); |
| 1010 |
} |
| 1011 |
}); |
| 1012 |
return matches; |
| 1013 |
} |
| 1014 |
|
| 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'; |
| 1024 |
}); |
| 1025 |
var el = document.getElementById('textarea-errors'); |
| 1026 |
el.style.display = 'flex'; |
| 1027 |
} |
| 1028 |
} |
| 1029 |
|
| 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'; |
| 1039 |
} |
| 1040 |
|
| 1041 |
function manageBatchItemsDisplay() { |
| 1042 |
batchItemsDisplay.style.display = batch.data.id ? 'block' : 'none' |
| 1043 |
}; |
| 1044 |
|
| 1045 |
function updateBatchInputs() { |
| 1046 |
nameInput.value = batch.data.name || ''; |
| 1047 |
cardnumberInput.value = batch.data.cardnumber || ''; |
| 1048 |
branchcodeSelect.value = batch.data.branchcode || ''; |
| 1049 |
} |
| 1050 |
|
| 1051 |
function debounce(func) { |
| 1052 |
var timeout; |
| 1053 |
return function (...args) { |
| 1054 |
return new Promise(function (resolve) { |
| 1055 |
if (timeout) { |
| 1056 |
clearTimeout(timeout); |
| 1057 |
} |
| 1058 |
timeout = setTimeout(function () { |
| 1059 |
return resolve(func(...args)); |
| 1060 |
}, debounceDelay); |
| 1061 |
}); |
| 1062 |
} |
| 1063 |
} |
| 1064 |
|
| 1065 |
function patronAutocomplete() { |
| 1066 |
patron_autocomplete( |
| 1067 |
$('#batch-form #batchcardnumber'), |
| 1068 |
{ |
| 1069 |
'on-select-callback': function( event, ui ) { |
| 1070 |
$("#batch-form #batchcardnumber").val( ui.item.cardnumber ); |
| 1071 |
return false; |
| 1072 |
} |
| 1073 |
} |
| 1074 |
); |
| 1075 |
}; |
| 1076 |
|
| 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; |
| 1086 |
return a; |
| 1087 |
}; |
| 1088 |
|
| 1089 |
})(); |