From 3b4e3557ace379e2f1a5fd9adef7ac6c038e0f53 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 --- Koha/REST/V1/Holds.pm | 5 +- api/v1/swagger/definitions/hold.yaml | 10 + api/v1/swagger/paths/holds.yaml | 11 +- .../prog/en/includes/holds_table.inc | 258 +-------- .../prog/en/modules/reserve/request.tt | 340 +++-------- koha-tmpl/intranet-tmpl/prog/js/holds.js | 547 ++++++++++++++++++ reserve/request.pl | 80 +-- 7 files changed, 690 insertions(+), 561 deletions(-) diff --git a/Koha/REST/V1/Holds.pm b/Koha/REST/V1/Holds.pm index 919d1bf8b0..a6127c4924 100644 --- a/Koha/REST/V1/Holds.pm +++ b/Koha/REST/V1/Holds.pm @@ -278,8 +278,9 @@ sub edit { $pickup_library_id //= $hold->branchcode; my $priority = $body->{priority} // $hold->priority; - my $hold_date = $body->{hold_date} // $hold->reservedate; - my $expiration_date = $body->{expiration_date} // $hold->expirationdate; + my $hold_date = exists $body->{hold_date} ? $body->{hold_date} : $hold->reservedate; + my $expiration_date = exists $body->{expiration_date} ? $body->{expiration_date} : $hold->expirationdate; + # suspended_until can also be set to undef my $suspended_until = $body->{suspended_until} || $hold->suspend_until; diff --git a/api/v1/swagger/definitions/hold.yaml b/api/v1/swagger/definitions/hold.yaml index 2ba3367e04..9a7cb7e8c2 100644 --- a/api/v1/swagger/definitions/hold.yaml +++ b/api/v1/swagger/definitions/hold.yaml @@ -123,6 +123,16 @@ properties: - object - "null" description: The item + item_group: + type: + - object + - "null" + description: The item group + 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..b56980c2be 100644 --- a/api/v1/swagger/paths/holds.yaml +++ b/api/v1/swagger/paths/holds.yaml @@ -104,6 +104,9 @@ - cancellation_requested - biblio - pickup_library + - patron + - item + - item_group collectionFormat: csv produces: - application/json @@ -289,11 +292,15 @@ format: date-time hold_date: description: Hold date - type: string + type: + - string + - "null" format: date expiration_date: description: Hold's expiration date - type: string + type: + - string + - "null" format: date additionalProperties: false consumes: 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 8b8addf15e..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 22bd8a8ab9..0fd807f734 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt @@ -1230,7 +1230,7 @@ [% END %] [% END %] [% END %] - [% IF ( reserveloop ) %] + [% IF ( total_holds ) %]
[% INCLUDE 'csrf-token.inc' %] [% IF ( multi_hold ) %] @@ -1248,10 +1248,6 @@

Existing holds

-
- - -
@@ -1271,147 +1267,80 @@
[% 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 %]
@@ -1484,6 +1413,9 @@ [% INCLUDE 'calendar.inc' %] [% INCLUDE 'columns_settings.inc' %] [% INCLUDE 'select2.inc' %] + [% Asset.js("js/holds.js") | $raw%] [% Asset.js("js/form-submit.js") | $raw%] @@ -1493,14 +1425,16 @@ [% END %] @@ -2008,13 +1855,6 @@ }); [% END %] - [% END %] [% INCLUDE 'intranet-bottom.inc' %] diff --git a/koha-tmpl/intranet-tmpl/prog/js/holds.js b/koha-tmpl/intranet-tmpl/prog/js/holds.js index d18d394787..a67ef904d7 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/holds.js +++ b/koha-tmpl/intranet-tmpl/prog/js/holds.js @@ -498,3 +498,550 @@ $(document).ready(function() { }); }); + +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; + let pageStart; + 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', 'item', 'item_group'], + 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._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: "", + orderable: false, + searchable: false, + render: function(data, type, row, meta) { + if (row.item_id) { + return ''+row.item.external_id+'' + } else if (row.item_group_id) { + return __("Next available item from group %s").format( row.item_group.description ); + } 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) { + if (row.status == 'W' || row.status == 'T') { + return ''+__("Print slip")+''; + } else { + return ''; + } + } + }, + ] + },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(".pager .table_controls .dt-search").remove(); + $(this).parent().find(".pager .table_controls .dt-buttons .dt_button_clear_filter").remove(); + 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')); + + }); + $('.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')); + }); + $(".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; + }); + // Remove any previously attached handlers + $("#cancelModalConfirmBtn").off("click"); + // Attach the handler to the button + $("#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({ + onReady: function(selectedDates, dateStr, instance) { + $(instance.altInput) + .wrap("") + .after($("") + .attr("href", "#") + .addClass("clear_date") + .addClass("fa fa-times") + .addClass("ps-2") + .on("click", function(e) { + e.preventDefault(); + instance.clear(); + }) + .attr("aria-hidden", true) + ); + }, + 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'); + dateStr = dateStr ? dateStr : null; + let req = fieldname == "hold_date" ? { "hold_date": dateStr } : { "expiration_date": dateStr }; + if (current_date != dateStr) { + $.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({ + onReady: function(selectedDates, dateStr, instance) { + $(instance.altInput) + .wrap("") + .after($("") + .attr("href", "#") + .addClass("clear_date") + .addClass("fa fa-times") + .addClass("ps-2") + .on("click", function(e) { + e.preventDefault(); + instance.clear(); + }) + .attr("aria-hidden", true) + ); + }, + }); + $(".toggle-suspend").one("click",function(e) { + e.preventDefault(); + const hold_id = $(this).data('id'); + const suspended = $(this).attr('data-suspended'); + const input = $(`.suspenddate[data-id="${hold_id}"]`); + const method = suspended == 'true' ? 'DELETE' : 'POST'; + let end_date = input.val() && method == 'POST' ? input.val() : null; + let params = (end_date !== null && end_date !== '') ? JSON.stringify({ "end_date": end_date }) : null; + $.ajax({ + method: method, + url: '/api/v1/holds/' + encodeURIComponent(hold_id) +'/suspension', + contentType: 'application/json', + data: params, + 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; + $.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; + } + $.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'); + $(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; + }); + }); +} diff --git a/reserve/request.pl b/reserve/request.pl index b9a3182d7c..20742ab809 100755 --- a/reserve/request.pl +++ b/reserve/request.pl @@ -603,6 +603,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'); @@ -618,61 +621,27 @@ 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... @@ -700,11 +669,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