From cf3a1c49d1453d47e24243a797ff942c5d14af3d Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Mon, 15 Aug 2022 12:19:31 +0100 Subject: [PATCH] Bug 30979: Add checkout modal to the OPAC This patch adds a new self checkout modal to the OPAC when OpacTrustedCheckout is enabled and a user is logged in. The new modal allows an end user to scan an item barcode to checkout. We check for item existance and availability and then check the item out after any confirmations have been displayed. --- .../bootstrap/en/includes/masthead.inc | 7 + .../bootstrap/en/includes/modals/checkout.inc | 40 +++ .../bootstrap/en/includes/opac-bottom.inc | 11 + .../opac-tmpl/bootstrap/js/modals/checkout.js | 228 ++++++++++++++++++ 4 files changed, 286 insertions(+) create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/includes/modals/checkout.inc create mode 100644 koha-tmpl/opac-tmpl/bootstrap/js/modals/checkout.js diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc index 941805f7a62..20b34bce959 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc +++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc @@ -81,6 +81,13 @@ [% END # / IF virtualshelves %] + [% IF ( Koha.Preference( 'OpacTrustedCheckout' ) && loggedinusername ) %] + + [% END %] [% IF Koha.Preference( 'opacuserlogin' ) == 1 || Koha.Preference( 'EnableOpacSearchHistory') || Koha.Preference( 'opaclanguagesdisplay' ) %] diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/modals/checkout.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/modals/checkout.inc new file mode 100644 index 00000000000..6a19e717de8 --- /dev/null +++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/modals/checkout.inc @@ -0,0 +1,40 @@ + + diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc index 4fab59077de..187f8e612df 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc +++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc @@ -117,6 +117,10 @@ [% END # /IF OpacLangSelectorMode == 'both' || OpacLangSelectorMode == 'footer' %] [% END # / UNLESS is_popup %] +[% IF ( Koha.Preference( 'OpacTrustedCheckout' ) && loggedinusername ) %] + [% INCLUDE 'modals/checkout.inc' %] +[% END %] + [% Asset.js("lib/jquery/jquery-3.6.0.min.js") | $raw %] [% Asset.js("lib/jquery/jquery-migrate-3.3.2.min.js") | $raw %] @@ -124,6 +128,9 @@ [% Asset.js("lib/bootstrap/js/bootstrap.bundle.min.js") | $raw %] [% Asset.js("lib/fontfaceobserver.min.js") | $raw %] [% Asset.js("lib/enquire.min.js") | $raw %] + [% Asset.js("js/global.js") | $raw %] [% IF ( OPACAmazonCoverImages || SyndeticsCoverImages ) %] +[% INCLUDE 'js-date-format.inc' %] [% PROCESS jsinclude %] [% IF ( Koha.Preference('OPACUserJS') ) %] [% END %] [% END %] +[% IF ( Koha.Preference( 'OpacTrustedCheckout' ) && loggedinusername ) %] + [% Asset.js("js/modals/checkout.js") | $raw %] +[% END %] [% KohaPlugins.get_plugins_opac_js | $raw %] diff --git a/koha-tmpl/opac-tmpl/bootstrap/js/modals/checkout.js b/koha-tmpl/opac-tmpl/bootstrap/js/modals/checkout.js new file mode 100644 index 00000000000..797d0e58610 --- /dev/null +++ b/koha-tmpl/opac-tmpl/bootstrap/js/modals/checkout.js @@ -0,0 +1,228 @@ +$(document).ready(function() { + + let checkouts_count = 0; + + function addResult(type, code, data) { + let result = ''; + if (type == 'danger') { + result += '
'; + } else if (type == 'warning') { + result += '
'; + } else if (type == 'info') { + result += '
'; + } else { + result += '
'; + } + + if (code == 'NOT_FOUND') { + result += _("Item '%s' not found").format(data); + } else if (code == 'RENEW_ISSUE') { + result += _("Item will be renewed").format(data); + } else if (code == 'OTHER_CHARGES') { + result += _("Your account currently has outstanding charges of '%s'").format(data); + } else if (code == 'DEBT') { + result += _("Your account is currently in debt by '%s'").format(data); + } else if (code == 'ISSUED_TO_ANOTHER') { + result += _("This item appears to be checked out to another patron, please return it to the desk"); + } else if (code == 'RESERVED') { + result += _("This item appears to be reserved for another patron, please return it to the desk"); + } else if (code == 'NOT_FOR_LOAN') { + result += _("This item is not normally for loan, please select another or ask at the desk"); + } else if (code == 'WTHDRAWN') { + result += _("This item is marked withdrawn, please select another or ask at the desk"); + } else if (code == 'EMPTY') { + result += _("Please enter the barcode for the item you wish to checkout"); + } else { + result += _("Message code '%s' with data '%s'").format(code, data); + } + + result += '
'; + $('#availabilityResult').append(result); + }; + + // On modal show, clear any prior results and set focus + $('#checkoutModal').on('shown.bs.modal', function(e) { + $('#checkoutResults').replaceWith('
'); + $('#availabilityResult').replaceWith('
'); + $('#checkoutsTable').hide(); + $('#checkout_barcode').val("").focus(); + }); + + // On modal submit + $('#checkoutModal').on('click', '#checkoutSubmit', function(e) { + + // Get item from barcode + let external_id = $('#checkout_barcode').val(); + if ( external_id === '' ) { + addResult('warning', 'EMPTY'); + return; + } + + let item_id; + let items = $.ajax({ + url: '/api/v1/public/items?external_id=' + external_id, + dataType: 'json', + type: 'GET' + }); + + $('#availabilityResult').replaceWith('
'); + + // Get availability of the item + let availability = items.then( + function(data, textStatus, jqXHR) { + if (data.length == 1) { + item_id = data[0].item_id; + return $.ajax({ + url: '/api/v1/public/checkouts/availability?item_id=' + item_id + '&patron_id=' + logged_in_user_id, + type: 'GET', + contentType: "json" + }); + } else { + addResult('danger', 'NOT_FOUND', external_id); + } + }, + function(data, textStatus, jqXHR) { + addResult('danger', 'NOT_FOUND', external_id); + console.log('Items request failed with: ' + textStatus); + console.log(data); + } + ); + + let checkout = availability.then( + function(data, textStatus, jqXHR) { + let result; + // blocked + if (Object.keys(data.blockers).length >= 1) { + for (const key in data.blockers) { + if (data.blockers.hasOwnProperty(key)) { + addResult('danger', `${key}`, `${data.blockers[key]}`); + console.log(`${key}: ${data.blockers[key]}`); + } + } + } + // requires confirmation + else if (Object.keys(data.confirms).length >= 1) { + for (const key in data.confirms) { + if (data.confirms.hasOwnProperty(key)) { + addResult('warning', `${key}`, `${data.confirms[key]}`); + } + } + $('#checkout_barcode').prop("readonly", true); + $('#checkoutSubmit').replaceWith(''); + console.log("Request succeeded confirmation is required"); + } + // straight checkout + else { + let params = { + "checkout_id": undefined, + "patron_id": logged_in_user_id, + "item_id": item_id, + "due_date": undefined, + "library_id": undefined, + "issuer_id": undefined, + "checkin_date": undefined, + "last_renewed_date": undefined, + "renewals_count": undefined, + "unseen_renewals": undefined, + "auto_renew": undefined, + "auto_renew_error": undefined, + "timestamp": undefined, + "checkout_date": undefined, + "onsite_checkout": false, + "note": undefined, + "note_date": undefined, + "note_seen": undefined, + }; + result = $.ajax({ + url: '/api/v1/public/checkouts', + type: 'POST', + data: JSON.stringify(params), + contentType: "json" + }); + } + + // warnings to display + if (Object.keys(data.warnings).length >= 1) { + for (const key in data.warnings) { + if (data.warnings.hasOwnProperty(key)) { + addResult('info', `${key}`, `${data.warnings[key]}`); + } + } + } + + // return a rejected promise if we've reached here + return result ? result : $.Deferred().reject('Checkout halted'); + }, + function(data, textStatus, jqXHR) { + console.log('Items request failed with: ' + textStatus); + console.log(data); + } + ); + + checkout.then( + function(data, textStatus, jqXHR) { + $('#checkout_barcode').val("").prop("readonly", false).focus(); + $('#checkoutResults').replaceWith('
' + _("Item '%s' was checked out").format(external_id) + '
'); + $('#checkoutsTable').show(); + $('#checkoutsTable > tbody').append('' + external_id +''+ $date(data.due_date) +''); + checkouts_count++; + $('#checkoutsCount').html(checkouts_count); + // data retrieved from url2 as provided by the first request + }, + function(data, textStatus, jqXHR) { + console.log("checkout.then failed"); + } + ); + + }); + + $('#checkoutModal').on('click', '#checkoutConfirm', function(e) { + let external_id = $('#checkout_barcode').val(); + let item_id = $('#item_id').val(); + let token = $('#confirm_token').val(); + let params = { + "checkout_id": undefined, + "patron_id": logged_in_user_id, + "item_id": item_id, + "due_date": undefined, + "library_id": undefined, + "issuer_id": undefined, + "checkin_date": undefined, + "last_renewed_date": undefined, + "renewals_count": undefined, + "unseen_renewals": undefined, + "auto_renew": undefined, + "auto_renew_error": undefined, + "timestamp": undefined, + "checkout_date": undefined, + "onsite_checkout": false, + "note": undefined, + "note_date": undefined, + "note_seen": undefined, + }; + let checkout = $.ajax({ + url: '/api/v1/public/checkouts?confirmation=' + token, + type: 'POST', + data: JSON.stringify(params), + contentType: "json" + }); + + checkout.then( + function(data, textStatus, jqXHR) { + $('#item_id').remove; + $('#confirm_token').remove; + $('#checkoutResults').replaceWith('
' + _("Item '%s' was checked out").format(external_id) + '
'); + $('#checkout_barcode').val("").prop("readonly", false).focus(); + $('#checkoutsTable').show(); + $('#checkoutsTable > tbody').append('' + external_id +''+ $date(data.due_date) +''); + $('#checkoutConfirm').replaceWith(''); + checkouts_count++; + $('#checkoutsCount').html(checkouts_count); + // data retrieved from url2 as provided by the first request + }, + function(data, textStatus, jqXHR) { + console.log("checkout.then failed"); + } + ); + }); +}); -- 2.39.0