From 227c269651d751b59a2b0d1505e8875e5f51ac7a Mon Sep 17 00:00:00 2001
From: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Date: Fri, 11 Feb 2022 14:03:36 +0100
Subject: [PATCH] Bug 30063: Make the main patron search use the REST API
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

To test:
* Length menu (PatronsPerPage)
* Query description
* Highlight of the current library

* sticky header - Does not work (?)

If the table does not show when you submit the filter form, make sure
you regenerated the compiled CSS.

Signed-off-by: Séverine Queune <severine.queune@bulac.fr>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
---
 .../prog/en/includes/patron-search-header.inc |   2 +-
 .../prog/en/includes/patron-search.inc        | 311 ++++++++++++---
 .../prog/en/includes/patronfields.inc         |   2 +-
 .../prog/en/modules/members/member.tt         | 376 ++----------------
 .../prog/en/modules/members/search.tt         |   4 +-
 members/member.pl                             |   4 +
 members/members-home.pl                       |   4 +
 7 files changed, 296 insertions(+), 407 deletions(-)

diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/patron-search-header.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/patron-search-header.inc
index 1aa4695213..e7f794d018 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/includes/patron-search-header.inc
+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/patron-search-header.inc
@@ -24,7 +24,7 @@
         <p><label for="searchfieldstype">Search fields:</label>
             <select name="searchfieldstype" id="searchfieldstype">
               [% pref_fields = Koha.Preference('DefaultPatronSearchFields').split(',') %]
-              [% default_fields = [ 'standard', 'surname', 'cardnumber', 'email', 'borrowernumber', 'userid', 'phone', 'address', 'dateofbirth', 'sort1', 'sort2' ] %]
+              [% default_fields = [ 'surname,firstname,othernames,cardnumber,userid', 'surname', 'cardnumber', 'email', 'borrowernumber', 'userid', 'phone', 'address', 'dateofbirth', 'sort1', 'sort2' ] %]
               [% search_options = default_fields.merge(pref_fields).unique %]
               [% FOREACH s_o IN search_options %]
                   [% display_name = PROCESS patron_fields name=s_o %]
diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/patron-search.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/patron-search.inc
index ebe1d0c315..11fce49397 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/includes/patron-search.inc
+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/patron-search.inc
@@ -1,6 +1,8 @@
 [% USE Koha %]
+[% USE I18N %]
 [% USE Branches %]
 [% USE raw %]
+[% USE Asset %]
 [% USE To %]
 
 [% BLOCK patron_search_filters_simple  %]
@@ -18,12 +20,11 @@
             <ol>
                 <li>
                     <label for="search_patron_filter">Search:</label>
-                    <div class="hint">Enter patron card number or partial name:</div>
                     <input type="text" id="search_patron_filter" value="[% search_filter | html %]" class="focus" />
                 </li>
 
-                [% FOR column IN columns %]
-                    [% SWITCH column %]
+                [% FOR f IN filters %]
+                    [% SWITCH f %]
                     [% CASE 'branch' %]
                         <li>
                             <label for="branchcode_filter">Library:</label>
@@ -47,11 +48,43 @@
                                 [% END %]
                             </select>
                         </li>
+                    [% CASE 'search_field' %]
+                        <li>
+                            <label for="searchfieldstype_filter">Search field:</label>
+                            <select name="searchfieldstype" id="searchfieldstype_filter">
+                                [% pref_fields = Koha.Preference('DefaultPatronSearchFields').split(',') %]
+                                [% default_fields = [ 'surname,firstname,othernames,cardnumber,userid', 'surname', 'cardnumber', 'email', 'borrowernumber', 'userid', 'phone', 'address', 'dateofbirth', 'sort1', 'sort2' ] %]
+                                [% search_options = default_fields.merge(pref_fields).unique %]
+                                [% FOREACH s_o IN search_options %]
+                                    [% display_name = PROCESS patron_fields name=s_o %]
+                                    [% NEXT IF !display_name %]
+                                    [% IF searchfieldstype == s_o %]
+                                        <option selected="selected" value=[% s_o | html %]>[% display_name | $raw %]</option>
+                                    [% ELSE %]
+                                        <option value=[% s_o | html %]>[% display_name | $raw %]</option>
+                                    [% END %]
+                                [% END %]
+                            </select>
+                        </li>
+                    [% CASE 'search_type' %]
+                        <li>
+                            <label for="searchtype_filter">Search type:</label>
+                            <select name="searchtype" id="searchtype_filter">
+                              [% IF searchtype == "start_with" %]
+                                <option value='start_with' selected="selected">Starts with</option>
+                                <option value="contain">Contains</option>
+                              [% ELSE %]
+                                <option value='start_with'>Starts with</option>
+                                <option value="contain" selected="selected">Contains</option>
+                              [% END %]
+                            </select>
+                        </li>
                     [% END %]
                 [% END %]
             </ol>
             <fieldset class="action">
                 <input type="submit" value="Search" />
+                <input type="button" value="Clear" id="clear_search" />
             </fieldset>
         </fieldset>
     </form>
@@ -78,12 +111,14 @@
         [% END %]
     </div>
 
+
+    <h3 style="display: none;">Patrons found for: <span id="searchpattern"></span></h3>
+
     <div id="[% table_id | html %]_search_results" style="display:none;">
 
         <div id="info" class="dialog message" style="display: none;"></div>
         <div id="error" class="dialog alert" style="display: none;"></div>
 
-
         <input type="hidden" id="firstletter_filter" value="" />
         [% IF open_on_row_click %]
         <table id="[% table_id | html %]" class="selections-table">
@@ -94,6 +129,7 @@
                 <tr>
                     [% FOR column IN columns %]
                         [% SWITCH column %]
+                            [% CASE 'checkbox' %]<th class="noExport"></th>
                             [% CASE 'cardnumber' %]<th>Card</th>
                             [% CASE 'dateofbirth' %]<th>Date of birth</th>
                             [% CASE 'name' %]<th>Name</th>
@@ -166,17 +202,51 @@
     [% INCLUDE 'js-patron-get-age.inc' %]
     [% INCLUDE 'js-patron-format.inc' %]
     [% INCLUDE 'js-patron-format-address.inc' %]
+    [% IF sticky_header %]
+        [% Asset.js("lib/hc-sticky.js") | $raw %]
+    [% END %]
 
     <script>
         var first_draw = 0;
         let patrons_table;
+        var Sticky;
+        var singleBranchMode = '[% singleBranchMode | html %]';
+        let logged_in_library_id = "[% Branches.GetLoggedInBranchcode | html %]";
 
-        $(document).ready(function(){
+        /* popstate event triggered by forward and back button. Need to refresh search */
+        window.addEventListener('popstate', (event) => {
+            getSearchByLocation( false );
+        });
 
+        $(document).ready(function(){
 
             $("#info").hide();
             $("#error").hide();
 
+            // Build the aLengthMenu
+            var aLengthMenu = [
+                [% Koha.Preference('PatronsPerPage') | html %], 10, 20, 50, 100, -1
+            ];
+            jQuery.unique(aLengthMenu);
+            aLengthMenu.sort(function( a, b ){
+                // Put "All" at the end
+                if ( a == -1 ) {
+                    return 1;
+                } else if ( b == -1 ) {
+                    return -1;
+                }
+                return parseInt(a) < parseInt(b) ? -1 : 1;}
+            );
+            var aLengthMenuLabel = [];
+            $(aLengthMenu).each(function(){
+                if ( this == -1 ) {
+                    // Label for -1 is "All"
+                    aLengthMenuLabel.push(_("All"));
+                } else {
+                    aLengthMenuLabel.push(this);
+                }
+            });
+
             let additional_filters = {
                 surname: function(){
                     let start_with = $("#firstletter_filter").val()
@@ -186,18 +256,23 @@
                 "-and": function(){
                     let filter = $("#search_patron_filter").val();
                     if (!filter) return "";
-                    [% SET search_fields = Koha.Preference('DefaultPatronSearchFields') || 'surname,firstname,othernames,cardnumber,userid' %]
-                    return [
-                        [% FOR search_field IN search_fields.split(',') %]
-                        {"me.[% search_field | html %]":{"like":"%"+filter+"%"}},
-                        [% END %]
-                        [% IF Koha.Preference('ExtendedPatronAttributes') && extended_attribute_types %]
-                        {
-                            "extended_attributes.value": { "like": "%" + filter + "%" },
+
+                    let filters = [];
+                    let search_type = $("#searchtype_filter").val() || "contain";
+                    let search_fields = $("#searchfieldstype_filter").val();
+                    if ( !search_fields ) {
+                        search_fields = "[% Koha.Preference('DefaultPatronSearchFields') || 'surname,firstname,othernames,cardnumber,userid' | html %]";
+                    }
+                    search_fields.split(',').forEach(function(e,i){
+                        filters.push({["me."+e]:{"like":"%"+filter+(search_type == "contain" ? "%" : "" )}});
+                    });
+                    [% IF Koha.Preference('ExtendedPatronAttributes') && extended_attribute_types %]
+                        filters.push({
+                            "extended_attributes.value": { "like": "%" + filter + (search_type == "contain" ? "%" : "" )},
                             "extended_attributes.code": extended_attribute_types
-                        }
-                        [% END %]
-                    ];
+                        });
+                    [% END %]
+                    return filters;
                 }
             };
             [% UNLESS default_sort_column %]
@@ -223,7 +298,10 @@
                         [% IF redirect_if_one_result %]
                             // redirect if there is only 1 result.
                             if ( first_draw && json.recordsFiltered == 1 ) {
-                                document.location.href = '[% redirect_url | url %]&borrowernumber=' + json.data[0].patron_id;
+                                let url = '[% redirect_url | url %]'.indexOf("?") != -1
+                                    ? '[% redirect_url | url %]&borrowernumber=' + json.data[0].patron_id
+                                    : '[% redirect_url | url %]?borrowernumber=' + json.data[0].patron_id;
+                                document.location.href = url;
                                 return false;
                             }
                             first_draw = 0;
@@ -231,6 +309,7 @@
                         return json.data;
                     }
                 },
+                [% IF open_on_row_click OR preview_on_name_click %]
                 "drawCallback": function( settings ) {
                     var api = this.api();
                     var data = api.data();
@@ -244,12 +323,14 @@
                         }).addClass('clickable');
                         $(tr).find("a.patron_name").attr('href', url);
                     });
-                    [% ELSE %]
+                    [% END %]
+                    [% IF preview_on_name_click %]
                     $.each($(this).find("tbody tr"), function(index, tr) {
                         $(tr).find("a.patron_name").addClass("patron_preview");
                     });
                     [% END %]
                 },
+                [% END %]
                 "iDeferLoading": 0,
                 "columns": [
                     [% FOR column IN columns %]
@@ -259,20 +340,27 @@
                         [% SWITCH column %]
                             [% CASE 'checkbox' %]
                             {
-                                "data": "borrowernumber",
-                                "searchable": true,
-                                "orderable": true,
+                                "data": "patron_id",
+                                "searchable": false,
+                                "orderable": false,
                                 "render": function( data, type, row, meta ) {
-                                    return "<input type=\"checkbox\" name="data ? escape_str($date(data) + " (" + _("%s years").format($get_age(data)) + ")") : "";
-
-                                    return "<label for='check" + data + "' class='content_hidden'>" + _("Select patron") + "</label><input type='checkbox' id='check" + data + "' class='selection' name='borrowernumber' value='" + data + "' />",
+                                    return "<label for='check" + data + "' class='content_hidden'>" + _("Select patron") + "</label><input type='checkbox' id='check" + data + "' class='selection' name='borrowernumber' value='" + data + "' />";
                                 }
                             }
                             [% CASE 'cardnumber' %]
                             {
                                 "data": "cardnumber",
                                 "searchable": true,
-                                "orderable": true
+                                "orderable": true,
+                                "render": function( data, type, row, meta ) {
+                                    let patron_id = encodeURIComponent(row.patron_id);
+                                    [% IF !open_on_row_click AND CAN_user_circulate_circulate_remaining_permissions %]
+                                        return "<a href=\"/cgi-bin/koha/circ/circulation.pl?borrowernumber=" + patron_id + "\" title=\"[% I18N.t("Check out") | html %]\" class=\"patron_name\" data-borrowernumber=\"" + patron_id + "\" style=\"white-space:nowrap\">" + escape_str(data) + "</a>";
+                                    [% ELSE %]
+                                        return escape_str(data);
+                                    [% END %]
+                                }
+
                             }
                             [% CASE 'dateofbirth' %]
                             {
@@ -340,7 +428,11 @@
                                 "orderable": true,
                                 "render": function( data, type, row, meta ) {
                                     let patron_id = encodeURIComponent(row.patron_id);
+                                    [% IF ! open_on_row_click %]
                                     return "<a href=\"/cgi-bin/koha/members/moremember.pl?borrowernumber=" + patron_id + "\" class=\"patron_name\" data-borrowernumber=\"" + patron_id + "\" style=\"white-space:nowrap\">" + $patron_to_html(row, { invert_name: 1 }) + "</a>";
+                                    [% ELSE %]
+                                    return $patron_to_html(row, { invert_name: 1 });
+                                    [% END %]
                                 }
                             }
                             [% CASE 'branch' %]
@@ -349,7 +441,12 @@
                                 "searchable": true,
                                 "orderable": true,
                                 "render": function( data, type, row, meta ) {
-                                    return escape_str(libraries_map[data].branchname);
+                                    let library_name = libraries_map[data].branchname
+                                    if( !singleBranchMode && data == logged_in_library_id ) {
+                                        return "<span class=\"currentlibrary\">" + escape_str(library_name) + "</span>";
+                                    } else {
+                                        return escape_str(library_name);
+                                    }
                                 }
                             }
                             [% CASE 'category' %]
@@ -426,11 +523,22 @@
                                 "data": function( row, type, val, meta ) {
 
                                     let patron_id = encodeURIComponent(row.patron_id);
-                                    [% IF selection_type == 'select' %]
-                                        return '<a href="#" class="btn btn-default btn-xs select_user" data-borrowernumber="' + patron_id + '">Select</a><input type="hidden" id="borrower_data' + patron_id + '" name="borrower_data'+ patron_id + '" value=\''+JSON.stringify(row)+'\' />';
-                                    [% ELSE %]
-                                        return '<a href="#" class="btn btn-default btn-xs add_user" data-borrowernumber="' + patron_id + '" data-firstname="' + encodeURIComponent(row.firstname) + '" data-surname="' + encodeURIComponent(row.surname) + '">Add</a><input type="hidden" id="borrower_data' + patron_id + '" name="borrower_data'+ patron_id + '" />';
+                                    let action_node = "";
+                                    [% FOR action IN actions %]
+                                    [% SWITCH action %]
+                                    [% CASE 'select' %]
+                                        action_node += '<a href="#" class="btn btn-default btn-xs select_user" data-borrowernumber="' + patron_id + '">Select</a><input type="hidden" id="borrower_data' + patron_id + '" name="borrower_data'+ patron_id + '" value=\''+JSON.stringify(row)+'\' />';
+                                    [% CASE 'add' %]
+                                        action_node += '<a href="#" class="btn btn-default btn-xs add_user" data-borrowernumber="' + patron_id + '" data-firstname="' + encodeURIComponent(row.firstname) + '" data-surname="' + encodeURIComponent(row.surname) + '">Add</a><input type="hidden" id="borrower_data' + patron_id + '" name="borrower_data'+ patron_id + '" />';
+                                    [% CASE 'edit' %]
+                                        action_node += '<a href="/cgi-bin/koha/members/memberentry.pl?op=modify&amp;destination=circ&amp;borrowernumber=' + patron_id + '" class="btn btn-default btn-xs"><i class="fa fa-pencil"></i> Edit</a>';
+                                    [% CASE 'checkout' %]
+                                        [% IF CAN_user_circulate_circulate_remaining_permissions %]
+                                            action_node += '<a class="btn btn-default btn-xs" href="/cgi-bin/koha/circ/circulation.pl?borrowernumber=' + patron_id + '"><i class="fa fa-barcode"></i> ' + _("Check out") + '</a>';
+                                        [% END %]
+                                    [% END %]
                                     [% END %]
+                                    return action_node;
                                 },
                                 "searchable": false,
                                 "orderable": false
@@ -442,14 +550,24 @@
                 'embed': [% To.json(embed) | $raw %],
                 "order": [[ [% order_column_index | html %], "asc" ]],
                 'bAutoWidth': false,
+                'lengthMenu': [aLengthMenu, aLengthMenuLabel],
                 'sPaginationType': 'full_numbers',
-                "iDisplayLength": [% Koha.Preference('PatronsPerPage') | html %],
+                "pageLength": [% Koha.Preference('PatronsPerPage') | html %],
+                [% IF sticky_header %]
+                "initComplete": function(settings, json) {
+                    Sticky = $("#[% sticky_header | html %]");
+                    Sticky.hcSticky({
+                        stickTo: "#[% sticky_to | html %]",
+                        stickyClass: "floating"
+                    });
+                },
+                [% END %]
             }, typeof table_settings !== 'undefined' ? table_settings : null, 1, additional_filters);
 
             $("#patron_search_form").on('submit', filter);
             $(".filterByLetter").on("click",function(e){
                 e.preventDefault();
-                filterByFirstLetterSurname($(this).text());
+                filterByFirstLetterSurname($(this).text(), true);
             });
             $("body").on("click",".add_user",function(e){
                 e.preventDefault();
@@ -478,8 +596,57 @@
                 $("#patronPreview .modal-body").html("<img src=\"[% interface | html %]/[% theme | html %]/img/spinner-small.gif\" alt=\"\" /> Loading");
             });
 
+            $("#clear_search").on("click",function(e){
+                e.preventDefault();
+                clearFilters();
+                $("#searchpattern").parent().hide();
+            });
+
+            if ( $("#search_patron_filter").val().length > 0 ) {
+                $("#patron_search_form").submit();
+            }
+
+            /* Initial page load does not trigger the popstate event, so we explicitly call this */
+            getSearchByLocation( false );
+
         });
 
+        function getSearchByLocation( setstate ){
+            /* Check to see if the URL contains a search parameter */
+            if( location.search != ""){
+                var params = new URLSearchParams( location.search );
+                var firstletter = params.get("firstletter");
+                /* Check to see if search is a first letter param */
+                if( firstletter ){
+                    /* Trigger function to return search results by letter */
+                    filterByFirstLetterSurname( firstletter, setstate );
+                }
+            }
+        }
+
+        function update_search_description(){
+            var searched = $("#searchfieldstype_filter").find("option:selected").text();
+            if ( $("#search_patron_filter").val() ) {
+                if ( $("#searchtype_filter").val() == 'start_with' ) {
+                    searched += _(" starting with ");
+                } else {
+                    searched += _(" containing ");
+                }
+                searched += "'" + $("#search_patron_filter").val() + "'";
+            }
+            if ( $("#firstletter_filter").val() ) {
+                searched += _(" begins with ") + "'" + $("#firstletter_filter").val() +"'";
+            }
+            if ( $("#categorycode_filter").val() ) {
+                searched += _(" with category ") + "'" + $("#categorycode_filter").find("option:selected").text() + "'";
+            }
+            if ( $("#branchcode_filter").val() ) {
+                searched += _(" in library ") + $("#branchcode_filter").find("option:selected").text();
+            }
+            $("#searchpattern").text(searched);
+            $("#searchpattern").parent().show();
+        }
+
         function filter() {
             $("#firstletter_filter").val('');
             $("#[% table_id | html %]_search_results").show();
@@ -500,50 +667,68 @@
             table_dt.search("");
             first_draw = 1; // Only redirect if we are coming from here
             table_dt.draw();
+            update_search_description();
             return false;
         }
 
+        function clearFilters() {
+            $("#searchfieldstype_filter option:first").prop("selected", true);
+            $("#searchtype_filter option[value='contain']").prop("selected", true);
+            $("#categorycode_filter option:first").prop("selected", true);
+            $("#branchcode_filter option:first").prop("selected", true);
+            $("#firstletter_filter").val('');
+            $("#search_patron_filter").val('');
+            /* remove any search string added by firstletter search */
+            history.pushState( {}, null, window.location.href.split("?" )[0]);
+            $("#[% table_id | html %]_search_results").hide();
+            update_search_description();
+        }
+
         // User has clicked on a letter
-        function filterByFirstLetterSurname(letter) {
+        function filterByFirstLetterSurname(letter, setstate ) {
             $("#firstletter_filter").val(letter);
+
             $("#[% table_id | html %]_search_results").show();
+
+            if ( setstate ) {
+                history.pushState( null, null, "?firstletter=" + letter );
+            }
+
             patrons_table.DataTable().draw();
+            update_search_description();
         }
 
         // modify parent window owner element
-        [% IF selection_type == 'add' %]
-            function add_user(borrowernumber, borrowername) {
-                var p = window.opener;
-                // In one place (serials/routing.tt), the page is reload on every add
-                // We have to wait for the page to be there
-                function wait_for_opener () {
-                    if ( ! $(opener.document).find('body').size() ) {
-                        setTimeout(wait_for_opener, 500);
+        function add_user(borrowernumber, borrowername) {
+            var p = window.opener;
+            // In one place (serials/routing.tt), the page is reload on every add
+            // We have to wait for the page to be there
+            function wait_for_opener () {
+                if ( ! $(opener.document).find('body').size() ) {
+                    setTimeout(wait_for_opener, 500);
+                } else {
+                    [%# Note that add_user could sent data instead of borrowername too %]
+                    $("#info").hide();
+                    $("#error").hide();
+                    if ( p.add_user(borrowernumber, borrowername) < 0 ) {
+                        $("#error").html(_("Patron '%s' is already in the list.").format(borrowername));
+                        $("#error").show();
                     } else {
-                        [%# Note that add_user could sent data instead of borrowername too %]
-                        $("#info").hide();
-                        $("#error").hide();
-                        if ( p.add_user(borrowernumber, borrowername) < 0 ) {
-                            $("#error").html(_("Patron '%s' is already in the list.").format(borrowername));
-                            $("#error").show();
-                        } else {
-                            $("#info").html(_("Patron '%s' added.").format(borrowername));
-                            $("#info").show();
-                        }
+                        $("#info").html(_("Patron '%s' added.").format(borrowername));
+                        $("#info").show();
                     }
                 }
-                wait_for_opener();
             }
-        [% ELSIF selection_type == 'select' %]
-            function select_user(borrowernumber, data) {
-                var p = window.opener;
-                [%  IF callback %]
-                    p.[% callback | html %](borrowernumber, data);
-                [%  ELSE %]
-                    p.select_user(borrowernumber, data);
-                [%  END %]
-                window.close();
-            }
-        [% END %]
+            wait_for_opener();
+        }
+        function select_user(borrowernumber, data) {
+            var p = window.opener;
+            [%  IF callback %]
+                p.[% callback | html %](borrowernumber, data);
+            [%  ELSE %]
+                p.select_user(borrowernumber, data);
+            [%  END %]
+            window.close();
+        }
     </script>
 [% END %]
diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/patronfields.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/patronfields.inc
index 7edc5c9082..6380e78ecd 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/includes/patronfields.inc
+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/patronfields.inc
@@ -1,6 +1,6 @@
 [%- BLOCK patron_fields -%]
      [%- SWITCH name -%]
-     [%- CASE 'standard' -%]<span>Standard</span>
+     [%- CASE 'surname,firstname,othernames,cardnumber,userid' -%]<span>Standard</span>
      [%- CASE 'borrowernumber' -%]<span>Borrowernumber</span>
      [%- CASE 'cardnumber' -%]<span>Card number</span>
      [%- CASE 'surname' -%]<span>Surname</span>
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/member.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/member.tt
index b38378641a..016060065d 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/member.tt
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/member.tt
@@ -6,6 +6,10 @@
 [% USE Categories %]
 [% SET footerjs = 1 %]
 [% PROCESS 'patronfields.inc' %]
+[% SET libraries = Branches.all %]
+[% SET categories = Categories.all.unblessed %]
+[% SET columns = ['cardnumber', 'name', 'dateofbirth', 'branch', 'category', 'dateexpiry', 'checkouts', 'account_balance', 'borrowernotes', 'action'] %]
+[% PROCESS 'patron-search.inc' %]
 [% INCLUDE 'doc-head-open.inc' %]
 <title>Patrons[% IF ( searching ) %] &rsaquo; Search results[% END %] &rsaquo; Koha</title>
 [% INCLUDE 'doc-head-close.inc' %]
@@ -53,13 +57,6 @@
           [% INCLUDE 'patron-toolbar.inc' %]
           [% INCLUDE 'noadd-warnings.inc' %]
 
-          <div class="browse">
-            Browse by last name:
-            [% FOREACH letter IN alphabet.split(' ') %]
-                <a href="#" class="filterByLetter">[% letter | html %]</a>
-            [% END %]
-          </div>
-
           [% IF CAN_user_borrowers_edit_borrowers && pending_borrower_modifications %]
             <div class="pending-info" id="patron_updates_pending">
               <a href="/cgi-bin/koha/members/members-update.pl">Patrons requesting modifications</a>:
@@ -67,10 +64,9 @@
             </div>
           [% END %]
 
-          <div id="searchresults">
-            <h3>Patrons found for: <span id="searchpattern">[% IF searchmember %] for '[% searchmember | html %]'[% END %]</span></h3>
+          <div id="patron_search_results">
             [% IF CAN_user_tools_manage_patron_lists || CAN_user_borrowers_edit_borrowers %]
-              <div class="searchheader fh-fixedHeader" id="searchheader">
+              <div class="searchheader fh-fixedHeader" id="searchheader" style="display:none;">
                   <div>
                       <a href="#" class="btn btn-link" id="select_all"><i class="fa fa-check"></i> Select all</a>
                       |
@@ -103,26 +99,10 @@
                 </div>
             [% END %]
 
-            <table id="memberresultst">
-              <thead>
-                <tr>
-                [% IF CAN_user_borrowers_edit_borrowers || CAN_user_tools_manage_patron_lists %]
-                  <th class="noExport">&nbsp;</th>
-                [% END %]
-                  <th>Card</th>
-                  <th>Name</th>
-                  <th>Date of birth</th>
-                  <th>Category</th>
-                  <th>Library</th>
-                  <th>Expires on</th>
-                  <th>OD/Checkouts</th>
-                  <th>Fines</th>
-                  <th>Circ note</th>
-                  <th class="noExport">&nbsp;</th>
-                </tr>
-              </thead>
-              <tbody></tbody>
-            </table>
+            [% IF CAN_user_borrowers_edit_borrowers || CAN_user_tools_manage_patron_lists %]
+                [% columns.unshift('checkbox') | html %]
+            [% END %]
+            [% PROCESS patron_search_table table_id => 'memberresultst' columns => columns %]
           </div>
 
             </main>
@@ -130,76 +110,7 @@
 
         <div class="col-sm-2 col-sm-pull-10">
             <aside>
-                <form method="get" id="searchform">
-                  <input type="hidden" id="firstletter_filter" value="" />
-                  <fieldset class="brief">
-                    <h3>Search patrons</h3>
-                    <ol>
-                      <li>
-                        <label for="searchmember_filter">Search for:</label>
-                        <input type="text" id="searchmember_filter" value="[% searchmember | html %]"/>
-                      </li>
-                      <li>
-                        <label for="searchfieldstype_filter">Search field:</label>
-                        <select name="searchfieldstype" id="searchfieldstype_filter">
-                            [% pref_fields = Koha.Preference('DefaultPatronSearchFields').split(',') %]
-                            [% default_fields = [ 'standard', 'surname', 'cardnumber', 'email', 'borrowernumber', 'userid', 'phone', 'address', 'dateofbirth', 'sort1', 'sort2' ] %]
-                            [% search_options = default_fields.merge(pref_fields).unique %]
-                            [% FOREACH s_o IN search_options %]
-                                [% display_name = PROCESS patron_fields name=s_o %]
-                                [% NEXT IF !display_name %]
-                                [% IF searchfieldstype == s_o %]
-                                    <option selected="selected" value=[% s_o | html %]>[% display_name | $raw %]</option>
-                                [% ELSE %]
-                                    <option value=[% s_o | html %]>[% display_name | $raw %]</option>
-                                [% END %]
-                            [% END %]
-                        </select>
-                      </li>
-                      <li>
-                        <label for="searchtype_filter">Search type:</label>
-                        <select name="searchtype" id="searchtype_filter">
-                          [% IF searchtype == "start_with" %]
-                            <option value='start_with' selected="selected">Starts with</option>
-                            <option value="contain">Contains</option>
-                          [% ELSE %]
-                            <option value='start_with'>Starts with</option>
-                            <option value="contain" selected="selected">Contains</option>
-                          [% END %]
-                        </select>
-                      </li>
-                      <li>
-                        <label for="categorycode_filter">Patron category:</label>
-                        [% SET categories = Categories.all() %]
-                        <select id="categorycode_filter">
-                          <option value="">Any</option>
-                          [% FOREACH cat IN categories %]
-                            [% IF cat.categorycode == categorycode_filter %]
-                    <option selected="selected" value="[% cat.categorycode | html %]">[% cat.description | html %]</option>
-                            [% ELSE %]
-                    <option value="[% cat.categorycode | html %]">[% cat.description | html %]</option>
-
-                            [% END %]
-                          [% END %]
-                        </select>
-                      </li>
-                      <li>
-                        <label for="branchcode_filter">Library:</label>
-                        [% SET branches = Branches.all( selected => branchcode_filter, only_from_group => 1 ) %]
-                        <select id="branchcode_filter">
-                          [% IF branches.size != 1 %]
-                            <option value="">Any</option>
-                          [% END %]
-                          [% PROCESS options_for_libraries libraries => branches %]
-                        </select>
-                      </li>
-                    </ol>
-                    <fieldset class="action">
-                      <input type="submit" value="Search" />
-                      <input type="button" value="Clear" id="clear_search" />
-                    </fieldset>
-                  </fieldset>
-                </form>
+                [% PROCESS patron_search_filters categories => categories, libraries => libraries, filters => ['search_field', 'search_type', 'category', 'branch'], search_filter => searchmember %]
             </aside>
         </div> <!-- /.col-sm-2.col-sm-pull-10 -->
     </div> <!-- /.row -->
@@ -234,16 +145,8 @@
     [% INCLUDE 'datatables.inc' %]
     [% INCLUDE 'columns_settings.inc' %]
     [% INCLUDE 'str/members-menu.inc' %]
-    [% Asset.js("lib/hc-sticky.js") | $raw %]
     [% Asset.js("js/members-menu.js") | $raw %]
     <script>
-        var singleBranchMode = '[% singleBranchMode | html %]';
-        var dtMemberResults;
-        var search = 1;
-        /* popstate event triggered by forward and back button. Need to refresh search */
-        window.addEventListener('popstate', (event) => {
-            getSearchByLocation( false );
-        });
 
         $(document).ready(function() {
             $('#merge-patrons').prop('disabled', true);
@@ -313,10 +216,6 @@
                 patronListAdd();
             });
 
-            $(".filterByLetter").on("click",function(e){
-                e.preventDefault();
-                filterByFirstLetterSurname( $(this).text(), true );
-            });
             $("#select_all").on("click",function(e){
                 e.preventDefault();
                 $(".selection").prop("checked", true).change();
@@ -325,11 +224,6 @@
                 e.preventDefault();
                 $(".selection").prop("checked", false).change();
             });
-            $("#clear_search").on("click",function(e){
-                e.preventDefault();
-                clearFilters(true);
-            });
-            $("#searchform").on("submit", filter);
 
             [% IF searchmember %]
                 $("#searchmember_filter").val("[% searchmember | html %]");
@@ -347,161 +241,16 @@
                 $("#branchcode_filter").val("[% branchcode_filter | html %]");
             [% END %]
 
-            [% IF view != "show_results" %]
-                search = 0;
-            [% ELSE %]
-                $("#searchresults").show();
-            [% END %]
-
-            // Build the aLengthMenu
-            var aLengthMenu = [
-                [% PatronsPerPage | html %], 10, 20, 50, 100, -1
-            ];
-            jQuery.unique(aLengthMenu);
-            aLengthMenu.sort(function( a, b ){
-                // Put "All" at the end
-                if ( a == -1 ) {
-                    return 1;
-                } else if ( b == -1 ) {
-                    return -1;
-                }
-                return parseInt(a) < parseInt(b) ? -1 : 1;}
-            );
-            var aLengthMenuLabel = [];
-            $(aLengthMenu).each(function(){
-                if ( this == -1 ) {
-                    // Label for -1 is "All"
-                    aLengthMenuLabel.push(_("All"));
-                } else {
-                    aLengthMenuLabel.push(this);
-                }
-            });
+            $("#searchheader").hide();
+            $("#patron_search_form").on('submit', function(){$("#searchheader").show();});
+            $("#clear_search").on("click",function(e){$("#searchheader").hide();});
 
             // Apply DataTables on the results table
-            var columns_settings = [% TablesSettings.GetColumns( 'members', 'member', 'memberresultst', 'json' ) | $raw %];
+            var table_settings = [% TablesSettings.GetColumns( 'members', 'member', 'memberresultst', 'json' ) | $raw %];
             [% UNLESS CAN_user_borrowers_edit_borrowers OR CAN_user_tools_manage_patron_lists %]
                 [%# Remove the first column if we do not display the checkbox %]
-                columns_settings.splice(0, 1);
+                table_settings.splice(0, 1);
             [% END %]
-            dtMemberResults = KohaTable("memberresultst", {
-                'bServerSide': true,
-                'sAjaxSource': "/cgi-bin/koha/svc/members/search",
-                'fnServerData': function(sSource, aoData, fnCallback) {
-                    if ( ! search ) {
-                        return;
-                    }
-                    aoData.push({
-                        'name': 'searchmember',
-                        'value': $("#searchmember_filter").val()
-                    },{
-                        'name': 'firstletter',
-                        'value': $("#firstletter_filter").val()
-                    },{
-                        'name': 'searchfieldstype',
-                        'value': $("#searchfieldstype_filter").val()
-                    },{
-                        'name': 'searchtype',
-                        'value': $("#searchtype_filter").val()
-                    },{
-                        'name': 'categorycode',
-                        'value': $("#categorycode_filter").val()
-                    },{
-                        'name': 'branchcode',
-                        'value': $("#branchcode_filter").val()
-                    },{
-                        'name': 'name_sorton',
-                        'value': 'borrowers.surname borrowers.firstname'
-                    },{
-                        'name': 'cardnumber_sorton',
-                        'value': 'borrowers.cardnumber',
-                    },{
-                        'name': 'dateofbirth_sorton',
-                        'value': 'borrowers.dateofbirth',
-                    },{
-                        'name': 'dateexpiry_sorton',
-                        'value': 'borrowers.dateexpiry',
-                    },{
-                        'name': 'category_sorton',
-                        'value': 'categories.description',
-                    },{
-                        'name': 'branch_sorton',
-                        'value': 'branches.branchname'
-                    },{
-                        'name': 'borrowernotes_sorton',
-                        'value': 'borrowers.borrowernotes'
-                    },{
-                        'name': 'template_path',
-                        'value': 'members/tables/members_results.tt',
-                    });
-                    $.ajax({
-                        'dataType': 'json',
-                        'type': 'POST',
-                        'url': sSource,
-                        'data': aoData,
-                        'success': function(json){
-                            // redirect if there is only 1 result.
-                            if ( json.iTotalDisplayRecords == 1 ) {
-                                var borrowernumber = json.aaData[0].borrowernumber;
-                                /* Overwrite history state of firstletter search since only one result was returned; This prevents a loop upon clicking back */
-                                history.replaceState( {}, null, window.location.href.split("?" )[0]);
-                                [% IF circsearch == 1 %]
-                                    [% SET redirect = "circ/circulation" %]
-                                [% ELSE %]
-                                    [% SET redirect = "members/moremember" %]
-                                [% END %]
-                                document.location.href="/cgi-bin/koha/[% redirect | $raw %].pl?borrowernumber="+borrowernumber;
-                                return false;
-                            }
-                            fnCallback(json);
-                        }
-                    });
-                },
-                'aoColumns':[
-                    [% IF CAN_user_borrowers_edit_borrowers || CAN_user_tools_manage_patron_lists %]
-                      { 'mDataProp': 'dt_borrowernumber', 'bSortable': false },
-                    [% END %]
-                    { 'mDataProp': 'dt_cardnumber' },
-                    { 'mDataProp': 'dt_name' },
-                    { 'mDataProp': 'dt_dateofbirth' },
-                    { 'mDataProp': 'dt_category' },
-                    {
-                        'mDataProp': function ( oObj ) {
-                            if( !singleBranchMode &&  oObj.dt_branch == "[% Branches.GetLoggedInBranchname | html %]" ){
-                                return "<span class=\"currentlibrary\">" + oObj.dt_branch + "</span>";
-                            } else {
-                                return oObj.dt_branch;
-                            }
-                        }
-                    },
-                    { 'mDataProp': 'dt_dateexpiry' },
-                    { 'mDataProp': 'dt_od_checkouts', 'bSortable': false },
-                    { 'mDataProp': 'dt_fines', 'bSortable': false },
-                    { 'mDataProp': 'dt_borrowernotes' },
-                    { 'mDataProp': 'dt_action', 'bSortable': false, 'sClass': 'actions' }
-                ],
-                'bFilter': false,
-                'bAutoWidth': false,
-                [% IF CAN_user_borrowers_edit_borrowers || CAN_user_tools_manage_patron_lists %]
-                    'aaSorting': [[2, 'asc']],
-                [% ELSE %]
-                    'aaSorting': [[1, 'asc']],
-                [% END %]
-                "aLengthMenu": [aLengthMenu, aLengthMenuLabel],
-                'sPaginationType': 'full_numbers',
-                "iDisplayLength": [% PatronsPerPage | html %],
-                "bProcessing": true,
-                "initComplete": function(settings, json) {
-                    Sticky = $("#searchheader");
-                    Sticky.hcSticky({
-                        stickTo: "#searchresults",
-                        stickyClass: "floating"
-                    });
-                }
-            }, columns_settings);
-            update_searched();
-            /* Initial page load doesn't trigger the popstate event, so we explicitly call this */
-            getSearchByLocation( false );
-
         });
 
         function patronListAdd(){
@@ -536,52 +285,6 @@
             return true;
         }
 
-        function getSearchByLocation( setstate ){
-            /* Check to see if the URL contains a search parameter */
-            if( location.search != ""){
-                var params = new URLSearchParams( location.search );
-                var firstletter = params.get("firstletter");
-                /* Check to see if search is a first letter param */
-                if( firstletter ){
-                    /* Trigger function to return search results by letter */
-                    filterByFirstLetterSurname( firstletter, setstate );
-                }
-            }
-        }
-
-        // Update the string "Results found ..."
-        function update_searched(){
-            var searched = $("#searchfieldstype_filter").find("option:selected").text();
-            if ( $("#searchmember_filter").val() ) {
-                if ( $("#searchtype_filter").val() == 'start_with' ) {
-                    searched += _(" starting with ");
-                } else {
-                    searched += _(" containing ");
-                }
-                searched += "'" + $("#searchmember_filter").val() + "'";
-            }
-            if ( $("#firstletter_filter").val() ) {
-                searched += _(" begins with ") + "'" + $("#firstletter_filter").val() +"'";
-            }
-            if ( $("#categorycode_filter").val() ) {
-                searched += _(" with category ") + "'" + $("#categorycode_filter").find("option:selected").text() + "'";
-            }
-            if ( $("#branchcode_filter").val() ) {
-                searched += _(" in library ") + $("#branchcode_filter").find("option:selected").text();
-            }
-            $("#searchpattern").text(searched);
-        }
-
-        // Redraw the table
-        function filter() {
-            $("#firstletter_filter").val('');
-            update_searched();
-            search = 1;
-            $("#searchresults").show();
-            dtMemberResults.fnDraw();
-            return false;
-        }
-
         $('#memberresultst tbody').on('click','td',function(e){
             var $checkbox = $(this).find("input[type=checkbox]");
             if (e.target.type != "checkbox") {
@@ -590,34 +293,27 @@
             }
         });
 
-        // User has clicked on the Clear button
-        function clearFilters(redraw) {
-            $("#searchform select").val('');
-            $("#firstletter_filter").val('');
-            $("#searchmember_filter").val('');
-            if(redraw) {
-                /* remove any search string added by firstletter search */
-                history.pushState( {}, null, window.location.href.split("?" )[0]);
-                update_searched();
-                search = 0;
-                $("#searchresults").hide();
-                dtMemberResults.fnDraw();
-            }
-        }
-
-        // User has clicked on a letter
-        function filterByFirstLetterSurname( letter, setstate ) {
-            clearFilters(false);
-            $("#firstletter_filter").val(letter);
-            if( setstate ){
-                history.pushState( null, null, "?firstletter=" + letter );
-            }
-            update_searched();
-            search = 1;
-            $("#searchresults").show();
-            dtMemberResults.fnDraw();
-        }
     </script>
+
+    [% IF circsearch == 1 %]
+        [% SET redirect_url = '/cgi-bin/koha/circ/circulation.pl' %]
+    [% ELSE %]
+        [% SET redirect_url = '/cgi-bin/koha/members/moremember.pl' %]
+    [% END %]
+    [% PROCESS patron_search_js
+        table_id    => 'memberresultst',
+        categories  => categories,
+        libraries   => libraries,
+        extended_attribute_types => attribute_type_codes,
+        columns     => columns,
+        actions     => ['edit', 'checkout'],
+        redirect_if_one_result => 1,
+        redirect_url           => redirect_url,
+        sticky_header => "searchheader",
+        sticky_to     => "patron_search_results",
+    %]
+
+
 [% END %]
 
 [% INCLUDE 'intranet-bottom.inc' %]
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/search.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/search.tt
index 0106acc4ce..ba77fb046e 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/search.tt
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/search.tt
@@ -17,7 +17,7 @@
 <div id="patron_search" class="yui-t7">
     <div class="container-fluid">
 
-        [% PROCESS patron_search_filters categories => categories, libraries => libraries, columns => columns, search_filter => searchmember %]
+        [% PROCESS patron_search_filters categories => categories, libraries => libraries, filters => ['branch', 'category'], search_filter => searchmember %]
         </form>
 
         [% PROCESS patron_search_table table_id => 'memberresultst' columns => columns %]
@@ -28,7 +28,7 @@
 </div>
 
 [% MACRO jsinclude BLOCK %]
-    [% PROCESS patron_search_js table_id => 'memberresultst', categories => categories, libraries => libraries, extended_attribute_types => attribute_type_codes, columns => columns, filter => filter, selection_type => selection_type, callback => callback %]
+    [% PROCESS patron_search_js table_id => 'memberresultst', categories => categories, libraries => libraries, extended_attribute_types => attribute_type_codes, columns => columns, filter => filter, actions => [selection_type], preview_on_name_click => 1, callback => callback %]
 [% END %]
 
 [% SET popup_window = 1 %]
diff --git a/members/member.pl b/members/member.pl
index b6ce83fb7a..bd85a3b613 100755
--- a/members/member.pl
+++ b/members/member.pl
@@ -29,6 +29,7 @@ use C4::Output qw( output_html_with_http_headers );
 use CGI qw( -utf8 );
 use Koha::List::Patron qw( GetPatronLists );
 use Koha::Patrons;
+use Koha::Patron::Attribute::Types;
 
 my $input = CGI->new;
 
@@ -79,6 +80,9 @@ $template->param(
     PatronsPerPage      => C4::Context->preference("PatronsPerPage") || 20,
     view                => $view,
     circsearch          => $circsearch,
+    attribute_type_codes => ( C4::Context->preference('ExtendedPatronAttributes')
+        ? [ Koha::Patron::Attribute::Types->search( { staff_searchable => 1 } )->get_column('code') ]
+        : [] ),
 );
 
 output_html_with_http_headers $input, $cookie, $template->output;
diff --git a/members/members-home.pl b/members/members-home.pl
index d5efc9d6b2..e3b903f463 100755
--- a/members/members-home.pl
+++ b/members/members-home.pl
@@ -27,6 +27,7 @@ use Koha::Patron::Modifications;
 use Koha::Libraries;
 use Koha::List::Patron qw( GetPatronLists );
 use Koha::Patron::Categories;
+use Koha::Patron::Attribute::Types;
 
 my $query = CGI->new;
 
@@ -72,6 +73,9 @@ $template->param(
     PatronAutoComplete => C4::Context->preference('PatronAutoComplete'),
     patron_lists => [ GetPatronLists() ],
     PatronsPerPage => C4::Context->preference("PatronsPerPage") || 20,
+    attribute_type_codes => ( C4::Context->preference('ExtendedPatronAttributes')
+        ? [ Koha::Patron::Attribute::Types->search( { staff_searchable => 1 } )->get_column('code') ]
+        : [] ),
 );
 
 output_html_with_http_headers $query, $cookie, $template->output;
-- 
2.20.1