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.

Signed-off-by: Silvia Meakins <smeakins@eso.org>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
This commit is contained in:
Martin Renvoize 2022-08-15 12:19:31 +01:00 committed by Tomas Cohen Arazi
parent bc55051e49
commit 709255fdfa
Signed by: tomascohen
GPG key ID: 0A272EA1B2F3C15F
4 changed files with 304 additions and 0 deletions

View file

@ -81,6 +81,13 @@
</div> <!-- / .dropdown-menu -->
</li> <!-- / .nav-item.dropdown -->
[% END # / IF virtualshelves %]
[% IF ( Koha.Preference( 'OpacTrustedCheckout' ) && loggedinusername ) %]
<li class="nav-item">
<a href="#" class="nav-link" title="Self check out" id="comenulink" role="button" data-toggle="modal" data-target="#checkoutModal">
<i id="checkout-icon" class="fa fa-barcode fa-icon-black" aria-hidden="true"></i> <span class="checkout-label">Self checkout</span>
</a>
</li>
[% END %]
</ul> <!-- / .navbar-nav -->
[% IF Koha.Preference( 'opacuserlogin' ) == 1 || Koha.Preference( 'EnableOpacSearchHistory') || Koha.Preference( 'opaclanguagesdisplay' ) %]

View file

@ -0,0 +1,40 @@
<!-- Checkout form hidden by default, used for modal window -->
<div class="modal" id="checkoutModal" tabindex="-1" role="dialog" aria-labelledby="checkoutModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title" id="checkoutModalLabel">Self checkout</h3>
<button type="button" class="closebtn" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div id="checkoutResults"></div>
<div id="availabilityResult"></div>
<div class="form-group">
<label for="checkout_barcode">Enter item barcode: </label>
<input type="text" name="checkout_barcode" id="checkout_barcode" required="required"></input>
</div>
<table id="checkoutsTable" class="table table-bordered table-striped" style="width:100%;">
<thead>
<tr>
<th>Barcode</th>
<th>Due date</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td colspan="2" class="text-right"><span id="checkoutsCount"></span> items checked out</td>
</tr>
</tfoot>
</table>
</div>
<div class="modal-footer">
<button type="submit" id="checkoutSubmit" class="btn btn-primary">Submit</button>
<button type="button" id="checkoutClose" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>

View file

@ -117,6 +117,10 @@
[% END # /IF OpacLangSelectorMode == 'both' || OpacLangSelectorMode == 'footer' %]
[% END # / UNLESS is_popup %]
[% IF ( Koha.Preference( 'OpacTrustedCheckout' ) && loggedinusername ) %]
[% INCLUDE 'modals/checkout.inc' %]
[% END %]
<!-- JavaScript includes -->
[% Asset.js("lib/jquery/jquery-3.6.0.min.js") | $raw %]
[% Asset.js("lib/jquery/jquery-migrate-3.3.2.min.js") | $raw %]
@ -284,6 +288,7 @@ $(document).ready(function() {
}
});
</script>
[% INCLUDE 'js-date-format.inc' %]
[% PROCESS jsinclude %]
[% IF ( Koha.Preference('OPACUserJS') ) %]
<script>
@ -300,6 +305,9 @@ $(document).ready(function() {
</script>
[% END %]
[% END %]
[% IF ( Koha.Preference( 'OpacTrustedCheckout' ) && loggedinusername ) %]
[% Asset.js("js/modals/checkout.js") | $raw %]
[% END %]
[% KohaPlugins.get_plugins_opac_js | $raw %]
</body>
</html>

View file

@ -0,0 +1,249 @@
$(document).ready(function() {
let checkouts_count = 0;
let current_item;
function addResult(type, code, data) {
let result = '';
if (type == 'danger') {
result += '<div class="alert alert-danger">';
} else if (type == 'warning') {
result += '<div class="alert alert-warning">';
} else if (type == 'info') {
result += '<div class="alert alert-info">';
} else {
result += '<div class="alert alert-success">';
}
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' || code == 'RESERVED_WAITING') {
result += _("This item appears to be reserved for another patron, please return it to the desk");
} else if (code == 'TOO_MANY') {
result += _("You have reached the maximum number of checkouts allowed on your account");
} else if (code == 'AGE_RESTRICTION') {
result += _("This item is age restricted");
} else if (code == 'NO_MORE_RENEWALS') {
result += _("Maximum renewals reached for this item");
} 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 += '</div>';
$('#availabilityResult').append(result);
};
function addCheckout(checkout) {
// Alert that checkout was successful
$('#checkoutResults').replaceWith('<div id="checkoutResults" class="alert alert-success">' + _("Item '%s' was checked out").format(current_item.external_id) + '</div>');
// Cleanup input and unset readonly
$('#checkout_barcode').val("").prop("readonly", false).focus();
// Display checkouts table if not already visible
$('#checkoutsTable').show();
// Add checkout to checkouts table
$('#checkoutsTable > tbody').append('<tr><td>' + current_item.external_id +'</td><td>'+ $date(checkout.due_date) +'</td></tr>');
$('#checkoutsCount').html(++checkouts_count);
// Reset to submission
$('#checkoutConfirm').replaceWith('<button type="submit" id="checkoutSubmit" class="btn btn-primary">Submit</button>');
};
// On modal show, clear any prior results and set focus
$('#checkoutModal').on('shown.bs.modal', function(e) {
$('#checkoutResults').replaceWith('<div id="checkoutResults"></div>');
$('#availabilityResult').replaceWith('<div id="availabilityResult"></div>');
$('#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('<div id="availabilityResult"></div>');
// Get availability of the item
let availability = items.then(
function(data, textStatus, jqXHR) {
if (data.length == 1) {
current_item = data[0];
item_id = current_item.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);
}
);
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]}`);
$('#checkout_barcode').val("").prop("readonly", false).focus();
}
}
}
// 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('<input type="hidden" id="item_id" value="' + item_id + '"><input type="hidden" id="confirm_token" value="' + data.confirmation_token + '"><button type="submit" id="checkoutConfirm" class="btn btn-warning">Confirm</button>');
}
// 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/patrons/'+logged_in_user_id+'/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) {
addCheckout(data);
// 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/patrons/'
+ logged_in_user_id
+ '/checkouts?confirmation='
+ token,
type: 'POST',
data: JSON.stringify(params),
contentType: "json"
});
checkout.then(
function(data, textStatus, jqXHR) {
$('#item_id').remove;
$('#confirm_token').remove;
$('#availabilityResult').replaceWith('<div id="availabilityResult"></div>');
addCheckout(data);
// data retrieved from url2 as provided by the first request
},
function(data, textStatus, jqXHR) {
console.log("checkout.then failed");
}
);
});
$('#checkoutModal').on('hidden.bs.modal', function (e) {
let pageName = $(location).attr("pathname");
if ( pageName == '/cgi-bin/koha/opac-user.pl' ) {
location.reload();
}
})
});