Koha/koha-tmpl/intranet-tmpl/prog/js/place_booking_modal.js
Martin Renvoize c358f9cc45
Bug 29002: Empty select2 options for patron at modal close
Select2's recommended reset doesn't remove <option> blocks it adds for
ajax responses.. as such it gets confused when you subsequently
re-initialise.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
2023-11-03 12:04:29 -03:00

561 lines
24 KiB
JavaScript

let dataFetched = false;
let bookable_items, bookings, checkouts, booking_id, booking_item_id, booking_patron;
$('#placeBookingModal').on('show.bs.modal', function(e) {
// Get context
let button = $(e.relatedTarget);
let biblionumber = button.data('biblionumber');
$('#booking_biblio_id').val(biblionumber);
let patron_id = button.data('patron') || 0;
booking_item_id = button.data('itemnumber');
let start_date = button.data('start_date');
let end_date = button.data('end_date');
// Get booking id if this is an edit
booking_id = button.data('booking');
if (booking_id) {
$('#placeBookingLabel').html('Edit booking');
$('#booking_id').val(booking_id);
} else {
$('#placeBookingLabel').html('Place booking');
// Ensure we don't accidentally update a booking
$('#booking_id').val('');
}
// Patron select2
$("#booking_patron_id").kohaSelect({
dropdownParent: $(".modal-content", "#placeBookingModal"),
width: '50%',
dropdownAutoWidth: true,
allowClear: true,
minimumInputLength: 3,
ajax: {
url: '/api/v1/patrons',
delay: 250,
dataType: 'json',
headers: {
"x-koha-embed": "library"
},
data: function(params) {
let q = buildPatronSearchQuery(params.term);
let query = {
'q': JSON.stringify(q),
'_page': params.page,
'_order_by': '+me.surname,+me.firstname',
};
return query;
},
processResults: function(data, params) {
let results = [];
data.results.forEach(function(patron) {
patron.id = patron.patron_id;
results.push(patron);
});
return {
"results": results, "pagination": { "more": data.pagination.more }
};
},
},
templateResult: function (patron) {
if (patron.library_id == loggedInLibrary) {
loggedInClass = "ac-currentlibrary";
} else {
loggedInClass = "";
}
let $patron = $("<span></span>")
.append(
"" +
(patron.surname
? escape_str(patron.surname) + ", "
: "") +
(patron.firstname
? escape_str(patron.firstname) + " "
: "") +
(patron.cardnumber
? " (" + escape_str(patron.cardnumber) + ")"
: "") +
"<small>" +
(patron.date_of_birth
? ' <span class="age_years">' +
$get_age(patron.date_of_birth) +
" " +
__("years") +
"</span>"
: "") +
(patron.library ?
" <span class=\"ac-library\">" +
escape_str(patron.library.name) +
"</span>"
: "") +
"</small>"
)
.addClass(loggedInClass);
return $patron;
},
templateSelection: function (patron) {
if (!patron.surname) {
return patron.text;
}
return (
escape_str(patron.surname) + ", " + escape_str(patron.firstname)
);
},
placeholder: "Search for a patron"
});
$('#booking_patron_id').on('select2:select', function (e) {
booking_patron = e.params.data;
});
// Adopt periodPicker
let periodPicker = $("#period").get(0)._flatpickr;
if ( !dataFetched ) {
// Fetch list of bookable items
let itemsFetch = $.ajax({
url: '/api/v1/biblios/' + biblionumber + '/items?bookable=1' + '&_per_page=-1',
dataType: 'json',
type: 'GET'
});
// Fetch list of existing bookings
let bookingsFetch = $.ajax({
url: '/api/v1/bookings?biblio_id=' + biblionumber,
dataType: 'json',
type: 'GET'
});
// Fetch list of current checkouts
let checkoutsFetch = $.ajax({
url: '/api/v1/biblios/' + biblionumber + '/checkouts?_per_page=-1',
dataType: 'json',
type: 'GET'
});
// Update item select2 and period flatpickr
$.when(itemsFetch, bookingsFetch, checkoutsFetch).then(
function(itemsFetch,bookingsFetch, checkoutsFetch){
// Set variables
bookable_items = itemsFetch[0];
bookings = bookingsFetch[0];
checkouts = checkoutsFetch[0];
// Merge current checkouts into bookings
for (checkout of checkouts) {
let booking = {
biblio_id: biblionumber,
booking_id: null,
end_date: checkout.due_date,
item_id: checkout.item_id,
patron_id: checkout.patron_id,
start_date: new Date().toISOString(),
};
bookings.unshift(booking);
}
// Item select2
$("#booking_item_id").select2({
dropdownParent: $(".modal-content", "#placeBookingModal"),
width: '50%',
dropdownAutoWidth: true,
minimumResultsForSearch: 20,
placeholder: "Select item"
});
// Update flatpickr mode
periodPicker.set('mode', 'range');
// Total bookable items
let bookable = 0;
for (item of bookable_items) {
bookable++;
// Populate item select (NOTE: Do we still need this check for pre-existing select option here?)
if (!($('#booking_item_id').find("option[value='" + item.item_id + "']").length)) {
// Create a DOM Option and de-select by default
let newOption = new Option(escape_str(item.external_id), item.item_id, false, false);
// Append it to the select
$('#booking_item_id').append(newOption);
}
}
// Set disable function for periodPicker
let disableExists = periodPicker.config.disable.filter(f => f.name === 'dateDisable');
if ( disableExists.length === 0 ) {
periodPicker.config.disable.push(function dateDisable(date){
// set local copy of selectedDates
let selectedDates = periodPicker.selectedDates;
// set booked counter
let booked = 0;
// reset the unavailable items array
let unavailable_items = [];
// reset the biblio level bookings array
let biblio_bookings = [];
// disable dates before selected date
if (!selectedDates[1] && (selectedDates[0] && selectedDates[0] > date)) {
return true;
}
// iterate existing bookings
for (booking of bookings) {
// Skip if we're editing this booking
if (booking_id && booking_id == booking.booking_id){
continue;
}
let start_date = flatpickr.parseDate(booking.start_date);
let end_date = flatpickr.parseDate(booking.end_date);
// patron has selected a start date (end date checks)
if (selectedDates[0]) {
// new booking start date is between existing booking start and end dates
if (selectedDates[0] >= start_date && selectedDates[0] <= end_date) {
if (booking.item_id) {
if (unavailable_items.indexOf(booking.item_id) === -1) {
unavailable_items.push(booking.item_id);
}
} else {
if (biblio_bookings.indexOf(booking.booking_id) === -1) {
biblio_bookings.push(booking.booking_id);
}
}
}
// new booking end date would be between existing booking start and end dates
else if (date >= start_date && date <= end_date) {
if (booking.item_id) {
if (unavailable_items.indexOf(booking.item_id) === -1) {
unavailable_items.push(booking.item_id);
}
} else {
if (biblio_bookings.indexOf(booking.booking_id) === -1) {
biblio_bookings.push(booking.booking_id);
}
}
}
// new booking would span existing booking
else if (selectedDates[0] <= start_date && date >= end_date) {
if (booking.item_id) {
if (unavailable_items.indexOf(booking.item_id) === -1) {
unavailable_items.push(booking.item_id);
}
} else {
if (biblio_bookings.indexOf(booking.booking_id) === -1) {
biblio_bookings.push(booking.booking_id);
}
}
}
// new booking would not conflict
else {
continue;
}
// check that there are available items
// available = all bookable items - booked items - booked biblios
let total_available = bookable_items.length - unavailable_items.length - biblio_bookings.length;
if (total_available === 0) {
return true;
}
}
// patron has not yet selected a start date (start date checks)
else if (date <= end_date && date >= start_date) {
// same item, disable date
if (booking.item_id && booking.item_id == booking_item_id) {
return true;
}
// count all clashes, both item and biblio level
booked++;
if (booked == bookable) {
return true;
}
// FIXME: The above is not intelligent enough to spot
// cases where an item must be used for a biblio level booking
// due to all other items being booking within the biblio level
// booking period... we end up with a clash
// To reproduce:
// * One bib with two bookable items.
// * Add item level booking
// * Add biblio level booking that extends one day beyond the item level booking
// * Try to book the item without an item level booking from the day before the biblio level
// booking is to be returned. Note this is a clash, the only item available for the biblio
// level booking is the item you just booked out overlapping the end date.
}
}
});
};
// Setup listener for item select2
$('#booking_item_id').on('select2:select', function(e) {
booking_item_id = e.params.data.id ? e.params.data.id : null;
// redraw pariodPicker taking selected item into account
periodPicker.redraw();
});
// Set onChange for flatpickr
let changeExists = periodPicker.config.onChange.filter(f => f.name ==='periodChange');
if(changeExists.length === 0) {
periodPicker.config.onChange.push(function periodChange(selectedDates, dateStr, instance) {
// Range set, update hidden fields and set available items
if ( selectedDates[0] && selectedDates[1] ) {
// set form fields from picker
let picker_start = dayjs(selectedDates[0]);
let picker_end = dayjs(selectedDates[1]).endOf('day');
$('#booking_start_date').val(picker_start.toISOString());
$('#booking_end_date').val(picker_end.toISOString());
// set available items in select2
let booked_items = bookings.filter(function(booking) {
let start_date = flatpickr.parseDate(booking.start_date);
let end_date = flatpickr.parseDate(booking.end_date);
// This booking ends before the start of the new booking
if ( end_date <= selectedDates[0] ) {
return false;
}
// This booking starts after then end of the new booking
if ( start_date >= selectedDates[1] ) {
return false;
}
// This booking overlaps
return true;
});
$("#booking_item_id > option").each(function() {
let option = $(this);
if ( booking_item_id && booking_item_id == option.val() ) {
option.prop('disabled',false);
} else if ( booked_items.some(function(booked_item){
return option.val() == booked_item.item_id;
}) ) {
option.prop('disabled',true);
} else {
option.prop('disabled',false);
}
});
$('#booking_item_id').trigger('change.select2');
}
// Range not set, reset field options
else {
$('#booking_item_id > option').each(function() {
$(this).prop('disabled', false);
});
$('#booking_item_id').trigger('change.select2');
}
});
};
// Enable flatpickr now we have date function populated
periodPicker.redraw();
$("#period_fields :input").prop('disabled', false);
// Redraw select with new options and enable
$('#booking_item_id').trigger('change');
$("#booking_item_id").prop("disabled", false);
// Set the flag to indicate that data has been fetched
dataFetched = true;
// Set form values
setFormValues(patron_id,booking_item_id,start_date,end_date,periodPicker);
},
function(jqXHR, textStatus, errorThrown){
console.log("Fetch failed");
}
);
} else {
setFormValues(patron_id,booking_item_id,start_date,end_date,periodPicker);
};
});
function setFormValues(patron_id,booking_item_id,start_date,end_date,periodPicker){
// If passed patron, pre-select
if (patron_id) {
let patronSelect = $('#booking_patron_id');
let patron = $.ajax({
url: '/api/v1/patrons/' + patron_id,
dataType: 'json',
type: 'GET'
});
$.when(patron).done(
function(patron){
// clone patron_id to id (select2 expects an id field)
patron.id = patron.patron_id;
patron.text = escape_str(patron.surname) + ", " + escape_str(patron.firstname);
// Add and select new option
let newOption = new Option(patron.text, patron.id, true, true);
patronSelect.append(newOption).trigger('change');
// manually trigger the `select2:select` event
patronSelect.trigger({
type: 'select2:select',
params: {
data: patron
}
});
}
);
}
// Set booking start & end if this is an edit
if ( start_date ) {
// Allow invalid pre-load so setDate can set date range
// periodPicker.set('allowInvalidPreload', true);
// FIXME: Why is this the case.. we're passing two valid Date objects
let start = new Date(start_date);
let end = new Date(end_date);
let dates = [ new Date(start_date), new Date(end_date) ];
periodPicker.setDate(dates, true);
}
// Reset periodPicker, biblio_id may have been nulled
else {
periodPicker.redraw();
};
// If passed an itemnumber, pre-select
if (booking_item_id) {
$('#booking_item_id').val(booking_item_id).trigger('change');
}
}
$("#placeBookingForm").on('submit', function(e) {
e.preventDefault();
let url = '/api/v1/bookings';
let start_date = $('#booking_start_date').val();
let end_date = $('#booking_end_date').val();
let item_id = $('#booking_item_id').val();
if (!booking_id) {
let posting = $.post(
url,
JSON.stringify({
"start_date": start_date,
"end_date": end_date,
"biblio_id": $('#booking_biblio_id').val(),
"item_id": item_id != 0 ? item_id : null,
"patron_id": $('#booking_patron_id').find(':selected').val()
})
);
posting.done(function(data) {
// Update bookings store for subsequent bookings
bookings.push(data);
// Update bookings page as required
if (typeof bookings_table !== 'undefined' && bookings_table !== null) {
bookings_table.api().ajax.reload();
}
if (typeof timeline !== 'undefined' && timeline !== null) {
timeline.itemsData.add({
id: data.booking_id,
booking: data.booking_id,
patron: data.patron_id,
start: dayjs(data.start_date).toDate(),
end: dayjs(data.end_date).toDate(),
content: $patron_to_html(booking_patron, {
display_cardnumber: true,
url: false
}),
editable: { remove: true, updateTime: true },
type: 'range',
group: data.item_id ? data.item_id : 0
});
timeline.focus(data.booking_id);
}
// Update bookings counts
$('.bookings_count').html(parseInt($('.bookings_count').html(), 10)+1);
// Close modal
$('#placeBookingModal').modal('hide');
});
posting.fail(function(data) {
$('#booking_result').replaceWith('<div id="booking_result" class="alert alert-danger">Failure</div>');
});
} else {
url += '/' + booking_id;
let putting = $.ajax({
'method': 'PUT',
'url': url,
'data': JSON.stringify({
"booking_id": booking_id,
"start_date": start_date,
"end_date": end_date,
"biblio_id": $('#booking_biblio_id').val(),
"item_id": item_id != 0 ? item_id : null,
"patron_id": $('#booking_patron_id').find(':selected').val()
})
});
putting.done(function(data) {
update_success = 1;
// Update bookings store for subsequent bookings
let target = bookings.find((obj) => obj.booking_id === data.booking_id);
Object.assign(target,data);
// Update bookings page as required
if (typeof bookings_table !== 'undefined' && bookings_table !== null) {
bookings_table.api().ajax.reload();
}
if (typeof timeline !== 'undefined' && timeline !== null) {
timeline.itemsData.update({
id: data.booking_id,
booking: data.booking_id,
patron: data.patron_id,
start: dayjs(data.start_date).toDate(),
end: dayjs(data.end_date).toDate(),
content: $patron_to_html(booking_patron, {
display_cardnumber: true,
url: false
}),
editable: { remove: true, updateTime: true },
type: 'range',
group: data.item_id ? data.item_id : 0
});
timeline.focus(data.booking_id);
}
// Close modal
$('#placeBookingModal').modal('hide');
});
putting.fail(function(data) {
$('#booking_result').replaceWith('<div id="booking_result" class="alert alert-danger">Failure</div>');
});
}
});
$('#placeBookingModal').on('hidden.bs.modal', function (e) {
$('#booking_patron_id').val(null).trigger('change');
$('#booking_patron_id').empty();
$('#booking_item_id').val(0).trigger('change');
$('#period').get(0)._flatpickr.clear();
$('#booking_start_date').val('');
$('#booking_end_date').val('');
$('#booking_id').val('');
});