Bugzilla – Attachment 184644 Details for
Bug 40517
Allow grouping existing holds
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 40517: Patron holds table: Add 'group selected holds' functionality
Bug-40517-Patron-holds-table-Add-group-selected-ho.patch (text/plain), 16.88 KB, created by
Pedro Amorim
on 2025-07-25 14:38:40 UTC
(
hide
)
Description:
Bug 40517: Patron holds table: Add 'group selected holds' functionality
Filename:
MIME Type:
Creator:
Pedro Amorim
Created:
2025-07-25 14:38:40 UTC
Size:
16.88 KB
patch
obsolete
>From 9744f2da037d27c81fa1f0d7550936833b79a23d Mon Sep 17 00:00:00 2001 >From: Pedro Amorim <pedro.amorim@openfifth.co.uk> >Date: Tue, 15 Jul 2025 16:02:57 +0000 >Subject: [PATCH] Bug 40517: Patron holds table: Add 'group selected holds' > functionality > >This adds the UI elements required to group selected holds from the >patron pages as well as 2 new concepts: > >Visual group ID: >The visual group ID is designed to be easy to understand from a UI/UX >perspective and not show the user an indefinitely incremented value. >When holds are cancelled, filled or moved to a different group, if the >old group is left empty or with a single hold, the hold group is deleted. >--- > Koha/Hold.pm | 33 +++- > Koha/Patron.pm | 8 +- > .../en/includes/modals/holds_table_modals.inc | 21 +++ > .../prog/en/includes/patron-detail-tabs.inc | 9 + > .../prog/en/modules/circ/circulation.tt | 1 + > .../prog/en/modules/members/moremember.tt | 1 + > koha-tmpl/intranet-tmpl/prog/js/holds.js | 155 +++++++++++++++--- > 7 files changed, 199 insertions(+), 29 deletions(-) > >diff --git a/Koha/Hold.pm b/Koha/Hold.pm >index 998e5beda21..e4ce7dae264 100644 >--- a/Koha/Hold.pm >+++ b/Koha/Hold.pm >@@ -179,6 +179,27 @@ sub delete { > return $deleted; > } > >+=head3 cleanup_hold_group >+ >+$self->cleanup_hold_group; >+ >+Check if a hold group is left with a single or zero holds. Delete hold_group if so. >+Accepts an optional $hold_group_id that, if defined, uses that instead of self->hold_group_id >+ >+=cut >+ >+sub cleanup_hold_group { >+ my ( $self, $hold_group_id ) = @_; >+ >+ my $hold_group_id_to_check = $hold_group_id // $self->hold_group_id; >+ my $hold_group = Koha::HoldGroups->find($hold_group_id_to_check); >+ return unless $hold_group; >+ >+ if ( $hold_group->holds->count <= 1 ) { >+ $hold_group->delete(); >+ } >+} >+ > =head3 set_transfer > > =cut >@@ -673,9 +694,10 @@ sub cancellation_requested { > > my $cancel_hold = $hold->cancel( > { >- [ charge_cancel_fee => 1||0, ] >- [ cancellation_reason => $cancellation_reason, ] >- [ skip_holds_queue => 1||0 ] >+ [ charge_cancel_fee => 1||0, ] >+ [ cancellation_reason => $cancellation_reason, ] >+ [ skip_holds_queue => 1||0 ] >+ [ skip_hold_group_cleanup => 1||0 ] > } > ); > >@@ -799,6 +821,8 @@ sub cancel { > } > ); > >+ $self->cleanup_hold_group() unless $params->{skip_hold_group_cleanup}; >+ > if ($autofill_next) { > my ( undef, $next_hold ) = C4::Reserves::CheckReserves( $self->item ); > if ($next_hold) { >@@ -886,9 +910,10 @@ sub fill { > if ( $self->hold_group_id ) { > my @holds = $self->hold_group->holds->as_list; > foreach my $h (@holds) { >- $h->cancel unless $h->reserve_id == $self->id; >+ $h->cancel( { skip_hold_group_cleanup => 1 } ) unless $h->reserve_id == $self->id; > } > } >+ $self->cleanup_hold_group(); > > Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue( > { biblio_ids => [ $old_me->biblionumber ] } ) >diff --git a/Koha/Patron.pm b/Koha/Patron.pm >index 23f38211db9..a57fb3fd8fe 100644 >--- a/Koha/Patron.pm >+++ b/Koha/Patron.pm >@@ -1852,6 +1852,8 @@ Creates and returns a hold group given a list of hold ids > > If force_grouped is supplied, the hold group will be created even if the holds are already grouped > >+Checks if the hold is moving from another group, and if so, removes the old group if empty >+ > =cut > > sub create_hold_group { >@@ -1908,9 +1910,13 @@ sub create_hold_group { > { visual_hold_group_id => $next_available_visual_hold_group_id } > ); > foreach my $hold_id (@$hold_ids) { >- my $hold = Koha::Holds->find($hold_id); >+ my $hold = Koha::Holds->find($hold_id); >+ my $previous_hold_group_id = $hold->hold_group_id; > > $hold->hold_group_id( $hold_group_rs->hold_group_id )->store; >+ if ( $previous_hold_group_id && $previous_hold_group_id != $hold_group_rs->hold_group_id ) { >+ $hold->cleanup_hold_group($previous_hold_group_id); >+ } > } > > return Koha::HoldGroup->_new_from_dbic($hold_group_rs); >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/holds_table_modals.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/holds_table_modals.inc >index 7beb583dff1..e8a458bfb17 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/holds_table_modals.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/holds_table_modals.inc >@@ -74,3 +74,24 @@ > </div> > </div> > [% END %] >+ >+[% BLOCK 'group-hold-modal' %] >+ <div id="group-modal" class="modal" role="dialog" aria-hidden="true"> >+ <div class="modal-dialog"> >+ <div class="modal-content"> >+ <div class="modal-header"> >+ <h1 class="modal-title" id="suspend-modal-label"> Group selected holds </h1> >+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> >+ </div> >+ <div class="modal-body"> >+ <p>Are you sure you want to group the selected holds?</p> >+ </div> >+ <div class="modal-footer"> >+ <button id="group-modal-submit" class="btn btn-primary" type="submit" name="submit">Group</button> >+ <button type="button" class="btn btn-default" data-bs-dismiss="modal">Cancel </button> >+ </div> >+ <div id="inputs"></div> >+ </div> >+ </div> >+ </div> >+[% END %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/patron-detail-tabs.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/patron-detail-tabs.inc >index b59888eca45..97f403594b9 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/patron-detail-tabs.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/patron-detail-tabs.inc >@@ -151,6 +151,9 @@ > <tr> > <th id="checkbox" data-colname="checkbox"><input type="checkbox" class="select_hold_all" /></th> > <th>Hold date</th> >+ [% IF Koha.Preference('DisplayAddHoldGroups') %] >+ <th>Hold group</th> >+ [% END %] > <th class="anti-the">Title</th> > <th>Call number</th> > <th>Item type</th> >@@ -173,6 +176,12 @@ > > [% PROCESS 'cancel-hold-modal' form_action = '/cgi-bin/koha/reserve/modrequest.pl' from_param = patronpage %] > [% PROCESS 'suspend-hold-modal' %] >+ [% PROCESS 'group-hold-modal' %] >+ [% IF Koha.Preference('DisplayAddHoldGroups') %] >+ <fieldset class="action"> >+ <button class="btn btn-primary group_selected_holds" data-bulk="true"></button> >+ </fieldset> >+ [% END # IF DisplayAddHoldGroups %] > > [% IF Koha.Preference('SuspendHoldsIntranet') %] > <fieldset class="action"> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt >index 439d2726d2c..f9081e0eda6 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt >@@ -1105,6 +1105,7 @@ > relatives_borrowernumbers.push("[% b | html %]"); > [% END %] > var SuspendHoldsIntranet = [% ( Koha.Preference('SuspendHoldsIntranet') ) ? 1 : 0 | html %]; >+ var DisplayAddHoldGroups = [% ( Koha.Preference('DisplayAddHoldGroups') ) ? 1 : 0 | html %]; > </script> > [% Asset.js("js/pages/circulation.js") | $raw %] > [% IF Koha.Preference('ClaimReturnedLostValue') || Koha.Preference('BundleLostValue') %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt >index f4db8dd9896..10907907bdb 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt >@@ -780,6 +780,7 @@ > relatives_borrowernumbers.push("[% b | html %]"); > [% END %] > var SuspendHoldsIntranet = [% ( Koha.Preference('SuspendHoldsIntranet') ) ? 1 : 0 | html %]; >+ var DisplayAddHoldGroups = [% ( Koha.Preference('DisplayAddHoldGroups') ) ? 1 : 0 | html %]; > </script> > [% Asset.js("js/pages/circulation.js") | $raw %] > [% IF Koha.Preference('ClaimReturnedLostValue') || Koha.Preference('BundleLostValue') %] >diff --git a/koha-tmpl/intranet-tmpl/prog/js/holds.js b/koha-tmpl/intranet-tmpl/prog/js/holds.js >index 49884d99e86..6f671d91916 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/holds.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/holds.js >@@ -164,6 +164,11 @@ $(document).ready(function () { > return ( > '<input type="checkbox" class="select_hold" data-id="' + > oObj.reserve_id + >+ (oObj.hold_group_id >+ ? '" data-hold-group-id="' + >+ oObj.hold_group_id + >+ '"' >+ : '"') + > '" data-borrowernumber="' + > borrowernumber + > '" data-biblionumber="' + >@@ -178,6 +183,28 @@ $(document).ready(function () { > sort: "reservedate", > }, > }, >+ ...(DisplayAddHoldGroups >+ ? [ >+ { >+ data: function (oObj) { >+ title = ""; >+ if (oObj.visual_hold_group_id) { >+ var link = >+ '<a class="hold-group" href="/cgi-bin/koha/reserve/hold-group.pl?hold_group_id=' + >+ oObj.hold_group_id + >+ '">' + >+ oObj.visual_hold_group_id + >+ "</a>"; >+ >+ title = >+ "<span>" + link + "</span>"; >+ } >+ >+ return title; >+ }, >+ }, >+ ] >+ : []), > { > data: function (oObj) { > title = >@@ -232,17 +259,6 @@ $(document).ready(function () { > "</span>"; > } > >- if (oObj.hold_group_id) { >- title += "<br>"; >- var link = >- '<a class="hold-group" href="/cgi-bin/koha/reserve/hold-group.pl?hold_group_id=' + >- oObj.hold_group_id + >- '">' + >- __("part of a hold group") + >- "</a>"; >- title += "<span>(" + link + ")</span>"; >- } >- > return title; > }, > }, >@@ -603,19 +619,7 @@ $(document).ready(function () { > e.preventDefault(); > let selected_holds; > if (!$(this).data("hold-id")) { >- selected_holds = >- "[" + >- $(".holds_table .select_hold:checked") >- .toArray() >- .map(el => >- JSON.stringify({ >- hold: $(el).data("id"), >- borrowernumber: $(el).data("borrowernumber"), >- biblionumber: $(el).data("biblionumber"), >- }) >- ) >- .join(",") + >- "]"; >+ selected_holds = get_selected_holds_data(); > } else { > selected_holds = > "[" + JSON.stringify({ hold: $(this).data("hold-id") }) + "]"; >@@ -785,6 +789,8 @@ $(document).ready(function () { > }); > } > >+ var MSG_GROUP_SELECTED = _("Group selected (%s)"); >+ > function updateSelectedHoldsButtonCounters() { > $(".cancel_selected_holds").html( > MSG_CANCEL_SELECTED.format( >@@ -796,6 +802,11 @@ $(document).ready(function () { > $(".holds_table .select_hold:checked").length > ) > ); >+ $(".group_selected_holds").html( >+ MSG_GROUP_SELECTED.format( >+ $(".holds_table .select_hold:checked").length >+ ) >+ ); > } > > updateSelectedHoldsButtonCounters(); >@@ -926,4 +937,100 @@ $(document).ready(function () { > '<input type="hidden" name="reserve_id" value="' + hold.hold + '">' > ); > } >+ >+ $(".group_selected_holds").html( >+ MSG_GROUP_SELECTED.format($(".holds_table .select_hold:checked").length) >+ ); >+ >+ $(".group_selected_holds").click(function (e) { >+ if ($(".holds_table .select_hold:checked").length > 1) { >+ let selected_holds = get_selected_holds_data(); >+ const group_ids = JSON.parse(selected_holds) >+ .filter(hold => hold.hold_group_id) >+ .map(hold => hold.hold_group_id); >+ >+ if (group_ids.length > 0) { >+ $("#group-modal .modal-body").prepend( >+ '<div class="alert alert-warning">' + >+ __( >+ "Already grouped holds will be moved to the new group" >+ ) + >+ "</div>" >+ ); >+ } >+ >+ $("#group-modal").modal("show"); >+ } >+ return false; >+ }); >+ >+ $("#group-modal-submit").click(function (e) { >+ e.preventDefault(); >+ let selected_holds = get_selected_holds_data(); >+ >+ const hold_ids = JSON.parse(selected_holds).map(hold => hold.hold); >+ >+ try { >+ group_holds(hold_ids) >+ .success(function () { >+ holdsTable.api().ajax.reload(); >+ }) >+ .fail(function (jqXHR) { >+ $("#group-modal .modal-body").prepend( >+ '<div class="alert alert-danger">' + >+ jqXHR.responseJSON.error + >+ "</div>" >+ ); >+ $("#group-modal-submit").prop("disabled", true); >+ }) >+ .done(function () { >+ $("#group-modal").modal("hide"); >+ $(".select_hold_all").click(); >+ }); >+ } catch (error) { >+ if (error.status === 404) { >+ alert(__("Unable to group, hold not found.")); >+ } else { >+ alert( >+ __( >+ "Your request could not be processed. Check the logs for details." >+ ) >+ ); >+ } >+ } >+ return false; >+ }); >+ >+ function group_holds(hold_ids) { >+ return $.ajax({ >+ method: "POST", >+ url: "/api/v1/patrons/" + borrowernumber + "/hold_groups", >+ contentType: "application/json", >+ data: JSON.stringify({ hold_ids: hold_ids, force_grouped: true }), >+ }); >+ } >+ >+ $("#group-modal").on("hidden.bs.modal", function () { >+ $("#group-modal .modal-body .alert-warning").remove(); >+ $("#group-modal .modal-body .alert-danger").remove(); >+ $("#group-modal-submit").prop("disabled", false); >+ }); >+ >+ function get_selected_holds_data() { >+ return ( >+ "[" + >+ $(".holds_table .select_hold:checked") >+ .toArray() >+ .map(el => >+ JSON.stringify({ >+ hold: $(el).data("id"), >+ borrowernumber: $(el).data("borrowernumber"), >+ biblionumber: $(el).data("biblionumber"), >+ hold_group_id: $(el).data("hold-group-id"), >+ }) >+ ) >+ .join(",") + >+ "]" >+ ); >+ } > }); >-- >2.39.5
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 40517
:
184640
|
184641
|
184642
|
184643
| 184644 |
184645
|
184646