/* * This file is a part of the LinkAhead Project. * * Copyright (C) 2020-2024 Henrik tom Wörden (h.tomwoerden@indiscale.com) * Copyright (C) 2020-2024 IndiScale GmbH (info@indiscale.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ /** * Return the formatted date of today. */ function get_formatted_date(split) { split = split || "."; var date = new Date(); var tzoffset = (new Date()).getTimezoneOffset() * 60000; return ((new Date(Date.now() - tzoffset)).toISOString()).split(split)[0]; } /** * This function retrieves an html form from the html subdirectory. * */ var getHTMLForm = async function(pageName, variables) { var site = await connection.get("webinterface/html/forms/" + pageName + ".html", "html"); var htmltext = site.getElementById("caosdb-form").innerHTML; var req = /\{([0-9a-z-A-Z_]+)\}/g; return htmltext.replace(req, (match, p1) => { return variables[p1] || ""; }); } /** * Box Loan, refactored code. */ var box_loan = function(logger, box_loan_config) { const datamodel = box_loan_config.datamodel; const _dismiss_button = '<button class="btn btn-secondary box-loan-btn">OK</button>' const _server_did_not_respond = "The server did not respond. Please reload the page."; const _return_box_button = `<a title="Return ${datamodel.box}." class="btn btn-link box-loan-btn">Return ${datamodel.box}</a>`; const _borrow_box_button = `<a title="Borrow ${datamodel.box}." class="btn btn-link box-loan-btn">Borrow ${datamodel.box}</a>`; const _confirm_loan_button = '<a title="Confirm Loan." class="btn btn-link box-loan-btn">Confirm Loan</a>'; const _manual_return_button = '<a title="Manual Return." class="btn btn-link box-loan-btn">Manual Return</a>'; const _accept_return_button = '<a title="Accept Return" class="btn btn-link box-loan-btn">Accept Return</a>'; const _reject_return_button = '<a title="Reject Return." class="btn btn-link box-loan-btn">Reject Return</a>'; const _accept_loan_button = '<a title="Accept Borrow Request." class="btn btn-link box-loan-btn">Accept Loan Request</a>'; const _box_update_alert = ` <div class="alert alert-danger alert-dismissible" role="alert"> <h4>Update the Box</h4> <p> Are you sure that the box record is up-to-date (references etc.)?<br>Possibly have a look at the notes in the loan by clicking on the "References" button.<br> Do you you want to apply further changes? </p><p> <button type="button" class="btn btn-danger caosdb-f-manual-return-cancel">Cancel and change the box manually</button> <button type="button" class="btn btn-secondary caosdb-f-manual-return-proceed">The box is up-to-date.</button> </p> </div> `; /** * Return a HTML span which tells the user to wait for something. * * The optional text parameter can be used to set the text that is shown. * * @param text * @return HTMLElement */ var getPleaseWaitSpan = function(text = "Processing. Please wait...") { return $('<span>' + text + '</span>')[0]; } /** * Return the actions panel of a box entity. */ var get_actions_panel = function(box) { return $(box).find('.caosdb-entity-actions-panel'); } /** * Open a mail with recepient address address, subject subject and body text. */ this.open_mail_generic = function(address, subject, text) { const mailto = 'mailto:' + address + '?subject=' + subject + '&body=' + text; location.href = mailto; } /** * This function checks whether a property with name name * is present in the property list p. * This function is used for determining the loan state. */ var loan_state_function = function(p, name) { var p1 = p.filter(x => x.name == name); if (p1.length == 1 && p1[0].value.length > 0) { return true; } return false; } /** * This function returns a string on the state of the loan. */ var get_loan_state_string = function(loan) { const p = getProperties(loan); if (loan_state_function(p, datamodel.returned)) { return "returned"; } if (loan_state_function(p, datamodel.returnAccepted)) { return "return_accepted"; } if (loan_state_function(p, datamodel.returnRequested)) { return "return_requested"; } if (loan_state_function(p, datamodel.lent)) { return "lent"; } if (loan_state_function(p, datamodel.loanAccepted)) { return "loan_accepted"; } if (loan_state_function(p, datamodel.loanRequested)) { return "loan_requested"; } const loan_id = getEntityID(loan); throw new Error(`Unknown loan state for Loan ${loan_id}.`); } /** * Query for a Loan entity which references the box and which has no * `returned` property. */ var get_active_loans = async function(boxid) { return await query(`FIND ${datamodel.loan} WITH ${datamodel.box} -> ${boxid} AND WHICH DOES NOT HAVE A ${datamodel.returned}`); } /** * Returns detailed information about the loan state of this box: */ var get_loan_state = async function(box) { const loan_state = { loan: undefined, state: undefined, }; const loan = (await get_active_loans(box.id))[0]; if (typeof loan === "undefined") { // no loan found return loan_state; } loan_state.loan = loan loan_state.state = get_loan_state_string(loan); return loan_state; } var run_script = async function(script, data) { const json_str = JSON.stringify(data); const params = { "-p0": { "filename": "form.json", "blob": new Blob([json_str], { type: "application/json" }) } }; try { const response = await connection.runScript(script, params); return form_elements.parse_script_result(response); } catch (error) { return error; } } var show_result = function(container, result, box) { const dismissable = $('<div/>'); const restore = $(container).children(); restore.hide(); $(container).append(dismissable); if (typeof result === "undefined") { $(dismissable).append(_server_did_not_respond); } if (result["code"] == 0) { // success! $(dismissable).append(result["stdout"]); } else if (result["code"] > 0) { // :( $(dismissable) .append(result["stderr"]) .append(result["stdout"]); } else { $(dismissable).append(result); } const dismiss_btn = $(_dismiss_button); dismiss_btn.click(function() { dismissable.remove(); restore.show(); init(box); }); dismissable.append(dismiss_btn); } var get_request_data = function(form) { const email = $(form).find("#email").val(); const first_name = $(form).find("#first-name").val(); const last_name = $(form).find("#last-name").val(); const comment = $(form).find("#loan-comment").val(); const expected_return_date = $(form).find("#expected-return").val(); const current_location = $(form).find("[name='location']").val(); const destination = $(form).find("[name='destination']").val(); const exhaust_contents = $(form).find("#exhaust-contents").prop("checked"); window.localStorage["borrower_email"] = email; window.localStorage["borrower_first_name"] = first_name; window.localStorage["borrower_last_name"] = last_name; window.localStorage["borrower_return_date"] = expected_return_date if (destination) { window.localStorage["borrower_destination"] = destination; } return { email: email, first_name: first_name, last_name: last_name, comment: comment, expected_return_date: expected_return_date, current_location: current_location, exhaust_contents: exhaust_contents, destination: destination }; } /** * Generate a borrow function for a box. * This function can also be used to assign values to multiple boxes. */ var borrow_function = async function(form, box) { const wait = getPleaseWaitSpan(); const actions_panel = get_actions_panel(box); $(actions_panel).append(wait).find('btn').hide() $(form).hide() const loan_request = get_request_data(form); $.extend(loan_request, { box: getEntityID(box), }); const result = await run_script("loan_management/request_loan.py", loan_request); $(wait).remove(); $(form).remove(); show_result(actions_panel, result, box); } var _add_form = async function(box, form_generator, submit_callback) { const form = $(await form_generator(getEntityID(box))); if (typeof form === "undefined") { return; } form.insertAfter($(box).find('.caosdb-entity-actions-panel')).hide(); form.submit(() => { submit_callback(form[0], box); return false; }); form.find("label").has(":required").prepend('<span style="font-size: 10px; color: red; margin-right: 4px; font-weight: 100;">*</span>'); // the footer adds the default styling as well as the *required-marker const submit = form.find(":input[type='submit']")[0]; const footer = form_elements.make_footer(); footer.append(submit); form.find("form").append(footer); return form[0]; } var _init_validator = function(form) { // initiate validator // Workaround for missing checkValidity function: if (form.checkValidity == undefined) { form.checkValidity = () => true; } if (form.reportValidity == undefined) { form.reportValidity = () => logger.debug("Workaround for validity report"); } form_elements.init_validator(form); const submit = $(form).find(":input[type='submit']")[0]; form_elements.toggle_submit_button_form_valid(form, submit); } /** * Add Return button to boxes' actions panels. */ var add_return_button = async function(box) { const but = $(_return_box_button); const form = await _add_form(box, generate_return_form, return_function); if (typeof form === "undefined") { return; } get_actions_panel(box).append(but); const config = { type: "reference_drop_down", name: "location", label: "", required: true, query: `FIND Record ${datamodel.location}`, make_desc: getEntityName, } const dd = form_elements.make_form_field(config); $(dd).find(".col-sm-9").removeClass("col-sm-9"); $(dd).find(".col-sm-3").removeClass("col-sm-3"); $(form).find("#current-location").replaceWith(dd); $(form).find("label").css("display", "block"); _init_validator(form); but.click(() => { but.remove(); $(form).toggle() }); } /** * If the user is administrator (or a data curator) a button is added * to accept the borrow request. Else a message is added "pending loan request". */ var add_borrow_accept_button = function(box, loan) { const actionPanel = get_actions_panel(box); if (userIsAdministrator() === true || userHasRole("curator") === true) { const but = $(_accept_loan_button); but.click(() => { but.remove(); accept_loan_function(box); }); actionPanel.append(but); } else { actionPanel.append($(create_link_to_loan(loan, "Pending loan request."))); } } var create_link_to_loan = function(loan, text) { const loan_id = getEntityID(loan); const href = `${connection.getBasePath()}Entity/${loan_id}`; const title = "Open loan entity."; const classes = "btn btn-link box-loan-btn"; return `<a title="${title}" class="${classes}" href="${href}">${text}<a>`; } /** * Add Borrow buttons to the boxes' action panels */ var add_borrow_button = async function(box) { const but = $(_borrow_box_button); get_actions_panel(box).append(but); const form = await _add_form(box, generate_form_borrow_checkout, borrow_function); replace_loan_destination(form); $(form).find("label").css("display", "block"); _init_validator(form); but.click(() => { but.remove(); $(form).toggle() }); } var open_accept_loan_mail = async function(loan, box) { try { const borrower = (await retrieve(getProperty(loan, "Borrower"), case_sensitive = false))[0]; const email = getProperty(borrower, datamodel.email, case_sensitive = false); const firstName = getProperty(borrower, datamodel.firstName, case_sensitive = false); const lastName = getProperty(borrower, datamodel.lastName, case_sensitive = false); const bn = getProperty(box, datamodel.number, case_sensitive = false); const date = getProperty(loan, datamodel.expectedReturn, case_sensitive = false); const subject = `Loan Request for ${datamodel.box} ${bn} accepted`; const body = `Dear ${firstName} ${lastName}, %0D%0D` + `You can pickup ${datamodel.box} number ${bn} on ${date}. %0D%0D` + `Kind Regards,%0D%0D ${getUserName()}`; open_mail_generic(email, subject, body) } catch (err) { logger.error(err); } } var accept_loan_function = async function(box) { const wait = getPleaseWaitSpan(); const actions_panel = get_actions_panel(box); $(actions_panel).append(wait).find('btn').hide() const loan = (await get_active_loans(box.id))[0]; const accept_loan_request = { loan: getEntityID(loan), }; const result = await run_script("loan_management/accept_loan_request.py", accept_loan_request); $(wait).remove(); show_result(actions_panel, result, box); open_accept_loan_mail(loan, box); } var manual_return_function = async function(box) { const wait = getPleaseWaitSpan(); const actions_panel = get_actions_panel(box); $(actions_panel).append(wait).find('btn').hide() const loan = (await get_active_loans(box.id))[0]; const manual_return_request = { loan: getEntityID(loan), }; const result = await run_script("loan_management/manual_return.py", manual_return_request); $(wait).remove(); show_result(actions_panel, result, box); } /** * Add Manual Return button to boxes' actions panels. */ var add_manual_return_button = function(box, loan) { const actionPanel = get_actions_panel(box); if (userIsAdministrator() === true || userHasRole("curator") === true) { const but = $(_manual_return_button); actionPanel.append(but); const box_update_alert = $(_box_update_alert); box_update_alert.find(".caosdb-f-manual-return-cancel").click(() => { but.show(); box_update_alert.hide(); }); box_update_alert.find(".caosdb-f-manual-return-proceed").click(() => { but.remove(); box_update_alert.remove(); manual_return_function(box); }); actionPanel.append(box_update_alert); box_update_alert.hide(); but.click(() => { but.hide(); box_update_alert.show(); }); } else { actionPanel.append($(create_link_to_loan(loan, "Pending return."))); } } var confirm_loan_function = async function(box) { const wait = getPleaseWaitSpan(); const actions_panel = get_actions_panel(box); $(actions_panel).append(wait).find('btn').hide() const loan = (await get_active_loans(box.id))[0]; const confirm_loan_request = { loan: getEntityID(loan), }; const result = await run_script("loan_management/confirm_loan.py", confirm_loan_request); $(wait).remove(); show_result(actions_panel, result, box); } var open_reject_return_mail = async function(loan, box) { try { const borrower = (await retrieve(getProperty(loan, "Borrower"), case_sensitive = false))[0]; const email = getProperty(borrower, datamodel.email, case_sensitive = false); const firstName = getProperty(borrower, datamodel.firstName, case_sensitive = false); const lastName = getProperty(borrower, datamodel.lastName, case_sensitive = false); const bn = getProperty(box, datamodel.number, case_sensitive = false); const subject = `Return Request for ${datamodel.box} ${bn} rejected`; const body = `Dear ${firstName} ${lastName}, %0D%0D` + `your return request for ${datamodel.box} number ${bn} cannot be accepted.` + `Kind Regards,%0D ${getUserName()}`; open_mail_generic(email, subject, body) } catch (err) { logger.error(err); } } var open_accept_return_mail = async function(loan, box) { try { const borrower = (await retrieve(getProperty(loan, "Borrower"), case_sensitive = false))[0]; const email = getProperty(borrower, datamodel.email, case_sensitive = false); const firstName = getProperty(borrower, datamodel.firstName, case_sensitive = false); const lastName = getProperty(borrower, datamodel.lastName, case_sensitive = false); const bn = getProperty(box, datamodel.number, case_sensitive = false); const date = getProperty(loan, datamodel.expectedReturn, case_sensitive = false); const subject = `Return Request for ${datamodel.box} ${bn} accepted`; const body = `Dear ${firstName} ${lastName}, %0D%0D` + `please return ${datamodel.box} number ${bn} on ${date}. %0D%0D` + `Kind Regards,%0D ${getUserName()}`; open_mail_generic(email, subject, body) } catch (err) { logger.error(err); } } var accept_return_function = async function(box) { const wait = getPleaseWaitSpan(); const actions_panel = get_actions_panel(box); $(actions_panel).append(wait).find('btn').hide() const loan = (await get_active_loans(box.id))[0]; const accept_return_request = { loan: getEntityID(loan), }; const result = await run_script("loan_management/accept_return_request.py", accept_return_request); $(wait).remove(); show_result(actions_panel, result, box); open_accept_return_mail(loan, box); } var reject_return_function = async function(box) { const wait = getPleaseWaitSpan(); const actions_panel = get_actions_panel(box); $(actions_panel).append(wait).find('btn').hide() const loan = (await get_active_loans(box.id))[0]; const reject_return_request = { loan: getEntityID(loan), }; const result = await run_script( "loan_management/reject_return_request.py", reject_return_request); $(wait).remove(); show_result(actions_panel, result, box); open_reject_return_mail(loan, box); } /** * Add two buttons for confirming or rejecting a return request. */ var add_confirm_reject_return = function(box, loan) { const actionPanel = get_actions_panel(box); if (userIsAdministrator() === true || userHasRole("curator") === true) { const confirmButton = $(_accept_return_button); actionPanel.append(confirmButton); const rejectButton = $(_reject_return_button); actionPanel.append(rejectButton); confirmButton.click(() => { confirmButton.remove(); rejectButton.remove(); accept_return_function(box); }); rejectButton.click(() => { confirmButton.remove(); rejectButton.remove(); reject_return_function(box); }); } else { actionPanel.append($(create_link_to_loan(loan, "Pending return request."))); } } /** * Add a button for confirming the loan. */ var add_confirm_loan_button = function(box, loan) { const actionsPanel = get_actions_panel(box); if (userIsAdministrator() === true || userHasRole("curator") === true) { const but = $(_confirm_loan_button); actionsPanel.append(but); but.click(() => { but.remove(); confirm_loan_function(box); }); } else { actionsPanel.append($(create_link_to_loan(loan, "Loan request accepted."))); } } var return_function = async function(form, box) { const wait = getPleaseWaitSpan(); const actions_panel = get_actions_panel(box); $(actions_panel).append(wait).find('btn').hide() $(form).hide(); const return_request = get_request_data(form); const loan = (await get_active_loans(box.id))[0]; $.extend(return_request, { box: getEntityID(box), loan: getEntityID(loan), }); const result = await run_script("loan_management/request_return.py", return_request); $(wait).remove(); $(form).remove(); show_result(actions_panel, result, box); } var replace_loan_destination = async function(node) { const config = { type: "reference_drop_down", name: "destination", label: "", required: true, query: `FIND Record ${datamodel.location}`, make_desc: getEntityName, } const dd = form_elements.make_form_field(config); $(dd).find(".col-sm-9").removeClass("col-sm-9"); $(dd).find(".col-sm-3").removeClass("col-sm-3"); $(node).find("#loan-destination").replaceWith(dd); } var add_menu_entry = async function () { if ($("#caosdb-f-bookmarks-borrow-all").length > 0) { return; } var modal = $(await generate_form_borrow_checkout(undefined, "loan-forms/borrow_all_bookmarked")).find(".modal"); replace_loan_destination(modal); $(modal).find("label").css("display", "block"); $('body').append(modal); modal.on('show.bs.modal', function() { modal.find(".modal-footer").show(); }); modal.find('.btn-primary').on('click', async function() { var loan_request = get_request_data(modal.find("form")[0]); // TODO this is a dependency that cannot be expressed by adding the module to the call of this module // because the ext_bookmarks module is initialized too late loan_request["box"] = ext_bookmarks.get_bookmarks(); const result = await run_script("loan_management/request_loan.py", loan_request); show_result(modal.find(".modal-body"), result, {}); modal.find(".modal-footer").hide(); modal.find(".box-loan-btn").attr("data-bs-dismiss", "modal") }); _init_validator(modal[0]); const borrow_btn = $('<li class="disabled" id="caosdb-f-bookmarks-borrow-all" data-bs-toggle="modal" data-bs-target="#caosdb-f-borrow-all-form-modal" title="Borrow all bookmarked items"> <a class="dropdown-item">Borrow all</a></li>'); $("#caosdb-f-bookmarks-clear").parent().append(borrow_btn); } /** * Add buttons for borrowing boxes. */ var add_buttons = function(boxes) { $(boxes).find('.box-loan-btn').remove(); $(boxes).each(async function() { const loan_state = await get_loan_state(this); if (typeof loan_state.loan !== "undefined") { if (loan_state.state == "loan_requested") { add_borrow_accept_button(this, loan_state.loan); } else if (loan_state.state == "loan_accepted") { add_confirm_loan_button(this, loan_state.loan); } else if (loan_state.state == "lent") { add_return_button(this); } else if (loan_state.state == "return_requested") { add_confirm_reject_return(this, loan_state.loan); } else if (loan_state.state == "return_accepted") { add_manual_return_button(this, loan_state.loan); } } else { add_borrow_button(this); } }); } /** * This function generates a form for borrowing boxes. * selectform can be used to generate the form for multiple boxes. * In case selectform is true, box will be ignored. */ var generate_form_borrow_checkout = async function(boxid, formhtml) { const email = window.localStorage["borrower_email"]; const firstname = window.localStorage["borrower_first_name"]; const lastname = window.localStorage["borrower_last_name"]; const destination = window.localStorage["borrower_destination"]; var exp_return = window.localStorage["borrower_return_date"]; if (exp_return < Date.now()) { exp_return = ""; } formhtml = formhtml || 'loan-forms/borrow_checkout'; return getHTMLForm(formhtml, { boxid: boxid, first_name: firstname, last_name: lastname, email: email, destination: destination, expected_return_date: exp_return, mindate: get_formatted_date("T"), lentType: datamodel.box }); } /** * Generate form for a new Return Request, with fields * * boxid (hidden) * first_name (pre-filled) * last_name (pre-filled) * email (pre-filled) * expectedreturn (pre-filled) * currentlocation (pre-filled) * comment */ var generate_return_form = async function(boxid) { const loan = (await get_active_loans(boxid))[0]; if (typeof loan === "undefined") { // no loan found logger.error("No loan found for box", boxid); return ; } const borrower = (await retrieve(getProperty(loan, "Borrower", case_sensitive = false)))[0]; if (typeof borrower === "undefined") { // no loan found logger.error("No borrower found for loan", loan); return ; } var email = getProperty(borrower, datamodel.email, case_sensitive = false); var first_name = getProperty(borrower, datamodel.firstName, case_sensitive = false); var last_name = getProperty(borrower, datamodel.lastName, case_sensitive = false); var exp_return = getProperty(loan, datamodel.expectedReturn, case_sensitive = false); var cur_loc = getProperty(loan, "destination", case_sensitive = false); return getHTMLForm("loan-forms/return_box", { boxid: boxid, first_name: first_name, last_name: last_name, email: email, expectedreturn: exp_return, currentlocation: cur_loc, mindate: get_formatted_date("T") }); } var init = function(boxes) { const init_boxes = boxes || $(".caosdb-entity-panel") .not("[data-version-successor]") .filter(function() { return $(this).find(".caosdb-parent-name").text().trim() === datamodel.box; }) .toArray(); add_menu_entry(); add_buttons(init_boxes); } return { init: init, add_buttons: add_buttons, get_active_loans: get_active_loans, get_loan_state_string: get_loan_state_string, }; }(log.getLogger("box_loan"), box_loan_config); $(document).ready(function() { if ("${BUILD_MODULE_BOX_LOAN}" == "ENABLED") { caosdb_modules.register(box_loan); } });