Bugzilla – Attachment 191670 Details for
Bug 23269
Long hold queues are slowing the service
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 23269: use REST API for patron_holds_table
0001-Bug-23269-use-REST-API-for-patron_holds_table.patch (text/plain), 124.11 KB, created by
Johanna Räisä
on 2026-01-20 07:21:56 UTC
(
hide
)
Description:
Bug 23269: use REST API for patron_holds_table
Filename:
MIME Type:
Creator:
Johanna Räisä
Created:
2026-01-20 07:21:56 UTC
Size:
124.11 KB
patch
obsolete
>From ffe7451da45a54427a6861245355dae526c4e545 Mon Sep 17 00:00:00 2001 >From: =?UTF-8?q?Johanna=20R=C3=A4is=C3=A4?= <johanna.raisa@gmail.com> >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/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 28041bdfaa..edd60df655 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 ad5b4c4d64..90fdc38f7c 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 8b4eb1f8ef..bede39b765 100644 >--- a/admin/columns_settings.yml >+++ b/admin/columns_settings.yml >@@ -2129,6 +2129,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 9a62c768c4..c1c6363ae4 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 dcd6413c03..71474e1218 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 8e44790a45..a12f41c60f 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 %] > >-<table id="patron_holds_table" class="holds_table"> >+[%#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? %] >+ >+<table id="patron_holds_table_[% biblio_id | html %]_[% split_data | html %]" class="holds_table" data-total-holds="[% total_holds | html %]"> > <thead> > <tr> > <th id="checkbox" data-colname="checkbox"><input type="checkbox" class="select_hold_all" /></th> > <th id="priority" data-colname="priority">Priority</th> >- [% IF ( CAN_user_reserveforothers_modify_holds_priority ) %] >- <th id="change_priority" data-colname="change_priority">Change priority</th> >- [% END %] >+ <th id="change_priority" data-colname="change_priority">Change priority</th> > <th id="patron" data-colname="patron">Patron</th> > <th id="notes" data-colname="notes">Notes</th> > <th id="date" data-colname="date">Date</th> > <th id="expiration" data-colname="expiration">Expiration</th> > <th id="pickup_library" data-colname="pickup_library">Pickup library</th> > <th id="details" data-colname="details">Details</th> >- [% IF ( CAN_user_reserveforothers_modify_holds_priority ) %] >- <th id="set_lowest_priority" data-colname="set_lowest_priority"><span style="display:none;">Set lowest priority</span><i class="fa fa-lg fa-rotate-90 fa-fast-forward" title="Toggle set to lowest priority"></i></th> >- [% END %] >+ <th id="set_lowest_priority" data-colname="set_lowest_priority"><span style="display:none;">Set lowest priority</span><i class="fa fa-lg fa-rotate-90 fa-fast-forward" title="Toggle set to lowest priority"></i></th> > <th id="delete" data-colname="delete">Delete</th> > <th id="suspend" data-colname="suspend">Suspend</th> > <th id="print_slip" data-colname="print_slip">Print hold/transfer slip</th> > </tr> > </thead> >- [%- 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 %] >- >- <tbody> >- [% 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' : '' %] >- <tr class="[% tr_class | html %]"> >- <td >- ><input >- type="checkbox" >- class="select_hold" >- data-id="[% hold.reserve_id | html %]" >- data-item_level_hold="[% hold.item_level_hold | html %]" >- data-itemnumber="[% hold.itemnumber | html %]" >- data-biblionumber="[% hold.biblionumber | html %]" >- data-waiting="[% hold.atdestination | html %]" >- data-intransit="[% hold.intransit | html %]" >- /></td> >- <td style="min-width:185px;" data-order="[% priority | html %]"> >- <div> >- <input type="hidden" name="reserve_id" value="[% hold.reserve_id | html %]" /> >- <input type="hidden" name="borrowernumber" value="[% hold.borrowernumber | html %]" /> >- <input type="hidden" name="biblionumber" value="[% hold.biblionumber | html %]" /> >- [% IF ( CAN_user_reserveforothers_modify_holds_priority ) %] >- [% IF Koha.Preference('HoldsSplitQueue') == "nothing" && !hold.found %] >- <label for="rank-request_[% hold.reserve_id | html %]">Change priority to:</label> >- <select name="rank-request" id="rank-request_[% hold.reserve_id | html %]" class="rank-request" data-hold-id="[% hold.reserve_id | html %]"> >- [% IF ( CAN_user_reserveforothers_modify_holds_priority ) %] >- [% FOREACH priority IN all_priorities %] >- [% IF priority == this_priority %] >- <option value="[% priority | html %]" selected="selected">[% priority | html %]</option> >- [% ELSIF priority > 0 %] >- <option value="[% priority | html %]">[% priority | html %]</option> >- [% END %] >- [% END %] >- [% ELSE %] >- <option value="[% hold.priority | html %]" selected="selected">[% this_priority | html %]</option> >- [% END %] >- <option value="del">del</option> >- </select> >- [% ELSE %] >- <input type="hidden" name="rank-request" class="rank-request" value="[% hold.priority | html %]" data-hold-id="[% hold.reserve_id | html %]" /> >- <select name="rank-request" class="rank-request" disabled="disabled" data-hold-id="[% hold.reserve_id | html %]"> >- [% IF ( hold.found ) %] >- [% IF ( hold.intransit ) %] >- <option value="T" selected="selected">In transit</option> >- [% ELSIF (hold.inprocessing) %] >- <option value="P" selected="selected">In processing</option> >- [% ELSE %] >- <option value="W" selected="selected">Waiting</option> >- [% END %] >- [% ELSE %] >- <option value="[% hold.priority | html %]" selected="selected">[% this_priority | html %]</option> >- [% END %] >- </select> >- [% END %] >- [% ELSE %] >- <input type="hidden" name="rank-request" class="rank-request" value="[% hold.priority | html %]" data-hold-id="[% hold.reserve_id | html %]" /> >- [% END %] >- <div> >- <label for="current_priority_[% hold.reserve_id | html %]">Current priority: </label> >- <span id="current_priority_[% hold.reserve_id | html %]" class="current_priority badge text-bg-info">[% hold.priority | html %]</span> >- </div> >- </div> >- </td> >- [%- IF ( CAN_user_reserveforothers_modify_holds_priority ) -%] >- [%- UNLESS hold.found -%] >- [%- SET prev_priority = loop.prev.priority -%] >- [%- SET next_priority = loop.next.priority -%] >- <td style="white-space:nowrap;"> >- <a >- class="hold-arrow" >- title="Move hold up" >- data-op="cud-move" >- data-where="up" >- data-first_priority="[% first_priority | html %]" >- data-last_priority="[% last_priority | html %]" >- data-prev_priority="[% prev_priority | html %]" >- data-next_priority="[% next_priority | html %]" >- data-borrowernumber="[% hold.borrowernumber | html %]" >- data-biblionumber="[% hold.biblionumber | html %]" >- data-reserve_id="[% hold.reserve_id | html %]" >- data-date="[% hold.date | html %]" >- > >- <i class="fa fa-lg icon-move-hold-up" aria-hidden="true"></i> >- </a> >- >- <a >- class="hold-arrow" >- title="Move hold to top" >- data-op="cud-move" >- data-where="top" >- data-first_priority="[% first_priority | html %]" >- data-last_priority="[% last_priority | html %]" >- data-prev_priority="[% prev_priority | html %]" >- data-next_priority="[% next_priority | html %]" >- data-borrowernumber="[% hold.borrowernumber | html %]" >- data-biblionumber="[% hold.biblionumber | html %]" >- data-reserve_id="[% hold.reserve_id | html %]" >- data-date="[% hold.date | html %]" >- > >- <i class="fa fa-lg icon-move-hold-top" aria-hidden="true"></i> >- </a> >- >- <a >- class="hold-arrow" >- title="Move hold to bottom" >- data-op="cud-move" >- data-where="bottom" >- data-first_priority="[% first_priority | html %]" >- data-last_priority="[% last_priority | html %]" >- data-prev_priority="[% prev_priority | html %]" >- data-next_priority="[% next_priority | html %]" >- data-borrowernumber="[% hold.borrowernumber | html %]" >- data-biblionumber="[% hold.biblionumber | html %]" >- data-reserve_id="[% hold.reserve_id | html %]" >- data-date="[% hold.date | html %]" >- > >- <i class="fa fa-lg icon-move-hold-bottom" aria-hidden="true"></i> >- </a> >- >- <a >- class="hold-arrow" >- title="Move hold down" >- data-op="cud-move" >- data-where="down" >- data-first_priority="[% first_priority | html %]" >- data-last_priority="[% last_priority | html %]" >- data-prev_priority="[% prev_priority | html %]" >- data-next_priority="[% next_priority | html %]" >- data-borrowernumber="[% hold.borrowernumber | html %]" >- data-biblionumber="[% hold.biblionumber | html %]" >- data-reserve_id="[% hold.reserve_id | html %]" >- data-date="[% hold.date | html %]" >- > >- <i class="fa fa-lg icon-move-hold-down" aria-hidden="true"></i> >- </a> >- </td> >- [%- ELSE -%] >- <td></td> >- [%- END -%] >- [%- END -%] >- <td> [% INCLUDE 'patron-title.inc' patron=hold.patron hide_patron_infos_if_needed=1 %] </td> >- <td>[% hold.notes | html | html_line_break %]</td> >- <td data-order="[% hold.date| html %]"> >- [% IF Koha.Preference('AllowHoldDateInFuture') %] >- <input type="text" class="flatpickr" value="[% hold.date | html %]" required="required" size="10" name="reservedate" /> >- [% ELSE %] >- [% hold.date | $KohaDates %] >- [% END %] >- </td> >- <td> >- <input type="text" class="flatpickr" data-flatpickr-futuredate="true" value="[% hold.expirationdate | html %]" size="10" name="expirationdate" /> >- </td> >- <td> >- [%- IF ( hold.found ) -%] >- <input type="hidden" name="pickup" value="[% hold.wbrcode | html %]" /> >- [%- IF ( hold.atdestination ) -%] >- Item waiting at <strong> [% hold.wbrname | html %]</strong>[% IF hold.desk_name %], [% hold.desk_name | html %],[% END %] since [% hold.waiting_date | $KohaDates %] >- [%- ELSIF (hold.intransit) -%] >- Item being transferred to <strong> [% hold.wbrname | html %]</strong> >- [%- ELSIF (hold.inprocessing) -%] >- Item being processed at <strong> [% hold.wbrname | html %]</strong> >- [%- ELSE -%] >- Hold expected at <strong>[% hold.wbrname | html %]</strong>, please checkin to verify status >- [%- END -%] >- [%- ELSE -%] >- [%- IF Koha.Preference('IndependentBranches') && Branches.all().size == 1 -%] >- [% Branches.GetName(hold.branchcode) | html %] <input type="hidden" name="pickup" value="[% hold.branchcode | html %]" /> >- [%- ELSE -%] >- <select class="pickup_location_dropdown" data-selected="[% hold.branchcode | html %]" data-hold-id="[% hold.reserve_id | html %]" data-pickup-location-source="hold" name="pickup"> >- <option selected="selected" value="[% hold.branchcode | html %]">[% Branches.GetName(hold.branchcode) | html %]</option> >- <option value="" disabled="disabled" class="loading">Loading...</option> >- </select> >- <img class="loading_[% hold.reserve_id | html %]" src="[% interface | html %]/[% theme | html %]/img/spinner-small.gif" alt="" style="display:none;" /> >- [%- END -%] >- [%- END -%] >- </td> >- <td> >- [%- IF ( hold.found ) -%] >- <a href="/cgi-bin/koha/catalogue/moredetail.pl?biblionumber=[% hold.biblionumber | uri %]&itemnumber=[% hold.itemnumber | uri %]#item[% hold.itemnumber | uri %]"> >- [%- IF ( hold.barcodenumber ) -%] >- [%- hold.barcodenumber | html -%] >- [%- ELSE -%] >- <span>No barcode</span> >- [%- END -%] >- <input type="hidden" name="itemnumber" value="[% hold.itemnumber | html %]" /> >- </a> >- [%- ELSE -%] >- [%- IF ( hold.item_level_hold ) -%] >- <em> >- [%- IF ! hold.change_hold_type_allowed -%] >- <span>Only item</span> >- <a href="/cgi-bin/koha/catalogue/moredetail.pl?biblionumber=[% hold.biblionumber | uri %]&itemnumber=[% hold.itemnumber | uri %]#item[% hold.itemnumber | uri %]"> >- [%- IF ( hold.barcodenumber ) -%] >- [%- hold.barcodenumber | html -%] >- [%- ELSE -%] >- <span>No barcode</span> >- [%- END -%] >- <input type="hidden" name="itemnumber" value="[% hold.itemnumber | html %]" /> >- </a> >- [%- ELSE -%] >- <select name="change_hold_type_[% hold.reserve_id | html %]"> >- <option selected="selected" value=""> >- [%- IF ( hold.barcodenumber ) -%] >- [% tx("Only item {barcode}", {barcode = hold.barcodenumber}) | html %] >- [%- ELSE -%] >- [% t("Only item [No barcode]") | html %] >- [%- END -%] >- </option> >- <option value="1">Next available</option> >- </select> >- [%- IF ( hold.itemnumber ) -%] >- <input type="hidden" name="itemnumber" value="[% hold.itemnumber | html %]" /> >- [%- END -%] >- [%- IF hold.itemtype -%] >- <span style="display:none">Next available [% ItemTypes.GetDescription( hold.itemtype ) | html %] item</span> >- [%- ELSE -%] >- <span style="display:none">Next available</span> >- [%- END -%] >- [%- END -%] >- </em> >- [%- ELSE -%] >- [%- IF hold.itemtype -%] >- <em>Next available [% ItemTypes.GetDescription( hold.itemtype ) | html %] item</em> >- [%- ELSIF hold.object.item_group -%] >- <em>Next available item from group <strong>[% hold.object.item_group.description | html %]</strong></em> >- [%- ELSE -%] >- <em>Next available</em> >- [%- END -%] >- >- <input type="hidden" name="itemnumber" value="" /> >- [%- END -%] >- [%- END -%] >- [%- IF hold.non_priority -%] >- <br /><i>Non priority hold</i> >- [%- END -%] >- [%- IF hold.hold_group_id -%] >- <div>(part of a <a href="/cgi-bin/koha/reserve/hold-group.pl?hold_group_id=[% hold.hold_group_id | uri %]" class="hold-group">hold group</a>)</div> >- [%- END -%] >- </td> >- [%- IF ( CAN_user_reserveforothers_modify_holds_priority ) -%] >- [%- UNLESS hold.found -%] >- <td> >- [% IF ( hold.lowestPriority ) %] >- <a >- class="hold-arrow" >- title="Remove lowest priority" >- data-op="cud-setLowestPriority" >- data-borrowernumber="[% hold.borrowernumber | html %]" >- data-biblionumber="[% hold.biblionumber | html %]" >- data-reserve_id="[% hold.reserve_id | html %]" >- data-date="[% hold.date | html %]" >- > >- <i class="fa fa-lg fa-rotate-90 icon-unset-lowest" aria-hidden="true"></i> >- </a> >- [% ELSE %] >- <a >- class="hold-arrow" >- title="Set lowest priority" >- data-op="cud-setLowestPriority" >- data-borrowernumber="[% hold.borrowernumber | html %]" >- data-biblionumber="[% hold.biblionumber | html %]" >- data-reserve_id="[% hold.reserve_id | html %]" >- data-date="[% hold.date | html %]" >- > >- <i class="fa fa-lg fa-rotate-90 icon-set-lowest" aria-hidden="true"></i> >- </a> >- [% END %] >- </td> >- [%- ELSE -%] >- <td></td> >- [%- END -%] >- [%- END -%] >- <td> >- <a class="cancel-hold deny" title="Cancel hold" data-borrowernumber="[% hold.borrowernumber | html %]" data-biblionumber="[% hold.biblionumber | html %]" data-id="[% hold.reserve_id | html %]" href="#"> >- <i class="fa fa-trash" aria-label="Cancel hold"></i> >- </a> >- </td> >- <td> >- [% IF Koha.Preference('SuspendHoldsIntranet') %] >- [% UNLESS ( hold.found ) %] >- [% IF ( hold.suspend ) %] >- <button class="btn btn-default btn-xs unsuspend-hold" data-reserve-id="[% hold.reserve_id | html %]" data-biblionumber="[% hold.biblionumber | html %]"> >- <i class="fa fa-play" aria-hidden="true"></i> Unsuspend >- </button> >- [% ELSE %] >- <button class="btn btn-default btn-xs suspend-hold" data-reserve-id="[% hold.reserve_id | html %]" data-biblionumber="[% hold.biblionumber | html %]"> >- <i class="fa fa-pause" aria-hidden="true"></i> Suspend >- </button> >- [% END %] >- >- [% IF Koha.Preference('AutoResumeSuspendedHolds') %] >- <label for="suspend_until_[% hold.reserve_id | html %]">[% IF ( hold.suspend ) %]on[% ELSE %]until[% END %]</label> >- <input >- type="text" >- name="suspend_until_[% hold.reserve_id | html %]" >- id="suspend_until_[% hold.reserve_id | html %]" >- size="10" >- value="[% hold.suspend_until | html %]" >- class="flatpickr" >- data-flatpickr-futuredate="true" >- /> >- [%- ELSE -%] >- <input type="hidden" name="suspend_until_[% hold.reserve_id | html %]" id="suspend_until_[% hold.reserve_id | html %]" value="" /> >- [%- END -%] >- [%- END -%] >- [%- END # IF SuspendHoldsIntranet -%] >- [%- IF ( hold.found ) -%] >- <a >- class="btn btn-default submit-form-link" >- href="#" >- id="revert_hold_[% hold.reserve_id | html %]" >- data-op="cud-move" >- data-where="down" >- data-first_priority="[% first_priority | html %]" >- data-last_priority="[% last_priority | html %]" >- data-prev_priority="0" >- data-next_priority="1" >- data-borrowernumber="[% hold.borrowernumber | html %]" >- data-biblionumber="[% hold.biblionumber | html %]" >- data-itemnumber="[% hold.itemnumber | html %]" >- data-reserve_id="[% hold.reserve_id | html %]" >- data-date="[% hold.date | html %]" >- data-action="request.pl" >- data-method="post" >- >[% IF hold.intransit %]Revert transit status[% ELSE %]Revert waiting status[% END %]</a >- > >- [%- END -%] >- </td> >- [% IF ( hold.intransit || hold.atdestination ) %] >- <td><input class="printholdslip" type="button" name="printholdslip" value="Print slip" data-reserve_id="[% hold.reserve_id | html %]" /></td> >- [% ELSE %] >- <td></td> >- [% END %] >- </tr> >- [% END %] >- </tbody> > </table> >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 e5877e7089..970706d1a5 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt >@@ -1276,7 +1276,7 @@ > [% END %] > [% END %] > [% END %] >- [% IF ( reserveloop ) %] >+ [% IF ( total_holds ) %] > <form id="existing_holds" name="T[% time | html %]" action="modrequest.pl" method="post" style="display:block"> > [% INCLUDE 'csrf-token.inc' %] > [% IF ( multi_hold ) %] >@@ -1295,11 +1295,7 @@ > <h2>Existing holds</h2> > <div id="toolbar" class="btn-toolbar sticky"> > <div class="btn-group"> >- <input type="hidden" name="op" value="cud-modifyall" /> >- <input type="submit" class="btn btn-primary" name="submit" value="Update hold(s)" /> >- </div> >- <div class="btn-group"> >- <button class="btn cancel_selected_holds" data-bulk="true"> Cancel selected (<span class="selected_holds_count"></span>)</button> >+ <button class="btn cancel_selected_holds" data-bulk="true"></button> > </div> > <div id="cancellation-reason-div" style="display:none"> > [% SET hold_cancellation = AuthorisedValues.GetAuthValueDropbox('HOLD_CANCELLATION') %] >@@ -1325,137 +1321,75 @@ > </div> > <div class="page-section"> > [% FOREACH biblioloo IN biblioloop %] >- [% IF ( biblioloo.reserveloop ) %] >+ [% IF ( biblioloo.total_holds ) %] > <div class="hold_title" id="hold_title_[% biblioloo.biblionumber | html %]"> > [% IF ( multi_hold ) %] > <h3> > <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% biblioloo.biblionumber | uri %]"> [% biblioloo.title | html %] </a> >- <span class="badge bg-info-subtle"><span>[% biblioloo.reserveloop.size | html %] [% tn('Hold', 'Holds', biblioloo.reserveloop.size) | $raw %]</span></span> >+ <span class="badge bg-info-subtle"><span>[% biblioloo.total_holds | html %] [% tn('Hold', 'Holds', biblioloo.total_holds) | $raw %]</span></span> > </h3> > [% END %] > > [% IF Koha.Preference('HoldsSplitQueue') == 'branch' %] >- >- [% SET branchcodes = [] %] >- >- [% FOREACH h IN biblioloo.reserveloop %] >- [% branchcodes.push( h.branchcode ) %] >- [% END %] >- [% branchcodes = branchcodes.unique %] >- [% IF ( branchcodes.empty ) %] >- <div class="alert alert-info"> There are no holds on this title. </div> >+ [% IF biblioloo.hold_branches.empty %] >+ <div class="holds_by_library"> >+ <h4>Any library</h4> >+ [% INCLUDE holds_table.inc biblio_id=biblioloo.biblionumber total_holds=biblioloo.total_holds split_data="any" %] >+ </div> > [% 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 %] > <div class="holds_by_library"> > <h4>[% Branches.GetName( b ) | html %]</h4> > >- [% INCLUDE holds_table.inc holds=holds_by_branch %] >+ [% INCLUDE holds_table.inc biblio_id=biblioloo.biblionumber total_holds=biblioloo.total_holds split_data=b %] > </div> >- [% 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 ) %] >- <div class="alert alert-info"> There are no holds on this title. </div> >+ [% IF biblioloo.hold_itemtypes.empty %] >+ <div class="holds_by_itemtype"> >+ <h4>Any item type</h4> >+ [% INCLUDE holds_table.inc biblio_id=biblioloo.biblionumber total_holds=biblioloo.total_holds split_data="any" %] >+ </div> > [% 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 %] > <div class="holds_by_itemtype"> >- [% IF i %] >+ [% IF i && i != 'any' %] > <h4>[% ItemTypes.GetDescription( i ) | html %]</h4> > [% ELSE %] > <h4>Any item type</h4> > [% 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 %] > </div> >- [% 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 ) %] >- <div class="alert alert-info"> There are no holds on this title. </div> >- [% ELSE %] >- >- [% FOREACH b IN branchcodes.sort %] >- <div class="holds_by_library"> >- <h4 class="library_holds">[% Branches.GetName( b ) | html %]</h4> >- [% SET holds_by_branch = [] %] >- [% FOREACH h IN biblioloo.reserveloop %] >- [% IF h.branchcode == b %] >- [% holds_by_branch.push( h ) %] >+ [% FOREACH b IN biblioloo.hold_branches %] >+ <div class="holds_by_library"> >+ <h4>[% Branches.GetName( b ) | html %]</h4> >+ [% FOREACH i IN biblioloo.hold_itemtypes %] >+ <div class="holds_by_itemtype"> >+ [% IF i && i != 'any' %] >+ <h4>[% ItemTypes.GetDescription( i ) | html %]</h4> >+ [% ELSE %] >+ <h4>Any item type</h4> > [% 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 %] >- <div class="holds_by_itemtype"> >- <h5 class="itemtype_holds"> >- [% IF i %] >- [% ItemTypes.GetDescription( i ) | html %] >- [% ELSE %] >- <span>Any item type</span> >- [% END %] >- </h5> >- >- [% 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 %] >- </div> >- <!-- /.holds_by_itemtype --> >- [% END %] >- </div> >- <!-- /.holds_by_library --> >- [% END # /FOREACH b %] >- [% END # /IF ( branchcodes.empty ) %] >- [% ELSE %] >- >- [% IF ( biblioloo.reserveloop.size ) %] >- [% INCLUDE holds_table.inc holds=biblioloo.reserveloop %] >- [% ELSE %] >- <div class="alert alert-info"> There are no holds on this title. </div> >+ [% SET split_data = b _ "_" _ i %] >+ [% INCLUDE holds_table.inc biblio_id=biblioloo.biblionumber total_holds=biblioloo.total_holds split_data=split_data %] >+ </div> >+ [% END %] >+ </div> > [% END %] >+ [% ELSE %] >+ [% INCLUDE holds_table.inc biblio_id=biblioloo.biblionumber total_holds=biblioloo.total_holds split_data="any" %] > [% END # /IF HoldsSplitQueue %] > </div> > <!-- /hold_title --> >- [% END # /IF biblioloo.reserveloop %] >+ [% ELSE %] >+ <div class="note"> There are no holds on this title. </div> >+ [% END # /IF ( biblioloo.total_holds ) %] > [% END # FOREACH biblioloo %] > </div> > </form> >@@ -1570,6 +1504,9 @@ > [% INCLUDE 'datatables.inc' %] > [% INCLUDE 'calendar.inc' %] > [% INCLUDE 'select2.inc' %] >+ <script> >+ hold_table_settings = [% TablesSettings.GetTableSettings( 'circ', 'holds', 'patron_holds_table', 'json' ) | $raw %]; >+ </script> > [% Asset.js("js/holds.js") | $raw %] > [% Asset.js("js/hold-group.js") | $raw %] > >@@ -1578,17 +1515,16 @@ > [% SET url_biblio_params = url_biblio_params _ "&multi_hold=1" %] > [% END %] > <script> >- $(document).ready(function () { >- hold_table_settings = [% TablesSettings.GetTableSettings( 'circ', 'holds', 'patron_holds_table', 'json' ) | $raw %]; >- $("#patron_holds_table").kohaTable( >- { >- paging: false, >- bKohaColumnsUseNames: true, >- }, >- hold_table_settings >- ); >- }); > var biblionumbers = [[% biblionumbers.join(', ') | $raw %]]; >+ var hold_branches = {[% FOREACH biblio_info IN biblioloop %] [% biblio_info.biblionumber | html %]: ["[% biblio_info.hold_branches.join('", "') | $raw %]"], [% END %]}; >+ var hold_itemtypes = {[% FOREACH biblio_info IN biblioloop %] [% biblio_info.biblionumber | html %]: ["[% biblio_info.hold_itemtypes.join('", "') | $raw %]"], [% END %]}; >+ var HoldsSplitQueue = "[% Koha.Preference('HoldsSplitQueue') | html %]"; >+ var HoldsSplitQueueNumbering = "[% Koha.Preference('HoldsSplitQueueNumbering') | html %]"; >+ var SuspendHoldsIntranet = [% ( Koha.Preference('SuspendHoldsIntranet') ) ? 1 : 0 | html %]; >+ var AutoResumeSuspendedHolds = [% ( Koha.Preference('AutoResumeSuspendedHolds') ) ? 1 : 0 | html %]; >+ var AllowHoldDateInFuture = [% ( Koha.Preference('AllowHoldDateInFuture') ) ? 1 : 0 | html %]; >+ var HidePatronName = [% ( Koha.Preference('HidePatronName') ) ? 1 : 0 | html %]; >+ var CAN_user_reserveforothers_modify_holds_priority = [%CAN_user_reserveforothers_modify_holds_priority ? 1 : 0 | html %]; > var borrowernumber = "[% patron.borrowernumber | $raw %]"; > var patron_homebranch = "[% To.json( Branches.GetName( patron.branchcode ) ) | $raw %]"; > var override_items = {[% FOREACH biblio_info IN biblioloop %][% FOREACH itemloo IN biblio_info.itemloop %][% IF ( itemloo.override ) %] >@@ -1616,6 +1552,31 @@ > $.fn.select2.defaults.set("dropdownAutoWidth", true ); > > $(document).ready(function() { >+ biblionumbers.forEach(function(biblionumber) { >+ let branches = hold_branches[biblionumber]; >+ let itemtypes = hold_itemtypes[biblionumber]; >+ switch (HoldsSplitQueue) { >+ case 'branch': >+ branches.forEach(function(branch) { >+ load_patron_holds_table(biblionumber, {name: HoldsSplitQueue, value: branch}); >+ }); >+ break; >+ case 'itemtype': >+ itemtypes.forEach(function(itemtype) { >+ load_patron_holds_table(biblionumber, {name: HoldsSplitQueue, value: itemtype}); >+ }); >+ break; >+ case 'branch_itemtype': >+ branches.forEach(function(branch) { >+ itemtypes.forEach(function(itemtype) { >+ load_patron_holds_table(biblionumber, {name: HoldsSplitQueue, value: branch + '_' + itemtype}); >+ }); >+ }); >+ break; >+ default: >+ load_patron_holds_table(biblionumber, {name: 'any', value: 'any'}); >+ } >+ }); > $('#cancellation-reason-div').hide(); > $('.rank-request').on('change', function() { > if ( $(".rank-request option:selected[value='del']").length ) { >@@ -1976,66 +1937,10 @@ > }); > [% END %] > >- $(".hold-arrow").click(function(e) { >- e.preventDefault(); >- let arrowForm = $("#hold-actions-form").attr({ >- action: 'request.pl', >- method: 'post' >- }); >- let arrow_op = $("<input />").attr({ >- name: 'op', >- type: 'hidden', >- value: $(this).data('op') >- }); >- let arrow_where = $("<input />").attr({ >- name: 'where', >- type: 'hidden', >- value: $(this).data('where') >- }); >- let arrow_fp = $("<input />").attr({ >- name: 'first_priority', >- type: 'hidden', >- value: $(this).data('first_priority') >- }); >- let arrow_lp = $("<input />").attr({ >- name: 'last_priority', >- type: 'hidden', >- value: $(this).data('last_priority') >- }); >- let arrow_pp = $("<input />").attr({ >- name: 'prev_priority', >- type: 'hidden', >- value: $(this).data('prev_priority') >- }); >- let arrow_np = $("<input />").attr({ >- name: 'next_priority', >- type: 'hidden', >- value: $(this).data('next_priority') >- }); >- let arrow_bn = $("<input />").attr({ >- name: 'borrowernumber', >- type: 'hidden', >- value: $(this).data('borrowernumber') >- }); >- let arrow_bb = $("<input />").attr({ >- name: 'biblionumber', >- type: 'hidden', >- value: $(this).data('biblionumber') >- }); >- let arrow_ri = $("<input />").attr({ >- name: 'reserve_id', >- type: 'hidden', >- value: $(this).data('reserve_id') >- }); >- let arrow_date = $("<input />").attr({ >- name: 'date', >- type: 'hidden', >- value: $(this).data('date') >- }); >- arrowForm.append(arrow_op,arrow_where,arrow_fp,arrow_lp,arrow_pp,arrow_np,arrow_bn,arrow_bb,arrow_ri,arrow_date); >- $("#hold-actions-form").submit(); >- return true; >- }) >+ if(!localStorage.selectedHolds || document.referrer.replace(/\?.*/, '') !== document.location.origin+document.location.pathname) { >+ localStorage.selectedHolds = []; >+ } >+ > }); > </script> > <script> >@@ -2051,13 +1956,6 @@ > }); > </script> > [% END %] >- <script> >- $(".printholdslip").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; >- }); >- </script> > [% 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 6ebe226e18..b124ef1595 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/datatables.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/datatables.js >@@ -1455,6 +1455,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 5e6537fa72..e89bdc8d4d 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( >- '<input type="hidden" name="biblionumber" value="' + >- biblionumber + >- '">' >- ); >- }); >- $("#cancel_modal_form #inputs").append( >- '<input type="hidden" name="op" value="cud-cancel_bulk">' >- ); >- 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( >- '<input type="hidden" name="ids" value="' + hold_ids + '">' >- ); >- } 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( > '<input type="hidden" name="op" value="cud-cancelall">' > ); >@@ -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 ( >+ '<input type="checkbox" class="select_hold ' + >+ table_class + >+ '" data-id="' + >+ data + >+ '" data-borrowernumber="' + >+ row.patron_id + >+ '" data-biblionumber="' + >+ biblio_id + >+ '" data-itemnumber="' + >+ row.item_id + >+ '" data-hold-group-id="' + >+ row.hold_group_id + >+ '" data-item_level_hold="' + >+ row.item_level_holds + >+ '" data-waiting="' + >+ (row.status === "W" ? "1" : "") + >+ '" data-intransit="' + >+ (row.status === "T" ? "1" : "") + >+ '" data-status="' + >+ (row.status ? row.status : "") + >+ '"/>' >+ ); >+ }, >+ }, >+ { >+ data: "priority", >+ orderable: true, >+ searchable: false, >+ render: function (data, type, row, meta) { >+ let select = >+ '<select name="rank-request" class="rank-request ' + >+ table_class + >+ '" data-id="' + >+ row.hold_id; >+ if ( >+ CAN_user_reserveforothers_modify_holds_priority && >+ split_name == "any" >+ ) { >+ for (var i = 0; i < totalHoldsSelect; i++) { >+ let selected; >+ let value; >+ let desc; >+ if (data == i && row.status == "T") { >+ select += '" disabled="disabled">'; >+ selected = " selected='selected' "; >+ value = "T"; >+ desc = "In transit"; >+ } else if (data == i && row.status == "P") { >+ select += '" disabled="disabled">'; >+ selected = " selected='selected' "; >+ value = "P"; >+ desc = "In processing"; >+ } else if (data == i && row.status == "W") { >+ select += '" disabled="disabled">'; >+ selected = " selected='selected' "; >+ value = "W"; >+ desc = "Waiting"; >+ } else if (data == i && !row.status) { >+ select += '">'; >+ selected = " selected='selected' "; >+ value = data; >+ desc = data; >+ } else { >+ if (i != 0) { >+ select += '">'; >+ value = i; >+ desc = i; >+ } else { >+ select += '">'; >+ } >+ } >+ if (value) { >+ select += >+ '<option value="' + >+ value + >+ '"' + >+ selected + >+ ">" + >+ desc + >+ "</option>"; >+ } >+ } >+ } else { >+ if (row.status == "T") { >+ select += >+ '" disabled="disabled"><option value="T" selected="selected">In transit</option></select>'; >+ } else if (row.status == "P") { >+ select += >+ '" disabled="disabled"><option value="P" selected="selected">In processing</option></select>'; >+ } else if (row.status == "W") { >+ select += >+ '" disabled="disabled"><option value="W" selected="selected">Waiting</option></select>'; >+ } else { >+ if ( >+ HoldsSplitQueue !== "nothing" && >+ HoldsSplitQueueNumbering === "virtual" >+ ) { >+ let virtualPriority = >+ meta.settings._iDisplayStart + >+ meta.row + >+ 1; >+ select += >+ '" disabled="disabled"><option value="' + >+ data + >+ '" selected="selected">' + >+ virtualPriority + >+ "</option></select>"; >+ } else { >+ select += >+ '" disabled="disabled"><option value="' + >+ data + >+ '" selected="selected">' + >+ data + >+ "</option></select>"; >+ } >+ } >+ } >+ select += "</select>"; >+ return select; >+ }, >+ }, >+ { >+ data: "", >+ orderable: false, >+ searchable: false, >+ render: function (data, type, row, meta) { >+ if (row.status) { >+ return null; >+ } >+ let buttons = >+ '<a class="hold-arrow move-hold ' + >+ table_class + >+ '" title="Move hold up" href="#" data-move-hold="up" data-priority="' + >+ row.priority + >+ '" reserve_id="' + >+ row.hold_id + >+ '"><i class="fa fa-lg icon-move-hold-up" aria-hidden="true"></i></a>'; >+ buttons += >+ '<a class="hold-arrow move-hold ' + >+ table_class + >+ '" title="Move hold to top" href="#" data-move-hold="top" data-priority="' + >+ row.priority + >+ '" reserve_id="' + >+ row.hold_id + >+ '"><i class="fa fa-lg icon-move-hold-top" aria-hidden="true"></i></a>'; >+ buttons += >+ '<a class="hold-arrow move-hold ' + >+ table_class + >+ '" title="Move hold to bottom" href="#" data-move-hold="bottom" data-priority="' + >+ row.priority + >+ '" reserve_id="' + >+ row.hold_id + >+ '"><i class="fa fa-lg icon-move-hold-bottom" aria-hidden="true"></i></a>'; >+ buttons += >+ '<a class="hold-arrow move-hold ' + >+ table_class + >+ '" title="Move hold down" href="#" data-move-hold="down" data-priority="' + >+ row.priority + >+ '" reserve_id="' + >+ row.hold_id + >+ '"><i class="fa fa-lg icon-move-hold-down" aria-hidden="true"></i></a>'; >+ 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 ( >+ '<a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=' + >+ row.patron.patron_id + >+ '">' + >+ data + >+ "</a>" >+ ); >+ } >+ }, >+ }, >+ { >+ 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 ( >+ '<input type="text" class="holddate ' + >+ table_class + >+ '" value="' + >+ $date(data, { dateformat: "rfc3339" }) + >+ '" size="10" name="hold_date" data-id="' + >+ row.hold_id + >+ '" data-current-date="' + >+ data + >+ '"/>' >+ ); >+ } else { >+ return $date(data); >+ } >+ }, >+ }, >+ { >+ data: "expiration_date", >+ orderable: true, >+ searchable: false, >+ render: function (data, type, row, meta) { >+ return ( >+ '<input type="text" class="expirationdate ' + >+ table_class + >+ '" value="' + >+ $date(data, { dateformat: "rfc3339" }) + >+ '" size="10" name="expiration_date" data-id="' + >+ row.hold_id + >+ '" data-current-date="' + >+ data + >+ '"/>' >+ ); >+ }, >+ }, >+ { >+ data: "pickup_library_id", >+ orderable: true, >+ searchable: false, >+ render: function (data, type, row, meta) { >+ var branchSelect = >+ "<select priority=" + >+ row.priority + >+ ' class="hold_location_select ' + >+ table_class + >+ '" data-id="' + >+ row.hold_id + >+ '" reserve_id="' + >+ row.hold_id + >+ '" name="pick-location" data-pickup-location-source="hold">'; >+ var libraryname; >+ for (var i = 0; i < libraries.length; i++) { >+ var selectedbranch; >+ var setbranch; >+ if (libraries[i]._id == data) { >+ selectedbranch = " selected='selected' "; >+ setbranch = __(" (current) "); >+ libraryname = libraries[i]._str; >+ } else if (libraries[i].pickup_location == false) { >+ continue; >+ } else { >+ selectedbranch = ""; >+ setbranch = ""; >+ } >+ branchSelect += >+ '<option value="' + >+ libraries[i]._id.escapeHtml() + >+ '"' + >+ selectedbranch + >+ ">" + >+ libraries[i]._str.escapeHtml() + >+ setbranch + >+ "</option>"; >+ } >+ branchSelect += "</select>"; >+ if (row.status == "T") { >+ return __( >+ "Item being transferred to <strong>%s</strong>" >+ ).format(libraryname); >+ } else if (row.status == "P") { >+ return __( >+ "Item being processed at <strong>%s</strong>" >+ ).format(libraryname); >+ } else if (row.status == "W") { >+ return __( >+ "Item waiting at <strong>%s</strong> 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 ( >+ '<a href="/cgi-bin/koha/catalogue/moredetail.pl?biblionumber=' + >+ row.biblio_id + >+ "&itemnumber=" + >+ row.item_id + >+ '">' + >+ row.item.external_id + >+ "</a>" >+ ); >+ } 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 = >+ '<a href="/cgi-bin/koha/catalogue/moredetail.pl?biblionumber=' + >+ row.biblio_id + >+ "&itemnumber=" + >+ row.item_id + >+ '">' + >+ (row.item.external_id >+ ? row.item.external_id.escapeHtml() >+ : __("No barcode")) + >+ "</a>"; >+ return __("Only item") + " " + link; >+ } else { >+ let select = >+ '<select id="change_hold_type" class="change_hold_type ' + >+ table_class + >+ '" data-id="' + >+ row.hold_id + >+ '">'; >+ select += >+ '<option value="" selected>' + >+ __("Only item") + >+ " " + >+ barcode + >+ "</option>"; >+ select += >+ '<option value="">' + >+ __("Next available") + >+ "</option>"; >+ select += "</select>"; >+ return select; >+ } >+ } else if (row.item_group_id) { >+ return __( >+ "Next available item from group <strong>%s</strong>" >+ ).format(row.item_group.description); >+ } else if (row.hold_group_id) { >+ return ( >+ "<div>(" + >+ __("part of") + >+ ' <a href="/cgi-bin/koha/reserve/hold-group.pl?hold_group_id=' + >+ row.hold_group_id + >+ '" class="hold-group">' + >+ __("hold group") + >+ "</a>)</div>" >+ ); >+ } else { >+ if (row.non_priority) { >+ return ( >+ "<em>" + >+ __("Next available") + >+ "</em><br/><i>" + >+ __("Non priority hold") + >+ "</i>" >+ ); >+ } else { >+ return "<em>" + __("Next available") + "</em>"; >+ } >+ } >+ }, >+ }, >+ { >+ data: "", >+ orderable: false, >+ searchable: false, >+ render: function (data, type, row, meta) { >+ if (row.item_id) { >+ return null; >+ } else { >+ if (row.lowest_priority) { >+ return ( >+ '<a class="hold-arrow toggle-lowest-priority ' + >+ table_class + >+ '" title="Remove lowest priority" href="#" data-op="cud-setLowestPriority" data-borrowernumber="' + >+ row.patron_id + >+ '" data-biblionumber="' + >+ biblio_id + >+ '" data-reserve_id="' + >+ row.hold_id + >+ '" data-date="' + >+ row.hold_date + >+ '"><i class="fa fa-lg fa-rotate-90 icon-unset-lowest" aria-hidden="true"></i></a>' >+ ); >+ } else { >+ return ( >+ '<a class="hold-arrow toggle-lowest-priority ' + >+ table_class + >+ '" title="Set lowest priority" href="#" data-op="cud-setLowestPriority" data-borrowernumber="' + >+ row.patron_id + >+ '" data-biblionumber="' + >+ biblio_id + >+ '" data-reserve_id="' + >+ row.hold_id + >+ '" data-date="' + >+ row.hold_date + >+ '"><i class="fa fa-lg fa-rotate-90 icon-set-lowest" aria-hidden="true"></i></a>' >+ ); >+ } >+ } >+ }, >+ }, >+ { >+ data: "hold_id", >+ orderable: false, >+ searchable: false, >+ render: function (data, type, row, meta) { >+ return ( >+ '<a class="cancel-hold deny ' + >+ table_class + >+ '" title="Cancel hold" data-id="' + >+ data + >+ '" data-biblionumber="' + >+ biblio_id + >+ '" data-borrowernumber="' + >+ row.patron_id + >+ '" href="#"><i class="fa fa-trash" aria-label="Cancel hold"></i></a>' >+ ); >+ }, >+ }, >+ { >+ 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 ( >+ '<a class="btn btn-default submit-form-link" href="#" id="revert_hold_' + >+ data + >+ '" data-op="cud-move" data-where="down" data-first_priority="1" data-last_priority="' + >+ totalHolds + >+ '" data-prev_priority="0" data-next_priority="1" data-borrowernumber="' + >+ row.patron_id + >+ '" data-biblionumber="' + >+ biblio_id + >+ '" data-itemnumber="' + >+ row.item_id + >+ '" data-reserve_id="' + >+ row.hold_id + >+ '" data-date="' + >+ row.hold_date + >+ '" data-action="request.pl" data-method="post">' + >+ link_value + >+ "</a>" >+ ); >+ } else { >+ let td = ""; >+ if (SuspendHoldsIntranet) { >+ td += >+ '<button class="btn btn-default btn-xs toggle-suspend ' + >+ table_class + >+ '" data-id="' + >+ data + >+ '" data-biblionumber="' + >+ biblio_id + >+ '" data-suspended="' + >+ row.suspended + >+ '">'; >+ if (row.suspended) { >+ td += >+ '<i class="fa fa-play" aria-hidden="true"></i> ' + >+ __("Unsuspend") + >+ "</button>"; >+ } else { >+ td += >+ '<i class="fa fa-pause" aria-hidden="true"></i> ' + >+ __("Suspend") + >+ "</button>"; >+ } >+ if (AutoResumeSuspendedHolds) { >+ if (row.suspended) { >+ td += >+ '<label for="suspend_until_' + >+ data + >+ '">' + >+ __("Suspend on") + >+ " </label>"; >+ } else { >+ td += >+ '<label for="suspend_until_' + >+ data + >+ '">' + >+ __("Suspend until") + >+ " </label>"; >+ } >+ td += >+ '<input type="text" name="suspend_until_' + >+ data + >+ '" data-id="' + >+ data + >+ '" size="10" value="' + >+ $date(row.suspended_until, { >+ dateformat: "rfc3339", >+ }) + >+ '" class="suspenddate ' + >+ table_class + >+ '" data-flatpickr-futuredate="true" data-suspend-date="' + >+ row.suspended_until + >+ '" />'; >+ } >+ 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 ( >+ '<a class="btn btn-default btn-xs printholdslip ' + >+ table_class + >+ '" data-reserve_id="' + >+ data + >+ '">' + >+ __("Print slip") + >+ "</a>" >+ ); >+ } 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 <span class='badge bg-danger'>%s</span> 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( >+ '<input type="hidden" name="rank-request" value="del">' >+ ); >+ $("#cancel_modal_form #inputs").append( >+ '<input type="hidden" name="biblionumber" value="' + >+ hold.biblionumber + >+ '">' >+ ); >+ $("#cancel_modal_form #inputs").append( >+ '<input type="hidden" name="borrowernumber" value="' + >+ hold.borrowernumber + >+ '">' >+ ); >+ $("#cancel_modal_form #inputs").append( >+ '<input type="hidden" name="reserve_id" value="' + >+ hold.hold + >+ '">' >+ ); >+ } >+ // 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( >+ '<div class="alert alert-danger">' + >+ jqXHR.responseJSON.error + >+ "</div>" >+ ); >+ $("#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("<span class='flatpickr_wrapper'></span>") >+ .after( >+ $("<a/>") >+ .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("<span class='flatpickr_wrapper'></span>") >+ .after( >+ $("<a/>") >+ .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 = $( >+ '<img class="rank-spinner" src="/intranet-tmpl/prog/img/spinner-small.gif" alt="Loading..." style="display:block;margin:0 auto;vertical-align:middle;">' >+ ); >+ $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( >+ '<div id="updating_reserveno' + >+ res_id + >+ '" class="waiting"><img src="/intranet-tmpl/prog/img/spinner-small.gif" alt="" /><span class="waiting_msg"></span></div>' >+ ); >+ 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( >+ '<div id="updating_holdno' + >+ hold_id + >+ '" class="waiting"><img src="/intranet-tmpl/prog/img/spinner-small.gif" alt="" /><span class="waiting_msg"></span></div>' >+ ); >+ 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 fbb94fbed3..948e649e92 100755 >--- a/reserve/request.pl >+++ b/reserve/request.pl >@@ -668,9 +668,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( >@@ -683,64 +687,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... >@@ -763,16 +730,21 @@ 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; > > if (@reserveloop) { > $template->param( reserveloop => \@reserveloop ); > } > >+ $template->param( total_holds => $total_holds ); >+ > if ( $patron && $multi_hold ) { > > # Add the valid pickup locations >-- >2.34.1 >
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 23269
:
173770
|
176705
|
177151
|
181244
|
181247
|
182728
|
182729
|
182730
|
182754
|
182755
|
182756
|
182757
|
191638
|
191669
|
191670
|
192821
|
192849
|
192850
|
192851
|
192939
|
192940
|
192941
|
192942
|
192943
|
193100
|
193101
|
193102
|
193103
|
193104
|
193198
|
193199
|
193200
|
193201
|
193219
|
193336