Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
box_loan.js 29.10 KiB
/*
 * 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);
    }
});