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:
parent
bc55051e49
commit
709255fdfa
4 changed files with 304 additions and 0 deletions
|
@ -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' ) %]
|
||||
|
|
|
@ -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">×</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>
|
|
@ -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>
|
||||
|
|
249
koha-tmpl/opac-tmpl/bootstrap/js/modals/checkout.js
Normal file
249
koha-tmpl/opac-tmpl/bootstrap/js/modals/checkout.js
Normal 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();
|
||||
}
|
||||
})
|
||||
});
|
Loading…
Reference in a new issue