From 59f69b0e3a7e2f1b57d2c44620a631bac5720854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johanna=20R=C3=A4is=C3=A4?= Date: Thu, 24 Oct 2024 09:31:39 +0300 Subject: [PATCH] Bug 23269: use REST API for patron_holds_table This patch uses the REST API to populate the holds table Test plan: 1) Apply the patch 2) Go to a record with holds 3) Check that the holds table is populated correctly 4) Check that hold tables functions work correctly 5) Change HoldsSplitQueue and HoldsSplitQueueNumbering system preference, try all variations 6) Repeat steps 3 and 4 7) Search for records and place holds to multiple records 8) Repeat steps 3 and 4 9) Try with lesser permissions Sponsored-by: Koha-Suomi Oy --- api/v1/swagger/definitions/hold.yaml | 5 + api/v1/swagger/paths/holds.yaml | 2 + .../prog/en/includes/holds_table.inc | 258 +------- .../prog/en/modules/reserve/request.tt | 306 +++------ koha-tmpl/intranet-tmpl/prog/js/holds.js | 615 ++++++++++++++++++ reserve/request.pl | 82 +-- 6 files changed, 734 insertions(+), 534 deletions(-) diff --git a/api/v1/swagger/definitions/hold.yaml b/api/v1/swagger/definitions/hold.yaml index 2ba3367e04..b8c6754396 100644 --- a/api/v1/swagger/definitions/hold.yaml +++ b/api/v1/swagger/definitions/hold.yaml @@ -123,6 +123,11 @@ properties: - object - "null" description: The item + patron: + type: + - object + - "null" + description: The patron pickup_library: type: - object diff --git a/api/v1/swagger/paths/holds.yaml b/api/v1/swagger/paths/holds.yaml index 820f9dde9a..4967061add 100644 --- a/api/v1/swagger/paths/holds.yaml +++ b/api/v1/swagger/paths/holds.yaml @@ -104,6 +104,8 @@ - cancellation_requested - biblio - pickup_library + - patron + - item collectionFormat: csv produces: - application/json diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/holds_table.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/holds_table.inc index dd99815cb2..22633b70f8 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/holds_table.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/holds_table.inc @@ -5,273 +5,23 @@ [% #This include depends on the asset js/form-submit.js. Any template making use of this include must import form-submit.js as well. %] [% #FIXME can form-submit.js be imported into this file directly? %] - +
- [% IF ( CAN_user_reserveforothers_modify_holds_priority ) %] - - [% END %] + - [% IF ( CAN_user_reserveforothers_modify_holds_priority ) %] - - [% END %] + - [%- SET first_priority = 0 -%] - [%- SET found_holds = 0 -%] - [%- SET last_priority = holds.last.priority -%] - - [% SET all_priorities = [] %] - [% FOREACH hold IN holds %] - [% all_priorities.push( hold.priority ) %] - [% END %] - - - [% FOREACH hold IN holds %] - [%- IF !hold.found && first_priority == 0 -%] - [%- first_priority = hold.priority -%] - [%- found_holds = loop.index() -%] - [%- END -%] - [%- IF Koha.Preference('HoldsSplitQueueNumbering') == 'actual' -%] - [%- this_priority = hold.priority -%] - [%- ELSE -%] - [%- this_priority = loop.count() - found_holds -%] - [%- END -%] - [% IF hold.suspend %] - - [% ELSE %] - - [% END %] - - - [%- IF ( CAN_user_reserveforothers_modify_holds_priority ) -%] - [%- UNLESS hold.found -%] - [%- SET prev_priority = loop.prev.priority -%] - [%- SET next_priority = loop.next.priority -%] - - [%- ELSE -%] - - [%- END -%] - [%- END -%] - - - - - - - [%- IF ( CAN_user_reserveforothers_modify_holds_priority ) -%] - [%- UNLESS hold.found -%] - - [%- ELSE -%] - - [%- END -%] - [%- END -%] - - - [% IF ( hold.intransit || hold.atdestination ) %] - - [% ELSE %] - - [% END %] - - [% END %] -
PriorityChange priorityChange priority Patron Notes Date Expiration Pickup library DetailsSet lowest prioritySet lowest priority Delete Suspend Print hold/transfer slip
- - - - [% IF ( CAN_user_reserveforothers_modify_holds_priority ) %] - [% IF Koha.Preference('HoldsSplitQueue') == "nothing" && !hold.found %] - - [% ELSE %] - - - [% END %] - [% ELSE %] - - [% hold.priority | html %] - [% END %] - - - - - - - - - - - - - - - - - - [% INCLUDE 'patron-title.inc' patron=hold.patron hide_patron_infos_if_needed=1 %] - [% hold.notes | html | html_line_break %] - [% IF Koha.Preference('AllowHoldDateInFuture') %] - - [% ELSE %] - [% hold.date | $KohaDates %] - [% END %] - - - - [%- IF ( hold.found ) -%] - - [%- IF ( hold.atdestination ) -%] - Item waiting at [% hold.wbrname | html %][% IF hold.desk_name %], [% hold.desk_name | html %],[% END %] since [% hold.waiting_date | $KohaDates %] - [%- ELSIF (hold.intransit) -%] - Item being transferred to [% hold.wbrname | html %] - [%- ELSIF (hold.inprocessing) -%] - Item being processed at [% hold.wbrname | html %] - [%- ELSE -%] - Hold expected at [% hold.wbrname | html %], please checkin to verify status - [%- END -%] - [%- ELSE -%] - [%- IF Koha.Preference('IndependentBranches') && Branches.all().size == 1 -%] - [% Branches.GetName(hold.branchcode) | html %] - [%- ELSE -%] - - - [%- END -%] - [%- END -%] - - [%- IF ( hold.found ) -%] - - [%- IF ( hold.barcodenumber ) -%] - [%- hold.barcodenumber | html -%] - [%- ELSE -%] - No barcode - [%- END -%] - - - [%- ELSE -%] - [%- IF ( hold.item_level_hold ) -%] - - [%- IF ! hold.change_hold_type_allowed -%] - Only item - - [%- IF ( hold.barcodenumber ) -%] - [%- hold.barcodenumber | html -%] - [%- ELSE -%] - No barcode - [%- END -%] - - - [%- ELSE -%] - - [%- IF ( hold.itemnumber ) -%] - - [%- END -%] - [%- IF hold.itemtype -%] - Next available [% ItemTypes.GetDescription( hold.itemtype ) | html %] item - [%- ELSE -%] - Next available - [%- END -%] - [%- END -%] - - [%- ELSE -%] - [%- IF hold.itemtype -%] - Next available [% ItemTypes.GetDescription( hold.itemtype ) | html %] item - [%- ELSIF hold.object.item_group -%] - Next available item from group [% hold.object.item_group.description | html %] - [%- ELSE -%] - Next available - [%- END -%] - - - [%- END -%] - [%- END -%] - [%- IF hold.non_priority -%] -
Non priority hold - [%- END -%] -
- [% IF ( hold.lowestPriority ) %] - - - [% ELSE %] - - - [% END %] - - - - - - - [% IF Koha.Preference('SuspendHoldsIntranet') %] - [% UNLESS ( hold.found ) %] - [% IF ( hold.suspend ) %] - - [% ELSE %] - - [% END %] - - [% IF Koha.Preference('AutoResumeSuspendedHolds') %] - - - [%- ELSE -%] - - [%- END -%] - [%- END -%] - [%- END # IF SuspendHoldsIntranet -%] - [%- IF ( hold.found ) -%] - [% IF hold.intransit %]Revert transit status[% ELSE %]Revert waiting status[% END %] - [%- END -%] -
+ diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt index 8d1c1fddc3..d5aa5dff4d 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt @@ -1229,7 +1229,7 @@ [% END %] [% END %] [% END %] - [% IF ( reserveloop ) %] + [% IF ( total_holds ) %]
[% INCLUDE 'csrf-token.inc' %] [% IF ( multi_hold ) %] @@ -1249,7 +1249,7 @@
- +
@@ -1270,147 +1270,81 @@
[% FOREACH biblioloo IN biblioloop %] - [% IF ( biblioloo.reserveloop ) %] + [% IF ( biblioloo.total_holds ) %]
[% IF ( multi_hold ) %]

[% biblioloo.title | html %] - [% biblioloo.reserveloop.size | html %] [% tn('Hold', 'Holds', biblioloo.reserveloop.size) | $raw %] + [% biblioloo.total_holds | html %] [% tn('Hold', 'Holds', biblioloo.total_holds) | $raw %]

[% END %] [% IF Koha.Preference('HoldsSplitQueue') == 'branch' %] - - [% SET branchcodes = [] %] - - [% FOREACH h IN biblioloo.reserveloop %] - [% branchcodes.push( h.branchcode ) %] - [% END %] - [% branchcodes = branchcodes.unique %] - [% IF ( branchcodes.empty ) %] -
- There are no holds on this title. + + [% IF biblioloo.hold_branches.empty %] +
+

Any library

+ [% INCLUDE holds_table.inc biblio_id=biblioloo.biblionumber total_holds=biblioloo.total_holds split_data="any" %]
[% ELSE %] - - [% FOREACH b IN branchcodes.sort %] - [% SET holds_by_branch = [] %] - [% FOREACH h IN biblioloo.reserveloop %] - [% IF h.branchcode == b %] - [% holds_by_branch.push( h ) %] - [% END %] - [% END %] + [% FOREACH b IN biblioloo.hold_branches %]

[% Branches.GetName( b ) | html %]

- [% INCLUDE holds_table.inc holds=holds_by_branch %] + [% INCLUDE holds_table.inc biblio_id=biblioloo.biblionumber total_holds=biblioloo.total_holds split_data=b %]
- [% END # /FOREACh b %] - [% END # /IF ( branchcodes.empty ) %] + [% END %] + [% END # /IF hold_branches.empty %] [% ELSIF Koha.Preference('HoldsSplitQueue') == 'itemtype' %] - [% SET itemtypes = [] %] - - [% FOREACH h IN biblioloo.reserveloop %] - [% SET hold_itemtype = h.object.item.effective_itemtype || h.itemtype %] - [% itemtypes.push( hold_itemtype ) %] - [% END %] - [% itemtypes = itemtypes.unique %] - [% IF ( itemtypes.empty ) %] -
- There are no holds on this title. + [% IF biblioloo.hold_itemtypes.empty %] +
+

Any item type

+ [% INCLUDE holds_table.inc biblio_id=biblioloo.biblionumber total_holds=biblioloo.total_holds split_data="any" %]
[% ELSE %] - - [% FOREACH i IN itemtypes.sort %] - [% SET holds_by_itemtype = [] %] - [% FOREACH h IN biblioloo.reserveloop %] - [% SET hold_itemtype = h.object.item.effective_itemtype || h.itemtype %] - [% IF hold_itemtype == i %] - [% holds_by_itemtype.push( h ) %] - [% END %] - [% END %] - + [% FOREACH i IN biblioloo.hold_itemtypes %]
- [% IF i %] + [% IF i && i != 'any' %]

[% ItemTypes.GetDescription( i ) | html %]

[% ELSE %]

Any item type

[% END %] - [% INCLUDE holds_table.inc holds=holds_by_itemtype %] + [% INCLUDE holds_table.inc biblio_id=biblioloo.biblionumber total_holds=biblioloo.total_holds split_data=i %]
- [% END # /FOREACH i %] - [% END # /IF ( itemtypes.empty ) %] + [% END %] + [% END # /IF hold_split.empty %] [% ELSIF Koha.Preference('HoldsSplitQueue') == 'branch_itemtype' %] - [% SET branchcodes = [] %] - - [% FOREACH h IN biblioloo.reserveloop %] - [% branchcodes.push( h.branchcode ) %] - [% END %] - [% branchcodes = branchcodes.unique %] - [% IF ( branchcodes.empty ) %] -
- There are no holds on this title. -
- [% ELSE %] - [% FOREACH b IN branchcodes.sort %] -
-

[% Branches.GetName( b ) | html %]

- [% SET holds_by_branch = [] %] - [% FOREACH h IN biblioloo.reserveloop %] - [% IF h.branchcode == b %] - [% holds_by_branch.push( h ) %] + [% FOREACH b IN biblioloo.hold_branches %] +
+

[% Branches.GetName( b ) | html %]

+ [% FOREACH i IN biblioloo.hold_itemtypes %] +
+ [% IF i && i != 'any' %] +

[% ItemTypes.GetDescription( i ) | html %]

+ [% ELSE %] +

Any item type

[% END %] - [% END %] - - [% SET itemtypes = [] %] - [% FOREACH h IN holds_by_branch %] - [% SET hold_itemtype = h.object.item.effective_itemtype || h.itemtype %] - [% itemtypes.push( hold_itemtype ) %] - [% END %] - [% itemtypes = itemtypes.unique %] - - [% FOREACH i IN itemtypes.sort %] -
-
- [% IF i %] - [% ItemTypes.GetDescription( i ) | html %] - [% ELSE %] - Any item type - [% END %] -
- - [% SET holds_by_itemtype = [] %] - [% FOREACH h IN holds_by_branch %] - [% SET hold_itemtype = h.object.item.effective_itemtype || h.itemtype %] - [% IF hold_itemtype == i %] - [% holds_by_itemtype.push( h ) %] - [% END %] - [% END %] - [% INCLUDE holds_table.inc holds=holds_by_itemtype %] -
- [% END %] -
- [% END # /FOREACH b %] - [% END # /IF ( branchcodes.empty ) %] - - [% ELSE %] - - [% IF ( biblioloo.reserveloop.size ) %] - [% INCLUDE holds_table.inc holds=biblioloo.reserveloop %] - [% ELSE %] -
- There are no holds on this title. + [% SET split_data = b _ "_" _ i %] + [% INCLUDE holds_table.inc biblio_id=biblioloo.biblionumber total_holds=biblioloo.total_holds split_data=split_data %] +
+ [% END %]
[% END %] + [% ELSE %] + [% INCLUDE holds_table.inc biblio_id=biblioloo.biblionumber total_holds=biblioloo.total_holds split_data="any"%] [% END # /IF HoldsSplitQueue %]
- [% END # /IF biblioloo.reserveloop %] + [% ELSE %] +
+ There are no holds on this title. +
+ [% END # /IF ( biblioloo.total_holds ) %] [% END # FOREACH biblioloo %]
@@ -1483,6 +1417,9 @@ [% INCLUDE 'calendar.inc' %] [% INCLUDE 'columns_settings.inc' %] [% INCLUDE 'select2.inc' %] + [% Asset.js("js/holds.js") | $raw%] [% Asset.js("js/form-submit.js") | $raw%] @@ -1492,14 +1429,16 @@ [% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/js/holds.js b/koha-tmpl/intranet-tmpl/prog/js/holds.js index d18d394787..b0708e22b6 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/holds.js +++ b/koha-tmpl/intranet-tmpl/prog/js/holds.js @@ -498,3 +498,618 @@ $(document).ready(function() { }); }); + +function fetch_libraries() { + return $.ajax({ + method: "GET", + url: "/api/v1/libraries", + data: {_per_page: -1}, + success: function(data, textStatus, request){ + }, + error: function( jqXHR, textStatus, errorThrown) { + console.log(errorThrown); + }, + }); +} + +async function load_patron_holds_table(biblio_id, split_data) { + const { name: split_name, value: split_value } = split_data; + const table_id = `#patron_holds_table_${biblio_id}_${split_value}`; + hold_table_settings.table = `patron_holds_table_${biblio_id}_${split_data.value}`; + let url = `/api/v1/holds/?q={"me.biblio_id":${biblio_id}`; + + if (split_name === 'branch' && split_value !== 'any') { + url += `, "me.pickup_library_id":"${split_value}"`; + } else if (split_name === 'itemtype' && split_value !== 'any') { + url += `, "me.itemtype":"${split_value}"`; + } else if (split_name === 'branch_itemtype') { + const [branch, itemtype] = split_value.split('_'); + url += itemtype === 'any' + ? `, "me.pickup_library_id":"${branch}"` + : `, "me.pickup_library_id":"${branch}", "me.itemtype":"${itemtype}"`; + } + + url += '}'; + const totalHolds = $(table_id).data('total-holds'); + const totalHoldsSelect = parseInt(totalHolds)+1; + const multiChange = {}; + let pageStart; + const libraries = await fetch_libraries(); + var holdsQueueTable = $(table_id).kohaTable({ + ajax: { + url: url, + data: function (params) { + pageStart = params.start; + var query = { + "_per_page": params.length, + "_page": params.start / params.length + 1, + "_order_by": "priority", + "_match": "exact" + }; + return query; + }, + }, + embed: ['patron'], + columnDefs: [ + { + targets: [2, 3], + className: 'dt-body-nowrap' + }, + { + targets: [3, 9], + visible: CAN_user_reserveforothers_modify_holds_priority ? true : false + }, + + ], + columns: [ + { + data: "hold_id", + orderable: false, + searchable: false, + render: function( data, type, row, meta ) { + return ''; + } + }, + { + data: "priority", + orderable: false, + searchable: false, + render: function(data, type, row, meta) { + let select = ''; + } else if (row.status == 'P') { + select += '" disabled="disabled">'; + } else if (row.status == 'W') { + select += '" disabled="disabled">'; + } else { + if (HoldsSplitQueue !== "nothing" && HoldsSplitQueueNumbering === 'virtual') { + let virtualPriority = pageStart + meta.row + 1; + select += '" disabled="disabled">'; + } else { + select += '" disabled="disabled">'; + } + } + } + select += ''; + return select; + } + }, + { + data: "", + orderable: false, + searchable: false, + render: function(data, type, row, meta) { + if (row.status) { + return null; + } + let buttons = ''; + buttons += ''; + buttons += ''; + buttons += ''; + return buttons; + } + }, + { + data: "patron.cardnumber", + orderable: false, + searchable: true, + render: function(data, type, row, meta) { + if(data == null) { + let library = libraries.find(library => library.library_id == row.pickup_library_id); + return __("A patron from library %s").format(library.name); + } else { + return ''+data+''; + } + } + }, + { + data: "notes", + orderable: false, + searchable: false, + render: function(data, type, row, meta) { + return data; + } + }, + { + data: "hold_date", + orderable: false, + searchable: false, + render: function(data, type, row, meta) { + if (AllowHoldDateInFuture) { + return ''; + } else { + return $date(data); + } + } + }, + { + data: "expiration_date", + orderable: false, + searchable: false, + render: function(data, type, row, meta) { + return ''; + } + }, + { + data: "pickup_library_id", + orderable: false, + searchable: false, + render: function(data, type, row, meta) { + var branchSelect=''; + if ( row.status == 'T' ) { + return __("Item being transferred to %s").format(libraryname); + } else if (row.status == 'P') { + return __("Item being processed at %s").format(libraryname); + } + else if (row.status == 'W') { + return __("Item waiting at %s since %s").format(libraryname, $date(row.waiting_date)); + } else { + return branchSelect; + } + } + }, + { + data: "item_id", + orderable: false, + searchable: false, + render: function(data, type, row, meta) { + let currentCell = $(table_id).DataTable().cells({"row":meta.row, "column":meta.col}).nodes(0); + if (data) { + $.ajax({ + method: "GET", + url: "/api/v1/items/"+data, + success: function(res){ + $(currentCell).html(''+res.external_id+''); + } + }); + return '
'; + } else if (row.item_group_id) { + $.ajax({ + method: "GET", + url: "/api/v1/biblios/"+biblio_id+"/item_groups/"+row.item_group_id, + success: function(res){ + $(currentCell).html(__("Next available item group %s item").format( res.description )); + } + }); + return '
'; + } else { + if (row.non_priority) { + return ''+__("Next available")+'
'+__("Non priority hold")+''; + } else { + return ''+__("Next available")+''; + } + } + } + }, + { + data: "", + orderable: false, + searchable: false, + render: function(data, type, row, meta) { + if (row.item_id) { + return null + } else { + if(row.lowest_priority){ + return ''; + } else { + return ''; + } + } + } + }, + { + data: "hold_id", + orderable: false, + searchable: false, + render: function(data, type, row, meta) { + return ''; + } + }, + { + data: "hold_id", + orderable: false, + searchable: false, + render: function(data, type, row, meta) { + if (row.status) { + var link_value = row.status == 'T' ? __("Revert transit status") : __("Revert waiting status"); + return ''+ link_value +''; + } else { + let td = ''; + if (SuspendHoldsIntranet) { + td += ''; + } else { + td += ' '+__("Suspend")+''; + } + if (AutoResumeSuspendedHolds) { + if (row.suspended) { + td += ''; + } else { + td += ''; + } + td += ''; + } + return td; + } else { + return null; + } + } + } + }, + { + data: "hold_id", + orderable: false, + searchable: false, + render: function(data, type, row, meta) { + return ''+__("Print slip")+''; + } + }, + ] + },hold_table_settings); + $(table_id).on( 'draw.dt', function () { + // Remove the search box. Don't know why it isn't working in the table settings + $(this).parent().find(".dataTables_filter, .dt_button_clear_filter").remove(); + let multiselect = false; + var MSG_CANCEL_SELECTED = _("Cancel selected (%s)"); + $('.cancel_selected_holds').html(MSG_CANCEL_SELECTED.format($('.holds_table .select_hold:checked').length)); + $('.holds_table .select_hold').each(function() { + if(localStorage.selectedHolds && localStorage.selectedHolds.includes($(this).data('id'))) { + $(this).prop('checked', true); + } + }); + $('.holds_table .select_hold_all').on("click",function() { + var table = $(this).parents('.holds_table'); + var count = $('.select_hold:checked', table).length; + $('.select_hold', table).prop('checked', !count); + $(this).prop('checked', !count); + $(this).parent().parent().toggleClass('selected'); + $('.cancel_selected_holds').html(MSG_CANCEL_SELECTED.format($('.holds_table .select_hold:checked').length)); + localStorage.selectedHolds = $('.holds_table .select_hold:checked').toArray().map(el => $(el).data('id')); + multiselect = true; + + }); + $('.holds_table .select_hold').on("click", function() { + var table = $(this).parents('.holds_table'); + var count = $('.select_hold:not(:checked)', table).length; + $('.select_hold_all', table).prop('checked', !count); + $(this).parent().parent().toggleClass('selected'); + $('.cancel_selected_holds').html(MSG_CANCEL_SELECTED.format($('.holds_table .select_hold:checked').length)); + localStorage.selectedHolds = $('.holds_table .select_hold:checked').toArray().map(el => $(el).data('id')); + multiselect = true; + }); + $(".cancel-hold").on("click",function(e) { + e.preventDefault; + var res_id = $(this).attr('reserve_id'); + $('.select_hold').prop('checked', false); + $('.select_hold_all').prop('checked', false); + $('.cancel_selected_holds').html(MSG_CANCEL_SELECTED.format(0)); + $('#cancelModal').modal('show').find('#cancelModalConfirmBtn').attr("data-id", res_id); + delete localStorage.selectedHolds; + }); + $('.cancel_selected_holds').on("click", function(e) { + e.preventDefault(); + if($('.holds_table .select_hold:checked').length) { + delete localStorage.selectedHolds; + $('#cancelModal').modal('show'); + } + return false; + }); + $("#cancelModalConfirmBtn").one("click",function(e) { + e.preventDefault(); + let hold_ids; + const hold_id = $('#cancelModal').modal('show').find('#cancelModalConfirmBtn').attr("data-id"); + let reason = $("#modal-cancellation-reason").val(); + if (hold_id) { + hold_ids = [hold_id]; + } else { + hold_ids = $('.holds_table .select_hold:checked').toArray().map(el => $(el).data('id')); + } + $('#cancelModal').find('.modal-footer #cancelModalConfirmBtn').before(''); + deleteHolds(hold_ids, reason); + $('#cancelModal').modal('show').find('#cancelModalConfirmBtn').attr("data-id", ""); + + }); + async function deleteHolds (hold_ids, reason) { + for (const hold_id of hold_ids) { + await deleteHold(hold_id, reason); + $('#cancelModal').find('.modal-body').append('

'+__("Hold")+' '+hold_id+' '+__("cancelled")+'

'); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + holdsQueueTable.api().ajax.reload(null, false); + setTimeout(() => { + $('#cancelModal').modal('hide'); + $('#cancelModal').find('.modal-footer #cancelModalConfirmBtn').prev('img').remove(); + $('#cancelModal').find('.modal-body').find('.hold-cancelled').remove(); + }); + } + async function deleteHold(hold_id, reason) { + try { + await $.ajax({ + method: "DELETE", + url: '/api/v1/holds/' + encodeURIComponent(hold_id), + data: JSON.stringify(reason), + }); + } catch (error) { + console.error("Error when deleting hold: " + hold_id); + } + } + $(".holddate, .expirationdate").flatpickr({ + dateFormat: flatpickr_dateformat_string, + onChange: function (selectedDates, dateStr, instance){ + let hold_id = $(instance.input).attr('data-id'); + let fieldname = $(instance.input).attr('name'); + let current_date = $(instance.input).attr('data-current-date'); + const date = new Date(selectedDates); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + let newdate; + if (year && month && day) { + newdate = [year, month, day].join('-'); + } + multiChange[hold_id] = fieldname == "hold_date" ? {"hold_date": newdate} : { "expiration_date": dateStr }; + let req = fieldname == "hold_date" ? { "hold_date": newdate } : { "expiration_date": newdate }; + if (current_date != newdate && !multiselect) { + $.ajax({ + method: "PATCH", + url: '/api/v1/holds/' + encodeURIComponent(hold_id), + contentType: 'application/json', + data: JSON.stringify(req), + success: function( data ){ holdsQueueTable.api().ajax.reload(null, false); }, + error: function( jqXHR, textStatus, errorThrown) { + holdsQueueTable.api().ajax.reload(null, false); + }, + }); + } + } + }); + $(".suspenddate").flatpickr({ + dateFormat: flatpickr_dateformat_string, + onChange: function (selectedDates, dateStr, instance){ + let hold_id = $(instance.input).data('id'); + let current_date = $(instance.input).attr('data-current-date'); + const date = new Date(selectedDates); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const newdate = [year, month, day].join('-'); + const method = dateStr ? 'POST' : 'DELETE'; + multiChange[hold_id] = {suspended_until: dateStr}; + if (current_date != newdate && !multiselect) { + $.ajax({ + method: method, + url: '/api/v1/holds/' + encodeURIComponent(hold_id) +'/suspension', + contentType: 'application/json', + data: JSON.stringify({ "end_date": newdate }), + success: function( data ){ holdsQueueTable.api().ajax.reload(null, false); }, + error: function( jqXHR, textStatus, errorThrown) { + holdsQueueTable.api().ajax.reload(null, false); + }, + }); + } + } + }); + $(".toggle-suspend").one("click",function(e) { + e.preventDefault(); + const hold_id = $(this).data('id'); + const suspended = $(this).attr('data-suspended'); + const method = suspended == 'true' ? 'DELETE' : 'POST'; + if (!multiselect) { + $.ajax({ + method: method, + url: '/api/v1/holds/' + encodeURIComponent(hold_id) +'/suspension', + success: function( data ){ holdsQueueTable.api().ajax.reload(null, false); }, + error: function( jqXHR, textStatus, errorThrown) { + holdsQueueTable.api().ajax.reload(null, false); + alert('There was an error:'+textStatus+" "+errorThrown); + }, + }); + } + }); + $(".rank-request").on("change", function(e){ + e.preventDefault(); + const hold_id = $(this).data('id'); + let priority = e.target.value; + multiChange[hold_id] = {priority: priority}; + if (!multiselect) { + $.ajax({ + method: "PUT", + url: '/api/v1/holds/' + encodeURIComponent(hold_id) +'/priority', + data: JSON.stringify(priority), + success: function( data ){ holdsQueueTable.api().ajax.reload(null, false); }, + error: function( jqXHR, textStatus, errorThrown) { + alert('There was an error:'+textStatus+" "+errorThrown); + }, + }); + } + }); + $(".move-hold").one("click", function(e){ + e.preventDefault(); + let toPosition = $(this).attr('data-move-hold'); + let priority = $(this).attr('data-priority'); + var res_id = $(this).attr('reserve_id'); + var moveTo; + if (toPosition == 'up'){ + moveTo = parseInt(priority)-1; + } + if (toPosition == 'down'){ + moveTo = parseInt(priority)+1; + } + if (toPosition == 'top'){ + moveTo = 1; + } + if (toPosition == 'bottom'){ + moveTo = totalHolds; + } + if (!multiselect) { + $.ajax({ + method: "PUT", + url: '/api/v1/holds/' + encodeURIComponent(res_id) +'/priority', + data: JSON.stringify(moveTo), + success: function( data ){ + holdsQueueTable.api().ajax.reload(null, false); + }, + error: function( jqXHR, textStatus, errorThrown) { + alert('There was an error:'+textStatus+" "+errorThrown); + }, + }); + } + }); + $(".toggle-lowest-priority").one("click", function(e){ + e.preventDefault(); + var res_id = $(this).attr('data-reserve_id'); + $.ajax({ + method: "PUT", + url: '/api/v1/holds/' + encodeURIComponent(res_id) +'/lowest_priority', + success: function( data ){ holdsQueueTable.api().ajax.reload(null, false); }, + error: function( jqXHR, textStatus, errorThrown) { + alert('There was an error:'+textStatus+" "+errorThrown); + }, + }); + }); + $(".hold_location_select").on("change", function(){ + $(this).prop("disabled",true); + var cur_select = $(this); + var res_id = $(this).attr('reserve_id'); + multiChange[res_id] = {pickup_library_id: $(this).val()}; + if (!multiselect) { + $(this).after('
'); + let api_url = '/api/v1/holds/' + encodeURIComponent(res_id) + '/pickup_location'; + $.ajax({ + method: "PUT", + url: api_url, + data: JSON.stringify({ "pickup_library_id": $(this).val() }), + headers: { "x-koha-override": "any" }, + success: function( data ){ holdsQueueTable.api().ajax.reload(null, false); }, + error: function( jqXHR, textStatus, errorThrown) { + alert('There was an error:'+textStatus+" "+errorThrown); + cur_select.prop("disabled",false); + $("#updating_reserveno"+res_id).remove(); + cur_select.val( cur_select.children('option[selected="selected"]').val() ); + }, + }); + } + }); + $('.printholdslip').one('click', function(){ + var reserve_id = $(this).attr('data-reserve_id'); + window.open("/cgi-bin/koha/circ/hold-transfer-slip.pl?reserve_id=" + reserve_id); + return false; + }); + }); + + $('.update_selected_holds').on('click', function (e) { + const data = [] + holdsQueueTable.api().rows('.selected').every(function (index, element) { + data.push(this.data()); + }); + fieldNames = { + hold_id: 'reserve_id', + hold_date: 'reservedate', + pickup_library_id: 'pickup', + item_id: 'itemnumber', + expiration_date: 'expirationdate', + suspended_until: 'suspend_until', + priority: 'rank-request', + }; + let biblionumberInput = $("").attr({ + type: "hidden", + name: "biblionumber", + value: biblio_id + }); + $("#existing_holds").append(biblionumberInput); + data.forEach((row) => { + Object.keys(row).forEach((key) => { + let update = multiChange[row.hold_id]; + if (update && update[key]) { + row[key] = update[key]; + } + if (fieldNames[key] && row[key] != null) { + let input = $("").attr({ + type: "hidden", + name: fieldNames[key], + value: row[key] + }); + $("#existing_holds").append(input); + } + }); + }); + }); +} diff --git a/reserve/request.pl b/reserve/request.pl index b19fa3b868..7a9b8bd8af 100755 --- a/reserve/request.pl +++ b/reserve/request.pl @@ -595,6 +595,9 @@ if ( ( $findborrower && $borrowernumber_hold || $findclub && $club_hold ) # existingreserves building my @reserveloop; + my $total_holds = 0; + my $hold_branches = []; + my $hold_itemtypes = []; my $always_show_holds = $input->cookie('always_show_holds'); $template->param( always_show_holds => $always_show_holds ); my $show_holds_now = $input->param('show_holds_now'); @@ -610,61 +613,29 @@ if ( ( $findborrower && $borrowernumber_hold || $findclub && $club_hold ) )->unblessed } }; - my @reserves = Koha::Holds->search( { biblionumber => $biblionumber }, { order_by => 'priority' } )->as_list; - foreach my $res ( - sort { - my $a_found = $a->found() || ''; - my $b_found = $a->found() || ''; - $a_found cmp $b_found; - } @reserves - ) - { - my %reserve; - if ( $res->is_found() ) { - $reserve{'holdingbranch'} = $res->item()->holdingbranch(); - $reserve{'biblionumber'} = $res->item()->biblionumber(); - $reserve{'barcodenumber'} = $res->item()->barcode(); - $reserve{'wbrcode'} = $res->branchcode(); - $reserve{'itemnumber'} = $res->itemnumber(); - $reserve{'wbrname'} = $res->branch()->branchname(); - $reserve{'atdestination'} = $res->is_at_destination(); - $reserve{'desk_name'} = ( $res->desk() ) ? $res->desk()->desk_name() : '' ; - $reserve{'found'} = $res->is_found(); - $reserve{'inprocessing'} = $res->is_in_processing(); - $reserve{'intransit'} = $res->is_in_transit(); - } - elsif ( $res->priority() > 0 ) { - if ( my $item = $res->item() ) { - $reserve{'itemnumber'} = $item->id(); - $reserve{'barcodenumber'} = $item->barcode(); - $reserve{'item_level_hold'} = 1; - } + $total_holds = Koha::Holds->search( { biblionumber => $biblionumber } )->count; + my $branches = Koha::Holds->search( + { + biblionumber => $biblionumber, + }, + { + select => [ { distinct => 'branchcode' } ], + as => [qw( branchcode )], } - $reserve{'expirationdate'} = $res->expirationdate; - $reserve{'date'} = $res->reservedate; - $reserve{'borrowernumber'} = $res->borrowernumber(); - $reserve{'biblionumber'} = $res->biblionumber(); - $reserve{'patron'} = $res->borrower; - $reserve{'notes'} = $res->reservenotes(); - $reserve{'waiting_date'} = $res->waitingdate(); - $reserve{'ccode'} = $res->item() ? $res->item()->ccode() : undef; - $reserve{'barcode'} = $res->item() ? $res->item()->barcode() : undef; - $reserve{'priority'} = $res->priority(); - $reserve{'lowestPriority'} = $res->lowestPriority(); - $reserve{'suspend'} = $res->suspend(); - $reserve{'suspend_until'} = $res->suspend_until(); - $reserve{'reserve_id'} = $res->reserve_id(); - $reserve{itemtype} = $res->itemtype(); - $reserve{branchcode} = $res->branchcode(); - $reserve{non_priority} = $res->non_priority(); - $reserve{object} = $res; - - if ( $holds_count_per_patron->{ $reserve{'borrowernumber'} } == 1 ) { - $reserve{'change_hold_type_allowed'} = 1; + )->unblessed; + + $hold_branches = [ map { $_->{branchcode} } @$branches ]; + my $itemtypes = Koha::Holds->search( + { + biblionumber => $biblionumber, + }, + { + select => [ { distinct => 'itemtype' } ], + as => [qw( itemtype )], } - - push( @reserveloop, \%reserve ); - } + )->unblessed; + + $hold_itemtypes = [ map { $_->{itemtype} ? $_->{itemtype} : 'any' } @$itemtypes ]; } # get the time for the form name... @@ -692,11 +663,16 @@ if ( ( $findborrower && $borrowernumber_hold || $findclub && $club_hold ) $biblioloopiter{author} = $biblio->author; $biblioloopiter{rank} = $fixedRank; $biblioloopiter{reserveloop} = \@reserveloop; + $biblioloopiter{total_holds} = $total_holds; + $biblioloopiter{hold_branches} = $hold_branches; + $biblioloopiter{hold_itemtypes} = $hold_itemtypes; if (@reserveloop) { $template->param( reserveloop => \@reserveloop ); } + $template->param( total_holds => $total_holds ); + if ( $patron && $multi_hold ) { # Add the valid pickup locations my @pickup_locations = $biblio->pickup_locations({ patron => $patron })->as_list; -- 2.34.1