From 0b081ff3effb5d0bdcaa82d0ae1053b22d5c5c0f 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 Signed-off-by: Nick Clemens --- Koha/Hold.pm | 18 + Koha/REST/V1/Holds.pm | 26 +- admin/columns_settings.yml | 1 + api/v1/swagger/definitions/hold.yaml | 10 + api/v1/swagger/paths/holds.yaml | 20 +- .../prog/en/includes/holds_table.inc | 366 +---- .../prog/en/modules/reserve/request.tt | 268 +--- koha-tmpl/intranet-tmpl/prog/js/datatables.js | 3 + koha-tmpl/intranet-tmpl/prog/js/holds.js | 1363 +++++++++++++++-- reserve/request.pl | 94 +- 10 files changed, 1412 insertions(+), 757 deletions(-) diff --git a/Koha/Hold.pm b/Koha/Hold.pm index d886f7d8b55..91269aa5356 100644 --- a/Koha/Hold.pm +++ b/Koha/Hold.pm @@ -783,6 +783,24 @@ sub is_suspended { return $self->suspend(); } +=head3 item_level_holds + +Returns the number (count) of item-level holds for this hold's biblionumber and patron + +=cut + +sub item_level_holds { + my ($self) = @_; + + return Koha::Holds->search( + { + biblionumber => $self->biblionumber, + borrowernumber => $self->borrowernumber, + itemnumber => { '!=', undef }, + } + )->count; +} + =head3 add_cancellation_request my $cancellation_request = $hold->add_cancellation_request({ [ creation_date => $creation_date ] }); diff --git a/Koha/REST/V1/Holds.pm b/Koha/REST/V1/Holds.pm index ad5b4c4d647..90fdc38f7c8 100644 --- a/Koha/REST/V1/Holds.pm +++ b/Koha/REST/V1/Holds.pm @@ -281,21 +281,27 @@ 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 $priority = $body->{priority} // $hold->priority; + my $hold_date = exists $body->{hold_date} ? $body->{hold_date} : $hold->reservedate; + my $expiration_date = exists $body->{expiration_date} ? $body->{expiration_date} : $hold->expirationdate; + my $item_id = exists $body->{item_id} ? $body->{item_id} : $hold->itemnumber; + my $item_level = + exists $body->{item_level} + ? ( $body->{item_level} ? 1 : 0 ) + : $hold->item_level_hold; # suspended_until can also be set to undef my $suspended_until = $body->{suspended_until} || $hold->suspend_until; my $params = { - reserve_id => $hold->id, - branchcode => $pickup_library_id, - rank => $priority, - suspend_until => $suspended_until, - itemnumber => $hold->itemnumber, - reservedate => $hold_date, - expirationdate => $expiration_date, + reserve_id => $hold->id, + branchcode => $pickup_library_id, + rank => $priority, + suspend_until => $suspended_until, + itemnumber => $item_id, + reservedate => $hold_date, + expirationdate => $expiration_date, + item_level_hold => $item_level }; C4::Reserves::ModReserve($params); diff --git a/admin/columns_settings.yml b/admin/columns_settings.yml index c249e0d0fda..0f5e37be684 100644 --- a/admin/columns_settings.yml +++ b/admin/columns_settings.yml @@ -2115,6 +2115,7 @@ modules: columnname: action patron_holds_table: + default_sort_order: 1 columns: - columnname: checkbox diff --git a/api/v1/swagger/definitions/hold.yaml b/api/v1/swagger/definitions/hold.yaml index 9a62c768c44..7ce4fb6fd9d 100644 --- a/api/v1/swagger/definitions/hold.yaml +++ b/api/v1/swagger/definitions/hold.yaml @@ -114,6 +114,11 @@ properties: item_level: type: boolean description: If the hold is placed at item level + item_level_holds: + type: + - integer + - "null" + description: Count of item level holds for this bibliographic record cancellation_requested: type: - boolean @@ -134,6 +139,11 @@ properties: - object - "null" description: The item + item_group: + type: + - object + - "null" + description: The item group pickup_library: type: - object diff --git a/api/v1/swagger/paths/holds.yaml b/api/v1/swagger/paths/holds.yaml index dcd6413c03a..71474e1218a 100644 --- a/api/v1/swagger/paths/holds.yaml +++ b/api/v1/swagger/paths/holds.yaml @@ -108,6 +108,8 @@ - pickup_library - item - patron + - item_group + - item_level_holds collectionFormat: csv produces: - application/json @@ -430,12 +432,26 @@ 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 + item_id: + description: Internal item identifier + type: + - integer + - "null" + item_level: + description: Whether the hold is item-level or not + type: + - boolean + - "null" additionalProperties: false consumes: - 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 8e44790a453..a12f41c60fb 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/holds_table.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/holds_table.inc @@ -2,379 +2,25 @@ [% SET hold_cancellation = AuthorisedValues.GetAuthValueDropbox('HOLD_CANCELLATION') %] [% USE AuthorisedValues %] - +[%#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 -%] - [% SET tr_class = hold.suspend ? 'suspend' : '' %] - - - - [%- 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 %] - - [% END %] -
- - [% hold.priority | html %] -
-
-
- - - - - - - - - - - - - - - - [% 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.hold_group_id -%] -
(part of a hold group)
- [%- 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 18056f0b06c..3d818f1d91a 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt @@ -1311,7 +1311,7 @@ [% END %] [% END %] [% END %] - [% IF ( reserveloop ) %] + [% IF ( total_holds ) %]
[% INCLUDE 'csrf-token.inc' %] [% IF ( multi_hold ) %] @@ -1330,11 +1330,7 @@

Existing holds

- - -
-
- +
[% 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 %]
@@ -1605,6 +1539,9 @@ [% INCLUDE 'datatables.inc' %] [% INCLUDE 'calendar.inc' %] [% INCLUDE 'select2.inc' %] + [% Asset.js("js/holds.js") | $raw %] [% Asset.js("js/hold-group.js") | $raw %] @@ -1613,17 +1550,16 @@ [% SET url_biblio_params = url_biblio_params _ "&multi_hold=1" %] [% END %] [% END %] - [% END %] [% INCLUDE 'intranet-bottom.inc' %] diff --git a/koha-tmpl/intranet-tmpl/prog/js/datatables.js b/koha-tmpl/intranet-tmpl/prog/js/datatables.js index 7b6dde8a7c6..b3cd2441d05 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/datatables.js +++ b/koha-tmpl/intranet-tmpl/prog/js/datatables.js @@ -1456,6 +1456,9 @@ function update_search_description( emptyTable: options.emptyTable ? options.emptyTable : __("No data available in table"), + infoFiltered: options.infoFiltered + ? options.infoFiltered + : __("(filtered from _MAX_ total entries)"), }, }, options diff --git a/koha-tmpl/intranet-tmpl/prog/js/holds.js b/koha-tmpl/intranet-tmpl/prog/js/holds.js index 5e6537fa721..e89bdc8d4d7 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/holds.js +++ b/koha-tmpl/intranet-tmpl/prog/js/holds.js @@ -791,13 +791,13 @@ $(document).ready(function () { } }); - if (!patron_page) { - $(".holds_table .select_hold_all").each(function () { - var table = $(this).parents(".holds_table"); - var count = $(".select_hold:not(:checked)", table).length; - $(".select_hold_all", table).prop("checked", !count); - }); - } + // if (!patron_page) { + // $(".holds_table .select_hold_all").each(function () { + // var table = $(this).parents(".holds_table"); + // var count = $(".select_hold:not(:checked)", table).length; + // $(".select_hold_all", table).prop("checked", !count); + // }); + // } function updateSelectedHoldsButtonCounters() { $(".move_selected_holds").html( @@ -808,20 +808,15 @@ $(document).ready(function () { $(".selected_holds_count").html( $(".holds_table .select_hold:checked").length ); - if (patron_page) { - var selectedHolds = $(".holds_table .select_hold:checked"); - var hasSelectedHolds = selectedHolds.length > 0; - var hasMultipleSelectedHolds = selectedHolds.length >= 2; + var selectedHolds = $(".holds_table .select_hold:checked"); + var hasSelectedHolds = selectedHolds.length > 0; + var hasMultipleSelectedHolds = selectedHolds.length >= 2; - $(".cancel_selected_holds, .suspend_selected_holds").prop( - "disabled", - !hasSelectedHolds - ); - $(".group_selected_holds").prop( - "disabled", - !hasMultipleSelectedHolds - ); - } + $(".cancel_selected_holds, .suspend_selected_holds").prop( + "disabled", + !hasSelectedHolds + ); + $(".group_selected_holds").prop("disabled", !hasMultipleSelectedHolds); } function updateMoveButtons(table) { @@ -841,96 +836,73 @@ $(document).ready(function () { $(".move_selected_holds").prop("disabled", !checked_count); } - updateSelectedHoldsButtonCounters(); + if (holds_table_patron_page()) { + updateSelectedHoldsButtonCounters(); - $(".holds_table .select_hold_all").click(function () { - var table; - if (!patron_page) { - table = $(this).parents(".holds_table"); - } else { - table = $(".holds_table:not(.fixedHeader-floating)"); - } + $(".holds_table .select_hold_all").click(function () { + var table = $(".holds_table:not(.fixedHeader-floating)"); - var checked_count = $(".select_hold:checked", table).length; - $(".select_hold", table).prop("checked", !checked_count); - $(this).prop("checked", !checked_count); + var checked_count = $(".select_hold:checked", table).length; + $(".select_hold", table).prop("checked", !checked_count); + $(this).prop("checked", !checked_count); - updateMoveButtons(table); + updateMoveButtons(table); - updateSelectedHoldsButtonCounters(); - $("#cancel_hold_alert").html( - MSG_CANCEL_ALERT.format( - $(".holds_table .select_hold:checked").length - ) - ); - $("#cancel_hold_alert").show(); - localStorage.selectedHolds = - "[" + - $(".holds_table .select_hold:checked") - .toArray() - .map(el => - JSON.stringify({ - hold: $(el).data("id"), - borrowernumber: $(el).data("borrowernumber"), - biblionumber: $(el).data("biblionumber"), - }) + updateSelectedHoldsButtonCounters(); + $("#cancel_hold_alert").html( + MSG_CANCEL_ALERT.format( + $(".holds_table .select_hold:checked").length ) - .join(",") + - "]"; - }); + ); + $("#cancel_hold_alert").show(); + localStorage.selectedHolds = + "[" + + $(".holds_table .select_hold:checked") + .toArray() + .map(el => + JSON.stringify({ + hold: $(el).data("id"), + borrowernumber: $(el).data("borrowernumber"), + biblionumber: $(el).data("biblionumber"), + }) + ) + .join(",") + + "]"; + }); - $(".holds_table").on("click", ".select_hold", function () { - var table = $(this).parents(".holds_table"); - var count = $(".select_hold:not(:checked)", table).length; - $(".select_hold_all", table).prop("checked", !count); + $(".holds_table").on("click", ".select_hold", function () { + var table = $(this).parents(".holds_table"); + var count = $(".select_hold:not(:checked)", table).length; + $(".select_hold_all", table).prop("checked", !count); - updateMoveButtons(table); + updateMoveButtons(table); - updateSelectedHoldsButtonCounters(); - $("#cancel_hold_alert").html( - MSG_CANCEL_ALERT.format( - $(".holds_table .select_hold:checked").length - ) - ); - $("#cancel_hold_alert").show(); - localStorage.selectedHolds = - "[" + - $(".holds_table .select_hold:checked") - .toArray() - .map(el => - JSON.stringify({ - hold: $(el).data("id"), - borrowernumber: $(el).data("borrowernumber"), - biblionumber: $(el).data("biblionumber"), - }) + updateSelectedHoldsButtonCounters(); + $("#cancel_hold_alert").html( + MSG_CANCEL_ALERT.format( + $(".holds_table .select_hold:checked").length ) - .join(",") + - "]"; - }); - - $(".cancel_selected_holds").click(function (e) { - e.preventDefault(); - if ($(".holds_table .select_hold:checked").length) { - $("#cancel_modal_form #inputs").empty(); - if (!patron_page) { - biblionumbers.forEach(function (biblionumber) { - $("#cancel_modal_form #inputs").append( - '' - ); - }); - $("#cancel_modal_form #inputs").append( - '' - ); - let hold_ids = $(".holds_table .select_hold:checked") + ); + $("#cancel_hold_alert").show(); + localStorage.selectedHolds = + "[" + + $(".holds_table .select_hold:checked") .toArray() - .map(el => $(el).data("id")) - .join(","); - $("#cancel_modal_form #inputs").append( - '' - ); - } else { + .map(el => + JSON.stringify({ + hold: $(el).data("id"), + borrowernumber: $(el).data("borrowernumber"), + biblionumber: $(el).data("biblionumber"), + }) + ) + .join(",") + + "]"; + }); + + $(".cancel_selected_holds").click(function (e) { + e.preventDefault(); + if ($(".holds_table .select_hold:checked").length) { + $("#cancel_modal_form #inputs").empty(); $("#cancel_modal_form #inputs").append( '' ); @@ -950,14 +922,12 @@ $(document).ready(function () { JSON.parse(hold_data).forEach(function (hold) { _append_patron_page_cancel_hold_modal_data(hold); }); + delete localStorage.selectedHolds; + $("#cancelModal").modal("show"); } - - delete localStorage.selectedHolds; - $("#cancelModal").modal("show"); - } - return false; - }); - + return false; + }); + } $("#itemSearchForm").on("submit", function (event) { event.preventDefault(); $("#move_hold_item_confirm").prop("disabled", true); @@ -1076,18 +1046,26 @@ $(document).ready(function () { $("#itemResultMessage").empty(); $("#move_hold_item_selection table tbody").empty(); $("#moveHoldItemModal").modal("show"); - $(".select_hold:checked").each(function () { - let reserve_id = $(this).data("id"); - let reserve_biblionumber = $(this).data("biblionumber"); - let reserve_itemnumber = $(this).data("itemnumber"); - let item_level_hold = $(this).data("item_level_hold"); - let item_waiting = $(this).data("waiting"); - let item_intransit = $(this).data("intransit"); - let error_message = $(this).data("item_level_hold") - ? "" - : __( - "Cannot move a waiting, in transit, or record level hold" - ); + const selectedHolds = + JSON.parse(localStorage.selectedHolds) || + $(".select_hold:checked"); + $(selectedHolds).each(function () { + let reserve_id = this.hold || $(this).data("id"); + let reserve_biblionumber = + this.biblionumber || $(this).data("biblionumber"); + let reserve_itemnumber = + this.itemnumber || $(this).data("itemnumber"); + let item_level_hold = + this.item_level_hold || $(this).data("item_level_hold"); + let item_waiting = this.waiting || $(this).data("waiting"); + let item_intransit = + this.intransit || $(this).data("intransit"); + let error_message = + this.item_level_hold || $(this).data("item_level_hold") + ? "" + : __( + "Cannot move a waiting, in transit, or record level hold" + ); let found_status = $(this).data("found"); if (item_level_hold && (!item_waiting || !item_intransit)) { $("#move_hold_item_selection table").append( @@ -1105,23 +1083,31 @@ $(document).ready(function () { $(".move_hold_biblio").click(function (e) { e.preventDefault(); $("#move_hold_biblio_confirm").prop("disabled", true); - if ($(".holds_table .select_hold:checked").length) { + const selectedHolds = + JSON.parse(localStorage.selectedHolds || "[]") || + $(".select_hold:checked"); + if (selectedHolds.length) { $("#biblioResultMessage").empty(); $("#move_hold_biblio_selection table tbody").empty(); $("#moveHoldBiblioModal").modal("show"); - $(".select_hold:checked").each(function () { - let reserve_id = $(this).data("id"); - let reserve_biblionumber = $(this).data("biblionumber"); - let reserve_itemnumber = $(this).data("itemnumber"); - let item_level_hold = $(this).data("item_level_hold"); - let item_status = $(this).data("status"); - let item_waiting = $(this).data("waiting"); - let item_intransit = $(this).data("intransit"); - let error_message = $(this).data("item_level_hold") - ? __( - "Cannot move a waiting, in transit, or item level hold" - ) - : ""; + $(selectedHolds).each(function () { + let reserve_id = this.hold || $(this).data("id"); + let reserve_biblionumber = + this.biblionumber || $(this).data("biblionumber"); + let reserve_itemnumber = + this.itemnumber || $(this).data("itemnumber"); + let item_level_hold = + this.item_level_hold || $(this).data("item_level_hold"); + let item_status = this.status || $(this).data("status"); + let item_waiting = this.waiting || $(this).data("waiting"); + let item_intransit = + this.intransit || $(this).data("intransit"); + let error_message = + this.item_level_hold || $(this).data("item_level_hold") + ? __( + "Cannot move a waiting, in transit, or item level hold" + ) + : ""; let found_status = $(this).data("found"); if (!item_level_hold && (!item_waiting || !item_intransit)) { $("#move_hold_biblio_selection table").append( @@ -1226,12 +1212,15 @@ $(document).ready(function () { }), }); } - - $("#cancelModal").on("hidden.bs.modal", function () { - $("#cancelModal .modal-body .alert-danger").remove(); - $("#cancelModalConfirmBtn").prop("disabled", false); - holdsTable.api().ajax.reload(); - }); + if (holds_table_patron_page()) { + $("#cancelModal").on("hidden.bs.modal", function () { + $("#cancelModal .modal-body .alert-danger").remove(); + $("#cancelModalConfirmBtn").prop("disabled", false); + if (holdsTable) { + holdsTable.api().ajax.reload(); + } + }); + } $("#group-modal-submit").click(function (e) { e.preventDefault(); @@ -1303,3 +1292,1099 @@ $(document).ready(function () { ); } }); + +async function load_patron_holds_table(biblio_id, split_data) { + const { name: split_name, value: split_value } = split_data; + let table_class = `patron_holds_table_${biblio_id}_${split_value}`; + const table_id = `#` + table_class; + 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; + var holdsQueueTable = $(table_id).kohaTable( + { + language: { + infoFiltered: "", + }, + ajax: { + url: url, + }, + embed: ["patron", "item", "item_group", "item_level_holds"], + 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: true, + 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 = + meta.settings._iDisplayStart + + 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: true, + 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: true, + searchable: false, + render: function (data, type, row, meta) { + if (AllowHoldDateInFuture) { + return ( + '' + ); + } else { + return $date(data); + } + }, + }, + { + data: "expiration_date", + orderable: true, + searchable: false, + render: function (data, type, row, meta) { + return ( + '' + ); + }, + }, + { + data: "pickup_library_id", + orderable: true, + 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.status) { + return ( + '' + + row.item.external_id + + "" + ); + } else if (row.item_id && row.item_level) { + let barcode = row.item.external_id + ? row.item.external_id + : __("No barcode"); + if (row.item_level_holds >= 2) { + let link = + '' + + (row.item.external_id + ? row.item.external_id.escapeHtml() + : __("No barcode")) + + ""; + return __("Only item") + " " + link; + } else { + let select = + '"; + return select; + } + } else if (row.item_group_id) { + return __( + "Next available item from group %s" + ).format(row.item_group.description); + } else if (row.hold_group_id) { + return ( + "
(" + + __("part of") + + ' ' + + __("hold group") + + ")
" + ); + } 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 + ); + holdsQueueTable.api().page(0).draw(false); + // Clear selectedHolds on page load + localStorage.removeItem("selectedHolds"); + $(table_id).on("draw.dt", function () { + function updateMSGCounters() { + var MSG_CANCEL_SELECTED = __("Cancel selected (%s)"); + var MSG_MOVE_SELECTED = __("Move selected (%s)"); + var MSG_CANCEL_ALERT = __( + "This action will cancel %s hold(s)." + ); + var selectedCount = JSON.parse( + localStorage.selectedHolds || "[]" + ).length; + $(".cancel_selected_holds").html( + MSG_CANCEL_SELECTED.format(selectedCount) + ); + $(".move_selected_holds").html( + MSG_MOVE_SELECTED.format(selectedCount) + ); + $("#cancel_hold_alert").html( + MSG_CANCEL_ALERT.format(selectedCount) + ); + if (selectedCount > 0) { + $("#cancel_hold_alert").show(); + } else { + $("#cancel_hold_alert").hide(); + } + } + + function updateMoveButtons() { + var checked_holds = JSON.parse(localStorage.selectedHolds || "[]"); + var checked_count = checked_holds.length; + + var item_level_count = checked_holds.filter(function (hold) { + return hold.item_level_hold > 0; + }).length; + + var record_level_count = checked_holds.filter(function (hold) { + return hold.item_level_hold === 0; + }).length; + + $(".move_hold_item").toggleClass("disabled", item_level_count <= 0); + $(".move_hold_biblio").toggleClass( + "disabled", + record_level_count <= 0 + ); + $(".move_selected_holds").prop("disabled", !checked_count); + } + + // Always deselect the "select all" checkbox when the page changes + $(".holds_table .select_hold_all").prop("checked", false); + updateMSGCounters(); + $(".holds_table .select_hold." + table_class).each(function () { + const selected = JSON.parse(localStorage.selectedHolds || "[]"); + const holdId = $(this).data("id"); + if (selected.some(s => s.hold === holdId)) { + $(this).prop("checked", true); + $(this).parent().parent().addClass("selected"); + var table = $(this).closest(".holds_table"); + var count = $( + ".select_hold." + table_class + ":not(:checked)", + table + ).length; + $(".select_hold_all." + table_class, table).prop( + "checked", + !count + ); + } + }); + $(".holds_table .select_hold_all").on("click", function () { + var table = $(this).closest(".holds_table"); + var isChecked = $(this).prop("checked"); + var allCheckboxes = table + .DataTable() + .rows({ search: "applied" }) + .nodes(); + let selected = JSON.parse(localStorage.selectedHolds || "[]"); + let pageHolds = []; + + $("input.select_hold", allCheckboxes).each(function () { + let hold_data = { + hold: $(this).data("id"), + borrowernumber: $(this).data("borrowernumber"), + biblionumber: $(this).data("biblionumber"), + itemnumber: $(this).data("itemnumber"), + waiting: $(this).data("waiting"), + intransit: $(this).data("intransit"), + status: $(this).data("status"), + item_level_hold: $(this).data("item_level_hold"), + hold_group_id: $(this).data("hold-group-id"), + }; + pageHolds.push(hold_data); + }); + + if (isChecked) { + // Add all page holds to the selection, avoiding duplicates + pageHolds.forEach(hold => { + if (!selected.some(s => s.hold === hold.hold)) { + selected.push(hold); + } + }); + $("input.select_hold", allCheckboxes).prop("checked", true); + $("input.select_hold", allCheckboxes).each(function () { + $(this).parent().parent().addClass("selected"); + }); + } else { + // Remove all page holds from the selection + const pageIds = pageHolds.map(h => h.hold); + selected = selected.filter(s => !pageIds.includes(s.hold)); + $("input.select_hold", allCheckboxes).prop("checked", false); + $("input.select_hold", allCheckboxes).each(function () { + $(this).parent().parent().removeClass("selected"); + }); + } + + localStorage.selectedHolds = JSON.stringify(selected); + updateMoveButtons(); + updateMSGCounters(); + }); + $(".holds_table .select_hold").on("click", function () { + let selected = JSON.parse(localStorage.selectedHolds || "[]"); + const hold_data = { + hold: $(this).data("id"), + borrowernumber: $(this).data("borrowernumber"), + biblionumber: $(this).data("biblionumber"), + itemnumber: $(this).data("itemnumber"), + waiting: $(this).data("waiting"), + intransit: $(this).data("intransit"), + status: $(this).data("status"), + item_level_hold: $(this).data("item_level_hold"), + hold_group_id: $(this).data("hold-group-id"), + }; + if ($(this).is(":checked")) { + if (!selected.some(s => s.hold === hold_data.hold)) { + selected.push(hold_data); + } + } else { + selected = selected.filter(s => s.hold !== hold_data.hold); + } + localStorage.selectedHolds = JSON.stringify(selected); + updateMoveButtons(); + updateMSGCounters(); + 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-hold." + table_class).on("click", function (e) { + e.preventDefault; + cancel_link = $(this); + $("#cancel_modal_form #inputs").empty(); + let reserve_id = cancel_link.data("id"); + let biblionumber = cancel_link.data("biblionumber"); + _append_patron_page_cancel_hold_modal_data({ + hold: reserve_id, + biblionumber: biblionumber, + borrowernumber: cancel_link.data("borrowernumber"), + }); + $("#cancelModal").modal("show"); + }); + $(".cancel_selected_holds").click(function (e) { + e.preventDefault(); + const selectedHolds = JSON.parse( + localStorage.selectedHolds || "[]" + ); + if (selectedHolds.length) { + $("#cancel_modal_form #inputs").empty(); + + selectedHolds.forEach(function (hold) { + _append_patron_page_cancel_hold_modal_data(hold); + }); + + //delete localStorage.selectedHolds; + $("#cancelModal").modal("show"); + } + return false; + }); + function _append_patron_page_cancel_hold_modal_data(hold) { + $("#cancel_modal_form #inputs").append( + '' + ); + $("#cancel_modal_form #inputs").append( + '' + ); + $("#cancel_modal_form #inputs").append( + '' + ); + $("#cancel_modal_form #inputs").append( + '' + ); + } + // Remove any previously attached handlers + $("#cancelModalConfirmBtn").off("click"); + // Attach the handler to the button + $("#cancelModalConfirmBtn").one("click", function (e) { + e.preventDefault(); + let formInputs = {}; + formInputs["reserve_id"] = $( + "#cancel_modal_form :input[name='reserve_id']" + ) + .map(function () { + return $(this).val(); + }) + .get(); + formInputs["cancellation-reason"] = $( + "#cancel_modal_form :input[name='cancellation-reason']" + ).val(); + cancel_holds( + formInputs["reserve_id"], + formInputs["cancellation-reason"] + ) + .success(function () { + holdsQueueTable.api().ajax.reload(null, false); + }) + .fail(function (jqXHR) { + $("#cancelModal .modal-body").prepend( + '
' + + jqXHR.responseJSON.error + + "
" + ); + $("#cancelModalConfirmBtn").prop("disabled", true); + }) + .done(function () { + $("#cancelModal").modal("hide"); + if ($(".select_hold_all").prop("checked")) { + $(".select_hold_all").click(); + } + }); + }); + function cancel_holds(hold_ids, cancellation_reason) { + return $.ajax({ + method: "DELETE", + url: "/api/v1/holds/cancellation_bulk", + contentType: "application/json", + data: JSON.stringify({ + hold_ids: hold_ids, + cancellation_reason: cancellation_reason, + }), + }); + } + $( + ".holddate." + table_class + ", .expirationdate." + table_class + ).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); + $(instance.input).attr( + "data-current-date", + dateStr + ); + }, + error: function (jqXHR, textStatus, errorThrown) { + holdsQueueTable.api().ajax.reload(null, false); + }, + }); + } + }, + }); + $(".suspenddate." + table_class).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." + table_class).one("click", function (e) { + e.preventDefault(); + const hold_id = $(this).data("id"); + const suspended = $(this).attr("data-suspended"); + const input = $( + `.suspenddate.` + table_class + `[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." + table_class).on("change", function (e) { + e.preventDefault(); + const hold_id = $(this).data("id"); + let priority = e.target.value; + // Replace select with spinner + const $select = $(this); + const $spinner = $( + 'Loading...' + ); + $select.hide().after($spinner); + $.ajax({ + method: "PUT", + url: + "/api/v1/holds/" + + encodeURIComponent(hold_id) + + "/priority", + data: JSON.stringify(priority), + success: function (data) { + holdsQueueTable.api().ajax.reload(null, false); + $spinner.remove(); + }, + error: function (jqXHR, textStatus, errorThrown) { + alert( + "There was an error:" + textStatus + " " + errorThrown + ); + $select.show(); + $spinner.remove(); + }, + }); + }); + $(".move-hold." + table_class).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." + table_class).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." + table_class).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() + ); + }, + }); + }); + $(".change_hold_type." + table_class).on("change", function () { + $(this).prop("disabled", true); + var cur_select = $(this); + var hold_id = $(this).attr("data-id"); + $(this).after( + '
' + ); + let api_url = "/api/v1/holds/" + encodeURIComponent(hold_id); + $.ajax({ + method: "PATCH", + url: api_url, + data: JSON.stringify({ item_id: null, item_level: false }), + headers: { "x-koha-override": "any" }, + contentType: "application/json", + 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_holdno" + hold_id).remove(); + cur_select.val( + cur_select.children('option[selected="selected"]').val() + ); + }, + }); + }); + $(".printholdslip." + table_class).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; + }); + updateMSGCounters(); + }); +} diff --git a/reserve/request.pl b/reserve/request.pl index fbc08a08269..9f0299ff775 100755 --- a/reserve/request.pl +++ b/reserve/request.pl @@ -669,9 +669,13 @@ 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'); + unless ( ( defined $always_show_holds && $always_show_holds eq 'DONT' ) && !$show_holds_now ) { my $holds_count_per_patron = { map { $_->{borrowernumber} => $_->{hold_count} } @{ Koha::Holds->search( @@ -684,64 +688,27 @@ if ( ( $findborrower && $borrowernumber_hold || $findclub && $club_hold ) )->unblessed } }; - my @reserves = Koha::Holds->search( - { 'me.biblionumber' => $biblionumber }, - { prefetch => [ 'borrowernumber', 'itemnumber' ], 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; - $reserve{hold_group_id} = $res->hold_group_id; - - 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... @@ -764,11 +731,14 @@ if ( ( $findborrower && $borrowernumber_hold || $findclub && $club_hold ) C4::Search::enabled_staff_search_views, ); - $biblioloopiter{biblionumber} = $biblionumber; - $biblioloopiter{title} = $biblio->title; - $biblioloopiter{author} = $biblio->author; - $biblioloopiter{rank} = $fixedRank; - $biblioloopiter{reserveloop} = \@reserveloop; + $biblioloopiter{biblionumber} = $biblionumber; + $biblioloopiter{title} = $biblio->title; + $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; # Pass through any reserve charge if ($patron) { @@ -791,6 +761,8 @@ if ( ( $findborrower && $borrowernumber_hold || $findclub && $club_hold ) $template->param( reserveloop => \@reserveloop ); } + $template->param( total_holds => $total_holds ); + if ( $patron && $multi_hold ) { # Add the valid pickup locations -- 2.39.5