Koha-clms/koha-tmpl/intranet-tmpl/prog/en/modules/bookings/list.tt

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 %]
&rsaquo; [% INCLUDE 'biblio-title-head.inc' %] &rsaquo; [% t("Catalog") | html %] &rsaquo; [% 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' %]