419 lines
20 KiB
Plaintext
419 lines
20 KiB
Plaintext
[% USE raw %]
|
|
[% USE Koha %]
|
|
[% USE Asset %]
|
|
[% USE KohaDates %]
|
|
[% PROCESS 'i18n.inc' %]
|
|
[% SET footerjs = 1 %]
|
|
[% INCLUDE 'doc-head-open.inc' %]
|
|
<title
|
|
>[% FILTER collapse %]
|
|
[% t("Bookings") | html %]
|
|
› [% INCLUDE 'biblio-title-head.inc' %] › [% t("Catalog") | html %] › [% t("Koha") | html %]
|
|
[% END %]</title
|
|
>
|
|
[% INCLUDE 'doc-head-close.inc' %]
|
|
[% Asset.css("css/bookings.css") | $raw %]
|
|
</head>
|
|
|
|
<body id="circ_request" class="catalog">
|
|
[% WRAPPER 'header.inc' %]
|
|
[% INCLUDE 'cat-search.inc' %]
|
|
[% END %]
|
|
|
|
[% WRAPPER 'sub-header.inc' %]
|
|
[% WRAPPER breadcrumbs %]
|
|
[% WRAPPER breadcrumb_item %]
|
|
<a href="/cgi-bin/koha/catalogue/search.pl">Catalog</a>
|
|
[% END %]
|
|
[% WRAPPER breadcrumb_item %]
|
|
[% INCLUDE 'biblio-title.inc' link = 1 %]
|
|
[% END %]
|
|
[% WRAPPER breadcrumb_item bc_active= 1 %]
|
|
<span>Bookings</span>
|
|
[% END %]
|
|
[% END #/ WRAPPER breadcrumbs %]
|
|
[% END #/ WRAPPER sub-header.inc %]
|
|
|
|
[% WRAPPER 'main-container.inc' aside='biblio-view-menu' %]
|
|
<div class="row">
|
|
[% INCLUDE 'cat-toolbar.inc' %]
|
|
<h1>Bookings for [% INCLUDE 'biblio-title-head.inc' %]</h1>
|
|
<div class="page-section" id="bookings-timeline"></div>
|
|
<div class="page-section">
|
|
[% INCLUDE 'additional-filters.inc' filters = [ { id = 'filter-cancelled', label_show = t('Include cancelled'), label_hide = t('Exclude cancelled') }, { id = 'filter-completed', label_show = t('Include completed'), label_hide = t('Exclude completed') },] filter_class = 'bookings' %]
|
|
<table id="bookings_table"></table>
|
|
</div>
|
|
</div>
|
|
[% END %]
|
|
[% INCLUDE modals/cancel_booking.inc %]
|
|
|
|
[% MACRO jsinclude BLOCK %]
|
|
[% Asset.js("lib/vis-timeline/vis-timeline-graph2d.min.js") | $raw %]
|
|
[% INCLUDE 'datatables.inc' %]
|
|
[% INCLUDE 'calendar.inc' %]
|
|
[% INCLUDE 'select2.inc' %]
|
|
[% INCLUDE 'js-date-format.inc' %]
|
|
[% Asset.js("lib/dayjs/plugin/isSameOrAfter.js") | $raw %]
|
|
[% Asset.js("lib/dayjs/plugin/isSameOrBefore.js") | $raw %]
|
|
<script nonce="[% Koha.CSPNonce | $raw %]">
|
|
dayjs.extend(window.dayjs_plugin_isSameOrAfter);
|
|
</script>
|
|
<script nonce="[% Koha.CSPNonce | $raw %]">
|
|
dayjs.extend(window.dayjs_plugin_isSameOrBefore);
|
|
</script>
|
|
[% Asset.js("js/modals/place_booking.js") | $raw %]
|
|
[% Asset.js("js/cancel_booking_modal.js") | $raw %]
|
|
[% Asset.js("js/combobox.js") | $raw %]
|
|
[% Asset.js("js/additional-filters.js") | $raw %]
|
|
<script nonce="[% Koha.CSPNonce | $raw %]">
|
|
var cancel_success = 0;
|
|
var update_success = 0;
|
|
var bookings_table;
|
|
var timeline;
|
|
let biblionumber = "[% biblionumber | uri %]";
|
|
Koha.addPermissions({
|
|
CAN_user_circulate_manage_bookings: [% CAN_user_circulate_manage_bookings ? 1 : 0 | html %],
|
|
});
|
|
</script>
|
|
<script nonce="[% Koha.CSPNonce | $raw %]">
|
|
$(document).ready(function () {
|
|
var items = $.ajax({
|
|
url: "/api/v1/biblios/%s/items?bookable=1".format(biblionumber),
|
|
dataType: "json",
|
|
type: "GET",
|
|
});
|
|
var bookings = $.ajax({
|
|
url: "/api/v1/biblios/%s/bookings?_per_page=-1".format(biblionumber),
|
|
headers: { "x-koha-embed": "patron" },
|
|
dataType: "json",
|
|
type: "GET",
|
|
});
|
|
|
|
$.when(items, bookings).then(
|
|
function (items, bookings) {
|
|
var itemsSet = new vis.DataSet([
|
|
{
|
|
id: 0,
|
|
content: _("Record level"),
|
|
},
|
|
]);
|
|
for (item of items[0]) {
|
|
itemsSet.add({
|
|
id: item.item_id,
|
|
content: _("Item %s").format(item.external_id),
|
|
});
|
|
}
|
|
|
|
var bookingsSet = new vis.DataSet();
|
|
for (booking of bookings[0]) {
|
|
const isActive = ["new", "issued"].includes(booking.status);
|
|
const patronContent = $patron_to_html(booking.patron, {
|
|
display_cardnumber: true,
|
|
url: false,
|
|
});
|
|
// Convert to library timezone for timeline display
|
|
const startServerTz = dayjs(booking.start_date).tz($timezone());
|
|
const endServerTz = dayjs(booking.end_date).tz($timezone());
|
|
bookingsSet.add({
|
|
id: booking.booking_id,
|
|
booking: booking.booking_id,
|
|
patron: booking.patron_id,
|
|
pickup_library: booking.pickup_library_id,
|
|
start: $toDisplayDate(startServerTz),
|
|
end: $toDisplayDate(endServerTz),
|
|
content: !isActive ? `<s>${patronContent}</s>` : patronContent,
|
|
className: booking.status === "cancelled" ? "cancelled" : "",
|
|
...(Koha.permissions.CAN_user_circulate_manage_bookings ? { editable: !["cancelled", "issued", "completed"].includes(booking.status) ? { remove: true, updateTime: true } : false } : { editable: false }),
|
|
type: "range",
|
|
group: booking.item_id ?? 0,
|
|
});
|
|
}
|
|
|
|
var container = document.getElementById("bookings-timeline");
|
|
var options = {
|
|
stack: true,
|
|
verticalScroll: true,
|
|
orientation: {
|
|
axis: "both",
|
|
item: "top",
|
|
},
|
|
timeAxis: { scale: "day", step: 1 },
|
|
dataAttributes: ["booking"],
|
|
|
|
// always snap to full days, independent of the scale
|
|
snap: function (date, scale, step) {
|
|
let offset = date.getTimezoneOffset() * 60 * 1000;
|
|
let day = 24 * 60 * 60 * 1000;
|
|
let seconds = Math.round(date / day) * day;
|
|
return seconds + offset;
|
|
},
|
|
|
|
// prevent overlapping bookings
|
|
onMoving: function (item, callback) {
|
|
var overlapping = bookingsSet.get({
|
|
filter: function (testItem) {
|
|
if (testItem.id == item.id) {
|
|
return false;
|
|
}
|
|
if (testItem.group != item.group) {
|
|
return false;
|
|
}
|
|
return item.start < testItem.end && item.end > testItem.start;
|
|
},
|
|
});
|
|
|
|
if (overlapping.length == 0) {
|
|
callback(item);
|
|
}
|
|
},
|
|
|
|
// action events
|
|
onMove: function (data, callback) {
|
|
let startDate = dayjs(data.start);
|
|
|
|
// set end datetime hours and minutes to the end of the day
|
|
let endDate = dayjs(data.end).endOf("day");
|
|
|
|
$("#placeBookingModal").modal(
|
|
"show",
|
|
$(
|
|
'<button data-booking="' +
|
|
data.id +
|
|
'" data-biblionumber="' +
|
|
biblionumber +
|
|
'" data-itemnumber="' +
|
|
data.group +
|
|
'" data-patron="' +
|
|
data.patron +
|
|
'" data-pickup_library="' +
|
|
data.pickup_library +
|
|
'" data-start_date="' +
|
|
startDate.toISOString() +
|
|
'" data-end_date="' +
|
|
endDate.toISOString() +
|
|
'">'
|
|
)
|
|
);
|
|
$("#placeBookingModal").on("hide.bs.modal", function (e) {
|
|
if (update_success) {
|
|
update_success = 0;
|
|
callback(data);
|
|
} else {
|
|
callback(null);
|
|
}
|
|
});
|
|
},
|
|
onRemove: function (item, callback) {
|
|
const button = document.createElement("button");
|
|
button.dataset.booking = item.booking || item.id;
|
|
$("#cancelBookingModal").modal("show", button);
|
|
$("#cancelBookingModal").on("hide.bs.modal", function (e) {
|
|
if (cancel_success) {
|
|
cancel_success = 0;
|
|
callback(item);
|
|
} else {
|
|
callback(null);
|
|
}
|
|
});
|
|
},
|
|
};
|
|
timeline = new vis.Timeline(container, bookingsSet, itemsSet, options);
|
|
},
|
|
function (jqXHR, textStatus, errorThrown) {
|
|
console.log("Fetch failed");
|
|
}
|
|
);
|
|
|
|
const additional_filters = AdditionalFilters.init(["filter-cancelled", "filter-completed"])
|
|
.onChange((filters, { anyFiltersNotApplied }) => {
|
|
bookings_table.DataTable().ajax.reload();
|
|
})
|
|
.build({
|
|
status: ({ filters, isNotApplied }) => {
|
|
const defaults = ["new", "issued"];
|
|
const filtered = [...defaults];
|
|
if (isNotApplied(filters["filter-cancelled"])) {
|
|
filtered.push("cancelled");
|
|
}
|
|
if (isNotApplied(filters["filter-completed"])) {
|
|
filtered.push("completed");
|
|
}
|
|
return { "-in": filtered };
|
|
},
|
|
});
|
|
|
|
var bookings_table_url = "/api/v1/biblios/%s/bookings".format(biblionumber);
|
|
bookings_table = $("#bookings_table").kohaTable(
|
|
{
|
|
ajax: {
|
|
url: bookings_table_url,
|
|
},
|
|
embed: ["item", "patron", "pickup_library"],
|
|
columns: [
|
|
{
|
|
data: "booking_id",
|
|
title: _("Booking ID"),
|
|
visible: false,
|
|
},
|
|
{
|
|
data: "",
|
|
title: _("Status"),
|
|
name: "status",
|
|
searchable: false,
|
|
orderable: false,
|
|
visible: true,
|
|
render: function (data, type, row, meta) {
|
|
const statusMap = {
|
|
new: () => _("New"),
|
|
cancelled: () => [_("Cancelled"), row.cancellation_reason].filter(Boolean).join(": "),
|
|
issued: () => _("Issued"),
|
|
completed: () => _("Completed"),
|
|
};
|
|
|
|
const statusText = statusMap[row.status] ? statusMap[row.status]() : _("Unknown");
|
|
|
|
const classMap = [
|
|
{ status: _("Cancelled"), class: "bg-secondary" },
|
|
{ status: _("Completed"), class: "bg-secondary" },
|
|
{ status: _("Issued"), class: "bg-info" },
|
|
{ status: _("New"), class: "bg-success" },
|
|
];
|
|
|
|
const badgeClass = classMap.find(mapping => statusText.startsWith(mapping.status))?.class || "bg-secondary";
|
|
|
|
return `<span class="badge rounded-pill ${badgeClass}">${statusText}</span>`;
|
|
},
|
|
},
|
|
{
|
|
data: "item.external_id",
|
|
title: _("Item"),
|
|
searchable: true,
|
|
orderable: true,
|
|
defaultContent: _("Any item"),
|
|
render: function (data, type, row, meta) {
|
|
if (row.item) {
|
|
return row.item.external_id + " (" + row.booking_id + ")";
|
|
} else {
|
|
return null;
|
|
}
|
|
},
|
|
},
|
|
{
|
|
data: "patron.firstname:patron.surname",
|
|
title: _("Patron"),
|
|
searchable: true,
|
|
orderable: true,
|
|
render: function (data, type, row, meta) {
|
|
return $patron_to_html(row.patron, {
|
|
display_cardnumber: true,
|
|
url: true,
|
|
});
|
|
},
|
|
},
|
|
{
|
|
data: "pickup_library.name",
|
|
title: _("Pickup library"),
|
|
searchable: true,
|
|
orderable: true,
|
|
render: function (data, type, row, meta) {
|
|
return row.pickup_library.name;
|
|
},
|
|
},
|
|
{
|
|
data: "start_date",
|
|
title: _("Start date"),
|
|
searchable: true,
|
|
orderable: true,
|
|
render: function (data, type, row, meta) {
|
|
return $date(row.start_date);
|
|
},
|
|
},
|
|
{
|
|
data: "end_date",
|
|
title: _("End date"),
|
|
searchable: true,
|
|
orderable: true,
|
|
render: function (data, type, row, meta) {
|
|
return $date(row.end_date);
|
|
},
|
|
},
|
|
{
|
|
data: "",
|
|
title: _("Actions"),
|
|
class: "actions",
|
|
searchable: false,
|
|
orderable: false,
|
|
render: function (data, type, row, meta) {
|
|
let result = "";
|
|
const is_readonly = ["cancelled", "issued", "completed"].includes(row.status);
|
|
if (Koha.permissions.CAN_user_circulate_manage_bookings && !is_readonly) {
|
|
result += `
|
|
<button type="button" class="btn btn-default btn-xs edit-action"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#placeBookingModal"
|
|
data-booking="%s"
|
|
data-biblionumber="%s"
|
|
data-itemnumber="%s"
|
|
data-patron="%s"
|
|
data-pickup_library="%s"
|
|
data-start_date="%s"
|
|
data-end_date="%s"
|
|
data-item_type_id="%s"
|
|
>
|
|
<i class="fa fa-pencil" aria-hidden="true"></i> %s
|
|
</button>
|
|
`.format(
|
|
escape_str(row.booking_id),
|
|
escape_str(biblionumber),
|
|
escape_str(row.item_id),
|
|
escape_str(row.patron_id),
|
|
escape_str(row.pickup_library_id),
|
|
escape_str(row.start_date),
|
|
escape_str(row.end_date),
|
|
escape_str(row.item.item_type_id),
|
|
_("Edit")
|
|
);
|
|
result += `
|
|
<button type="button" class="btn btn-default btn-xs cancel-action"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#cancelBookingModal"
|
|
data-booking="%s"
|
|
>
|
|
<i class="fa fa-trash" aria-hidden="true"></i> %s
|
|
</button>
|
|
`.format(escape_str(row.booking_id), _("Cancel"));
|
|
const csrf_token = $('meta[name="csrf-token"]').attr("content");
|
|
result += `
|
|
<form name="checkout-transform" method="post" action="/cgi-bin/koha/circ/circulation.pl?borrowernumber=%s">
|
|
<input type="hidden" name="csrf_token" value="${csrf_token}" />
|
|
<input type="hidden" name="op" value="cud-checkout"/>
|
|
<input type="hidden" name="borrowernumber" value="%s"/>
|
|
<input type="hidden" name="barcode" value="%s"/>
|
|
<input type="hidden" name="duedatespec" value="%s"/>
|
|
<button id="checkout_action" class="btn btn-default btn-xs" type="submit">
|
|
<i class="fa fa-check-circle" aria-hidden="true"></i> %s
|
|
</button>
|
|
</form>
|
|
`.format(escape_str(row.patron_id), escape_str(row.patron_id), escape_str(row.item.external_id), escape_str(row.end_date), _("Transform to checkout"));
|
|
}
|
|
return result;
|
|
},
|
|
},
|
|
],
|
|
},
|
|
undefined,
|
|
0,
|
|
additional_filters
|
|
);
|
|
|
|
$("#cancellation-reason").comboBox({
|
|
displayProperty: "name",
|
|
placeholder: _("Select or type a reason"),
|
|
context: "modal",
|
|
});
|
|
});
|
|
</script>
|
|
[% END %]
|
|
|
|
[% INCLUDE 'intranet-bottom.inc' %]
|