Bug 35329: (follow-up) Trigger Select2 upon modal open
[koha.git] / koha-tmpl / intranet-tmpl / prog / en / includes / patron-search.inc
1 [% USE Koha %]
2 [% USE I18N %]
3 [% USE Branches %]
4 [% USE ExtendedAttributeTypes %]
5 [% USE Categories %]
6 [% USE raw %]
7 [% USE Asset %]
8 [% USE To %]
9
10 [% SET search_results_block_id = 'searchresults' %]
11
12 [%# Display a simple form %]
13 [% BLOCK patron_search_filters_simple  %]
14     <form method="get" class="patron_search_form">
15         <div class="hint">Enter patron card number or partial name:</div>
16         <input type="text" size="40" class="search_patron_filter" class="focus" autocomplete="off" />
17         <input type="submit" class="btn btn-primary" value="Search" />
18     </form>
19 [% END %]
20
21 [%# Display a complex patron search form %]
22 [%# - Search: input %]
23 [%# You can then pass a list of filters %]
24 [%# - branch: select library list %]
25 [%# - category: select patron category list %]
26 [%# - sort1: select patron sort1 field %]
27 [%# - sort2: select patron sort2 field %]
28 [%# - search_field: select patron field list %]
29 [%# - search_type: select 'contains' or 'starts with' %]
30 [%- searchtype = searchtype || Koha.Preference('DefaultPatronSearchMethod') -%]
31 [% BLOCK patron_search_filters %]
32     <form method="get" class="patron_search_form">
33         <fieldset class="brief">
34             <h3>Search for patron</h3>
35             <ol>
36                 <li>
37                     <label for="search_patron_filter">Search:</label>
38                     <input type="text" class="search_patron_filter" value="[% search_filter | html %]" class="focus" />
39                 </li>
40
41                 [% FOR f IN filters %]
42                     [% SWITCH f %]
43                     [% CASE 'branch' %]
44                         <li>
45                             <label for="branchcode_filter">Library:</label>
46                             <select class="branchcode_filter">
47                                 [% SET libraries = Branches.all( only_from_group => 1 ) %]
48                                 [% IF libraries.size != 1 %]
49                                     <option value="">Any</option>
50                                 [% END %]
51                                 [% FOREACH l IN libraries %]
52                                     <option value="[% l.branchcode | html %]">[% l.branchname | html %]</option>
53                                 [% END %]
54                             </select>
55                         </li>
56                     [% CASE 'category' %]
57                         <li>
58                             <label for="categorycode_filter">Category:</label>
59                             <select class="categorycode_filter">
60                                 <option value="">Any</option>
61                                 [% FOREACH category IN Categories.limited.unblessed %]
62                                     <option value="[% category.categorycode | html %]">[% category.description | html %]</option>
63                                 [% END %]
64                             </select>
65                         </li>
66                     [% CASE 'sort1' %]
67                         <li>
68                             <label for="sort1_filter">Sort 1:</label>
69                             [% PROCESS 'av-build-dropbox.inc' no_id => 1, name="sort1_filter", category="Bsort1", empty=1, size = 20 %]
70                         </li>
71                     [% CASE 'sort2' %]
72                         <li>
73                             <label for="sort2_filter">Sort 2:</label>
74                             [% PROCESS 'av-build-dropbox.inc' no_id => 1, name="sort2_filter", category="Bsort2", empty=1, size = 20 %]
75                         </li>
76                     [% CASE 'search_field' %]
77                         <li>
78                             [% INCLUDE patron_fields_dropdown %]
79                         </li>
80                     [% CASE 'search_type' %]
81                         <li>
82                             <label for="searchtype_filter">Search type:</label>
83                             <select name="searchtype" class="searchtype_filter">
84                               [% IF searchtype == "starts_with" %]
85                                 <option value='starts_with' selected="selected">Starts with</option>
86                                 <option value="contains">Contains</option>
87                               [% ELSE %]
88                                 <option value='starts_with'>Starts with</option>
89                                 <option value="contains" selected="selected">Contains</option>
90                               [% END %]
91                             </select>
92                         </li>
93                     [% END %]
94                 [% END %]
95             </ol>
96             <fieldset class="action">
97                 <input type="submit" class="btn btn-primary" value="Search" />
98                 <input type="button" value="Clear" class="clear_search" />
99             </fieldset>
100         </fieldset>
101     </form>
102 [% END %]
103
104 [%# Display the table with: %]
105 [%# - At the top a hint about a possible filter %]
106 [%# - Browse by last name %]
107 [%# - The table %]
108 [%# Get the following parameters: %]
109 [%# - filter: can be 'suggestions_managers', 'orders_managers', 'funds_owners', 'funds_users' or 'erm_users' to filter patrons on their permissions %]
110 [%# - table_id: the ID of the table %]
111 [%# open_on_row_click: See patron_search_js %]
112 [%# columns: See patron_search_js %]
113 [% BLOCK patron_search_table %]
114
115     [% UNLESS table_id %]
116         [% SET table_id = "memberresultst" %]
117     [% END %]
118
119     [% IF filter == 'suggestions_managers' %]
120         <div class="hint">Only staff with superlibrarian or suggestions_manage permissions are returned in the search results</div>
121     [% ELSIF filter == 'orders_managers' OR filter == 'baskets_managers' %]
122         <div class="hint">Only staff with superlibrarian or acquisitions permissions (or order_manage permission if granular permissions are enabled) are returned in the search results</div>
123     [% ELSIF filter == 'funds_owners' OR filter == 'funds_users' %]
124         <div class="hint">Only staff with superlibrarian or acquisitions permissions (or budget_modify permission if granular permissions are enabled) are returned in the search results</div>
125     [% ELSIF filter == 'erm_users' %]
126         <div class="hint">Only staff with superlibrarian or ERM permissions are returned in the search results</div>
127     [% END %]
128
129     <div class="browse">
130         Browse by last name:
131         [% SET alphabet = Koha.Preference('alphabet').split(' ') %]
132         [% UNLESS alphabet.size %]
133             [% alphabet = ['A' .. 'Z'] %]
134         [% END %]
135         [% FOREACH letter IN alphabet %]
136             <a href="#" class="filterByLetter">[% letter | html %]</a>
137         [% END %]
138     </div>
139
140     <h3 style="display: none;">Patrons found for: <span class="searchpattern"></span></h3>
141
142     <div id="[% table_id | html %]_search_results" style="display:none;">
143
144         <div class="info" class="dialog message" style="display: none;"></div>
145         <div class="error" class="dialog alert" style="display: none;"></div>
146
147         <input type="hidden" class="firstletter_filter" value="" />
148         [% IF open_on_row_click %]
149         <table id="[% table_id | html %]" class="selections-table">
150         [% ELSE %]
151         <table id="[% table_id | html %]">
152         [% END %]
153             <thead>
154                 <tr>
155                     [% FOR column IN columns %]
156                         [% SWITCH column %]
157                             [% CASE 'checkbox' %]<th class="noExport"></th>
158                             [% CASE 'cardnumber' %]<th>Card</th>
159                             [% CASE 'dateofbirth' %]<th>Date of birth</th>
160                             [% CASE 'name' %]<th>Name</th>
161                             [% CASE 'name-address' %]<th>Name</th>
162                             [% CASE 'address' %]<th>Address</th>
163                             [% CASE 'address-library' %]<th>Address</th>
164                             [% CASE 'branch' %]<th data-filter="libraries">Library</th>
165                             [% CASE 'category' %]<th data-filter="categories">Category</th>
166                             [% CASE 'dateexpiry' %]<th>Expires on</th>
167                             [% CASE 'borrowernotes' %]<th>Notes</th>
168                             [% CASE 'phone' %]<th>Phone</th>
169                             [% CASE 'checkouts' %]<th>Checkouts</th>
170                             [% CASE 'account_balance' %]<th>Fines</th>
171                             [% CASE 'action' %]<th class="noExport">&nbsp;</th>
172                         [% END %]
173                     [% END %]
174                 </tr>
175               </thead>
176             <tbody></tbody>
177         </table>
178     </div>
179
180 <!-- Patron preview modal -->
181 <div class="modal" id="patronPreview" tabindex="-1" role="dialog" aria-labelledby="patronPreviewLabel">
182     <div class="modal-dialog" role="document">
183         <div class="modal-content">
184             <div class="modal-body">
185                 <div id="loading">
186                     <img src="[% interface | html %]/[% theme | html %]/img/spinner-small.gif" alt="" /> Loading
187                 </div>
188             </div>
189         </div>
190     </div>
191 </div>
192
193 [% END %]
194
195 [%# Integrate all the JS code, outside of a script tag %]
196 [%# Get the following parameters: %]
197 [%# - redirect_if_one_result: Redirect to the patron if the search returns only one result, note that it will not redirect if filters of the DT are used (this is a feature) %]
198 [%# - redirect_url: The URL to use, the borrowernumber parameter will be added %]
199 [%# - redirect_if_attribute_equal: Name of the attribute to use for the redirect. Query using this attribute, before the normal search %]
200 [%# filter: Same as patron_search_table %]
201 [%# open_on_row_click: boolean, default off. Will allow to select a patron by clicking on the whole tr element %]
202 [%# columns: list of columns that will be displayed. Possible values are: 'checkbox', 'cardnumber', 'dateofbirth', 'address', 'name', 'name-address', 'branch', 'category', 'dateexpiry', 'borrowernotes, 'phone', 'checkouts', 'account_balance', 'action' %]
203 [%# preview_on_name_click: Open a modal window with patron's info when the name is clicked %]
204 [%# actions: list of buttons to display in the action column. Possible values are: 'select', 'add', 'edit', 'checkout' %]
205 [%# sticky_header and sticky_to: If we need a sticky header %]
206 [%# callback: name of the JS function that will be called when a patron is selected. Only work with action=select %]
207 [%# display_search_description: boolean, default off. Display the description of the search %]
208 [%# adjust_history: boolean, default off. Change the current url when a first letter is selected %]
209 [% BLOCK patron_search_js %]
210
211     [% IF redirect_if_one_result && !redirect_url %]
212         <script>console.log("Wrong call of patron_search_js - missing redirect_url");</script>
213     [% END %]
214
215     <script>
216         [% SET libraries = Branches.all %]
217         [% SET categories = Categories.limited.unblessed %]
218         var categories = [% To.json(categories) | $raw %].map(e => {
219             e['_id'] = e.categorycode.toLowerCase();
220             e['_str'] = e.description;
221             return e;
222         });
223         var categories_map = categories.reduce((map, e) => {
224             map[e._id] = e;
225             return map;
226         }, {});
227         var libraries  = [% To.json(libraries) | $raw %].map(e => {
228             e['_id'] = e.branchcode;
229             e['_str'] = e.branchname;
230             return e;
231         });
232         var libraries_map = libraries.reduce((map, e) => {
233             map[e._id] = e;
234             return map;
235         }, {});
236
237         [% IF Koha.Preference('ExtendedPatronAttributes') %]
238             [% SET extended_attribute_types = [ ExtendedAttributeTypes.codes( staff_searchable => 1, searched_by_default => 1 ) ] %]
239             var extended_attribute_types = [% To.json(extended_attribute_types || []) | $raw %];
240         [% END %]
241
242         $(document).on("shown.bs.modal", function(){
243             $('select[name="sort1_filter"]').select2({allowClear:true});
244             $('select[name="sort2_filter"]').select2({allowClear:true});
245         }).on("hidden.bs.modal", function(){
246             ["sort1_filter", "sort2_filter"].forEach(function(item){
247                 if( $('select[name=' +  item + ']').data("select2") ){
248                     $('select[name=' +  item + ']').select2("destroy");
249                 }
250             });
251         });
252     </script>
253
254     [% INCLUDE 'datatables.inc' %]
255     [% INCLUDE 'js-patron-get-age.inc' %]
256     [% INCLUDE 'js-patron-format.inc' %]
257     [% INCLUDE 'js-patron-format-address.inc' %]
258     [% IF sticky_header %]
259         [% Asset.js("lib/hc-sticky.js") | $raw %]
260     [% END %]
261
262     <script>
263     {
264
265         function get_patron_search_form(){
266             let parent_block = $("#[% search_results_block_id | html %]");
267             let patron_search_form = parent_block.siblings(".patron_search_form");
268             if ( !patron_search_form.length ) patron_search_form = $(".patron_search_form");
269             return patron_search_form;
270         }
271         let first_draw = 0;
272         let patrons_table;
273         let Sticky;
274         let singleBranchMode = '[% singleBranchMode | html %]';
275         let logged_in_library_id = "[% Branches.GetLoggedInBranchcode | html %]";
276         [% IF do_not_defer_loading %]
277             let defer_loading = 0;
278         [% ELSE %]
279             let defer_loading = 1;
280         [% END %]
281
282         [% IF adjust_history %]
283             /* popstate event triggered by forward and back button. Need to refresh search */
284             window.addEventListener('popstate', (event) => {
285                 getSearchByLocation( false );
286             });
287         [% END %]
288
289         [% SWITCH filter %]
290         [% CASE 'suggestions_managers' %]
291             let patron_search_url = '/api/v1/suggestions/managers';
292         [% CASE 'baskets_managers' %]
293             let patron_search_url = '/api/v1/acquisitions/baskets/managers';
294         [% CASE 'funds_owners' %]
295             let patron_search_url = '/api/v1/acquisitions/funds/owners';
296         [% CASE 'funds_users' %]
297             let patron_search_url = '/api/v1/acquisitions/funds/users';
298         [% CASE 'erm_users' %]
299             let patron_search_url = '/api/v1/erm/users';
300         [% CASE %]
301             let patron_search_url = '/api/v1/patrons';
302         [% END %]
303         $(document).ready(function(){
304
305             let parent_block = $("#[% search_results_block_id | html %]");
306             let patron_search_form = get_patron_search_form();
307
308             parent_block.find(".info").hide();
309             parent_block.find(".error").hide();
310
311             // Build the aLengthMenu
312             let aLengthMenu = [
313                 [% Koha.Preference('PatronsPerPage') | html %], 10, 20, 50, 100, -1
314             ];
315             jQuery.unique(aLengthMenu);
316             aLengthMenu.sort(function( a, b ){
317                 // Put "All" at the end
318                 if ( a == -1 ) {
319                     return 1;
320                 } else if ( b == -1 ) {
321                     return -1;
322                 }
323                 return parseInt(a) < parseInt(b) ? -1 : 1;}
324             );
325             let aLengthMenuLabel = [];
326             $(aLengthMenu).each(function(){
327                 if ( this == -1 ) {
328                     // Label for -1 is "All"
329                     aLengthMenuLabel.push(_("All"));
330                 } else {
331                     aLengthMenuLabel.push(this);
332                 }
333             });
334
335             let additional_filters = {
336                 surname: function(){
337                     let start_with = parent_block.find(".firstletter_filter").val()
338                     if (!start_with) return "";
339                     return { "like": start_with + "%" }
340                 },
341                 "-and": function(){
342                     let filters = [];
343
344                     let search_type = patron_search_form.find(".searchtype_filter").val();
345                     let search_fields = patron_search_form.find(".searchfieldstype_filter").val() || "standard";
346                     let pattern = patron_search_form.find(".search_patron_filter").val();
347
348                     filters = buildPatronSearchQuery(
349                         pattern,
350                         {
351                             search_type: search_type,
352                             search_fields: search_fields,
353                             ...(typeof extended_attribute_types != 'undefined' && {extended_attribute_types: extended_attribute_types})
354                         }
355                     );
356
357                     let f_sort1 = patron_search_form.find("select[name='sort1_filter']").val();
358                     if ( f_sort1 ) {
359                         filters.push({
360                             "me.sort1": f_sort1
361                         });
362                     }
363                     let f_sort2 = patron_search_form.find("select[name='sort2_filter']").val();
364                     if ( f_sort2 ) {
365                         filters.push({
366                             "me.sort2": f_sort2
367                         });
368                     }
369
370                     return filters;
371                 }
372             };
373
374             [% UNLESS default_sort_column %]
375                 [% default_sort_column = "name" %]
376             [% END %]
377             [% SET order_column_index = 0 %]
378             [% SET embed = ['extended_attributes'] %]
379             patrons_table = $("#[% table_id | html %]").kohaTable({
380                 "ajax": {
381                     "url": patron_search_url,
382                     "dataSrc": function ( json ) {
383                         [% IF redirect_if_one_result %]
384                             // redirect if there is only 1 result.
385                             if ( first_draw && json.recordsFiltered == 1 ) {
386                                 let url = '[% redirect_url | url %]'.indexOf("?") != -1
387                                     ? '[% redirect_url | url %]&borrowernumber=' + json.data[0].patron_id
388                                     : '[% redirect_url | url %]?borrowernumber=' + json.data[0].patron_id;
389                                 document.location.href = url;
390                                 return false;
391                             }
392                             first_draw = 0;
393                         [% END %]
394                         return json.data;
395                     }
396                 },
397                 [% IF open_on_row_click OR preview_on_name_click OR remember_selections %]
398                 "drawCallback": function( settings ) {
399                     var api = this.api();
400                     var data = api.data();
401                     if ( data.length == 0 ) return;
402
403                     [% IF open_on_row_click %]
404                     $.each($(this).find("tbody tr"), function(index, tr) {
405                         let url = "[% on_click_url | url %]&borrowernumber=" + data[index].patron_id;
406                         $(tr).off('click').on('click', function() {
407                             document.location.href = url;
408                         }).addClass('clickable');
409                         $(tr).find("a.patron_name").attr('href', url);
410                     });
411                     [% END %]
412                     [% IF preview_on_name_click %]
413                     $.each($(this).find("tbody tr"), function(index, tr) {
414                         $(tr).find("a.patron_name").addClass("patron_preview");
415                     });
416                     [% END %]
417                     [% IF remember_selections %]
418                         prepSelections();
419                     [% END %]
420                 },
421                 [% END %]
422                 "deferLoading": defer_loading,
423                 "columns": [
424                     [% FOR column IN columns %]
425                         [% IF default_sort_column == column %]
426                             [% order_column_index = loop.count - 1%]
427                         [% END %]
428                         [% SWITCH column %]
429                             [% CASE 'checkbox' %]
430                             {
431                                 "data": "patron_id",
432                                 "searchable": false,
433                                 "orderable": false,
434                                 "render": function( data, type, row, meta ) {
435                                     return "<label for='check" + data + "' class='content_hidden'>" + _("Select patron") + "</label><input type='checkbox' class='check" + data + " selection' name='borrowernumber' value='" + data + "' />";
436                                 }
437                             }
438                             [% CASE 'cardnumber' %]
439                             {
440                                 "data": "cardnumber",
441                                 "searchable": true,
442                                 "orderable": true,
443                                 "render": function( data, type, row, meta ) {
444                                     let patron_id = encodeURIComponent(row.patron_id);
445                                     [% IF !open_on_row_click AND CAN_user_circulate_circulate_remaining_permissions %]
446                                         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>";
447                                     [% ELSE %]
448                                         return escape_str(data);
449                                     [% END %]
450                                 }
451
452                             }
453                             [% CASE 'dateofbirth' %]
454                             {
455                                 "data": "date_of_birth",
456                                 "type": "date",
457                                 "searchable": true,
458                                 "orderable": true,
459                                 "render": function( data, type, row, meta ) {
460                                     return data ? "<span class=\"dateofbirth\">" + escape_str($date(data)) + "<span class=\"agehint\"> (" + _("%s years").format($get_age(data)) + ")</span></span>" : "";
461                                 }
462                             }
463                             [% CASE 'address' %]
464                             {
465                                 "data": "me.street_number:me.address:me.address2:me.city:me.state:me.postal_code:me.country",
466                                 "searchable": true,
467                                 "orderable": true,
468                                  "render": function( data, type, row, meta ) {
469                                     let r = '<div class="address"><ul>';
470                                     r += $format_address(row, { no_line_break: true, include_li: true });
471                                     r += '</div></ul>';
472                                     return r;
473                                 }
474                             }
475                             [% CASE 'address-library' %]
476                             {
477                                 "data": "me.street_number:me.address:me.address2:me.city:me.state:me.postal_code:me.country",
478                                 "searchable": true,
479                                 "orderable": true,
480                                 "render": function( data, type, row, meta ) {
481                                     let r = '<div class="address"><ul>';
482                                     r += $format_address(row, { no_line_break: true, include_li: true });
483                                     r += '</div></ul>';
484                                     r += " " + escape_str(libraries_map[row.library_id].branchname);
485                                     return r;
486                                 }
487                             }
488                             [% CASE 'name-address' %]
489                             {
490                                 "data": "me.surname:me.firstname:me.middle_name:me.othernames:me.street_number:me.address:me.address2:me.city:me.state:me.postal_code:me.country",
491                                 "searchable": true,
492                                 "orderable": true,
493                                 "render": function( data, type, row, meta ) {
494                                     let patron_id = encodeURIComponent(row.patron_id);
495                                     let r = '';
496                                     [% IF ! open_on_row_click %]
497                                     r += "<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>";
498                                     [% ELSE %]
499                                     r += $patron_to_html(row, { invert_name: 1 });
500                                     [% END %]
501                                     r += '<br/>';
502                                     r += '<div class="address"><ul>';
503                                     r += $format_address(row, { no_line_break: true, include_li: true });
504
505                                     if ( row.email ) {
506                                         r += "<li>" + _("Email: ") + "<a href='mailto:" + encodeURIComponent(row.email) + "'>" + escape_str(row.email) + "</a></li>";
507                                     }
508                                     r += '</ul></div>'
509
510                                     return r;
511                                 }
512                             }
513                             [% CASE 'name' %]
514                             {
515                                 "data": "me.surname:me.firstname:me.middle_name:me.othernames",
516                                 "searchable": true,
517                                 "orderable": true,
518                                 "render": function( data, type, row, meta ) {
519                                     let patron_id = encodeURIComponent(row.patron_id);
520                                     [% IF ! open_on_row_click %]
521                                     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>";
522                                     [% ELSE %]
523                                     return $patron_to_html(row, { invert_name: 1 });
524                                     [% END %]
525                                 }
526                             }
527                             [% CASE 'branch' %]
528                             {
529                                 "data": "library_id",
530                                 "searchable": true,
531                                 "orderable": true,
532                                 "render": function( data, type, row, meta ) {
533                                     let library_name = libraries_map[data].branchname
534                                     if( !singleBranchMode && data == logged_in_library_id ) {
535                                         return "<span class=\"currentlibrary\">" + escape_str(library_name) + "</span>";
536                                     } else {
537                                         return escape_str(library_name);
538                                     }
539                                 }
540                             }
541                             [% CASE 'category' %]
542                             {
543                                 "data": "category_id",
544                                 "searchable": true,
545                                 "orderable": true,
546                                 "render": function( data, type, row, meta ) {
547                                     return escape_str(categories_map[data.toLowerCase()].description);
548                                 }
549                             }
550                             [% CASE 'dateexpiry' %]
551                             {
552                                 "data": "expiry_date",
553                                 "type": "date",
554                                 "searchable": true,
555                                 "orderable": true,
556                                 "render": function( data, type, row, meta ) {
557                                     return data ? escape_str($date(data)) : "";
558                                 }
559                             }
560                             [% CASE 'borrowernotes' %]
561                             {
562                                 "data": "staff_notes",
563                                 "searchable": true,
564                                 "orderable": true,
565                                 [%# We don't escape here, we allow html tag in staff notes %]
566                             }
567                             [% CASE 'phone' %]
568                             {
569                                 "data": "phone",
570                                 "searchable": true,
571                                 "orderable": true,
572                                 "render": function( data, type, row, meta ) {
573                                     return escape_str(data);
574                                 }
575                             }
576                             [% CASE 'checkouts' %][% embed.push('checkouts+count', 'overdues+count') %]
577                             {
578                                 "data": "",
579                                 "searchable": false,
580                                 "orderable": false,
581                                 "render": function( data, type, row, meta ) {
582                                     if ( row.overdues_count ) {
583                                         return "<span class='overdue'><strong>"+row.overdues_count + "</strong></span>" + " / " + row.checkouts_count;
584                                     } else {
585                                         return "0 / " + row.checkouts_count;
586                                     }
587                                 }
588                             }
589                             [% CASE 'account_balance' %][% embed.push('account_balance') %]
590                             {
591                                 "data": "",
592                                 "searchable": false,
593                                 "orderable": false,
594                                 "render": function( data, type, row, meta ) {
595                                     let r = "<span style='text-align: right; display: block;'><a href=\"/cgi-bin/koha/members/boraccount.pl?borrowernumber="+row.patron_id+"\">";
596                                     let balance_str = row.account_balance || 0;
597                                     balance_str = balance_str.escapeHtml().format_price();
598                                     if ( row.account_balance < 0 ) {
599                                         // FIXME Format price here
600                                         r += "<span class='credit'>" + balance_str + "</span>";
601                                     } else if ( row.account_balance > 0 ) {
602                                         r += "<span class='debit'><strong>" + balance_str  + "</strong></span>"
603                                     } else {
604                                         r += balance_str;
605                                     }
606                                     r += "</a></span>";
607                                     return r;
608                                 }
609                             }
610
611                             [% CASE 'action' %]
612                             {
613                                 "data": function( row, type, val, meta ) {
614
615                                     let patron_id = encodeURIComponent(row.patron_id);
616                                     let action_node = "";
617                                     [% FOR action IN actions %]
618                                     [% SWITCH action %]
619                                     [% CASE 'select' %]
620                                         action_node += '<a href="#" class="btn btn-default btn-xs select_user" data-borrowernumber="' + patron_id + '">' + _("Select") + '</a>';
621                                     [% CASE 'add' %]
622                                         action_node += '<a href="#" class="btn btn-default btn-xs add_user" data-borrowernumber="' + patron_id + '">' + _("Add") + '</a>';
623                                     [% CASE 'edit' %]
624                                         action_node += '<a href="/cgi-bin/koha/members/memberentry.pl?op=edit_form&amp;destination=circ&amp;borrowernumber=' + patron_id + '" class="btn btn-default btn-xs"><i class="fa-solid fa-pencil" aria-hidden="true"></i> ' + _("Edit") + '</a>';
625                                     [% CASE 'checkout' %]
626                                         [% IF CAN_user_circulate_circulate_remaining_permissions %]
627                                             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>';
628                                         [% END %]
629                                     [% END %]
630                                     [% END %]
631
632                                     let patron_str = JSON.stringify(row);
633                                     let input_node = $('<input type="hidden" name="borrower_data'+ patron_id + '"/>');
634                                     $(input_node).val(patron_str);
635                                     action_node += $(input_node).prop('outerHTML');
636
637                                     return action_node;
638                                 },
639                                 "searchable": false,
640                                 "orderable": false
641                             }
642                         [% END %]
643                         [% UNLESS loop.last %],[% END %]
644                     [% END %]
645                 ],
646                 'embed': [% To.json(embed) | $raw %],
647                 "order": [[ [% order_column_index | html %], "asc" ]],
648                 "autoWidth": false,
649                 'lengthMenu': [aLengthMenu, aLengthMenuLabel],
650                 "pagingType": 'full_numbers',
651                 "pageLength": [% Koha.Preference('PatronsPerPage') | html %],
652                 [% IF sticky_header %]
653                 "initComplete": function(settings, json) {
654                     $("#[% sticky_header | html %]").show();
655                     Sticky = $("#[% sticky_header | html %]");
656                     Sticky.hcSticky({
657                         stickTo: "#[% sticky_to | html %]",
658                         stickyClass: "floating"
659                     });
660                 },
661                 [% END %]
662                 fixedHeader: false,
663             }, typeof table_settings !== 'undefined' ? table_settings : null, 1, additional_filters);
664
665             patron_search_form.on('submit', filter);
666             patron_search_form.on('submit', update_search_type);
667             patron_search_form.on('submit', function(){
668                 parent_block.find(".searchheader").show();
669             });
670
671
672             $(".filterByLetter").on("click",function(e){
673                 e.preventDefault();
674                 filterByFirstLetterSurname($(this).text(), true);
675             });
676             patrons_table.on("click",".add_user",function(e){
677                 e.preventDefault();
678                 var borrowernumber = $(this).data("borrowernumber");
679                 var borrower_data = JSON.parse(patrons_table.find("input[name='borrower_data"+borrowernumber+"']").val());
680                 modal_add_user( borrowernumber, $patron_to_html( borrower_data, { display_cardnumber: false, url: false } ) );
681             });
682             patrons_table.on("click",".select_user",function(e){
683                 e.preventDefault();
684                 var borrowernumber = $(this).data("borrowernumber");
685                 var borrower_data = JSON.parse(patrons_table.find("input[name='borrower_data"+borrowernumber+"']").val());
686                 modal_select_user( borrowernumber, borrower_data );
687                 $(this).closest(".modal").modal('hide');
688             });
689
690             patrons_table.on("click",".patron_preview", function( e ){
691                 e.preventDefault();
692                 var borrowernumber = $(this).data("borrowernumber");
693                 var page = "/cgi-bin/koha/members/moremember.pl?print=brief&borrowernumber=" + borrowernumber;
694                 $("#patron_preview_modal").load( page + " div.container-fluid", function(){
695                     $("#patron_preview_modal").find(".close").on("click", function(){
696                         $('#patron_preview_modal').html(_("Loading...")).removeClass("show");
697                     });
698                 });
699                 $("#patron_preview_modal").addClass("show");
700             });
701
702             $("#patronPreview").on('hidden.bs.modal', function (e) {
703                 $("#patronPreview .modal-body").html("<img src=\"[% interface | html %]/[% theme | html %]/img/spinner-small.gif\" alt=\"\" /> Loading");
704             });
705
706             patron_search_form.find(".clear_search").on("click",function(e){
707                 e.preventDefault();
708                 clearFilters();
709                 patron_search_form.find(".searchpattern").parent().hide();
710             });
711
712             if ( !defer_loading ) {
713                 patron_search_form.submit();
714             }
715
716             [% IF adjust_history %]
717                 /* Initial page load does not trigger the popstate event, so we explicitly call this */
718                 getSearchByLocation( false );
719             [% END %]
720         });
721
722         [% IF adjust_history %]
723             function getSearchByLocation( setstate ){
724                 /* Check to see if the URL contains a search parameter */
725                 if( location.search != ""){
726                     var params = new URLSearchParams( location.search );
727                     var firstletter = params.get("firstletter");
728                     /* Check to see if search is a first letter param */
729                     if( firstletter ){
730                         /* Trigger function to return search results by letter */
731                         filterByFirstLetterSurname( firstletter, setstate );
732                     }
733                 }
734             }
735         [% END %]
736
737         function update_search_type(){
738             $("#searchtype").val($("#searchtype_filter").val());
739         }
740
741         function update_search_description(){
742             let parent_block = $("#[% search_results_block_id | html %]");
743             let patron_search_form = get_patron_search_form();
744             var searched = patron_search_form.find(".searchfieldstype_filter").find("option:selected").text();
745             let pattern = patron_search_form.find(".search_patron_filter").val();
746             if ( pattern ) {
747                 if ( patron_search_form.find(".searchtype_filter").val() == 'starts_with' ) {
748                     searched += _(" starting with ");
749                 } else {
750                     searched += _(" containing ");
751                 }
752                 searched += "'" + pattern + "'";
753             }
754             let firstletter_filter = parent_block.find(".firstletter_filter").val();
755             if ( firstletter_filter ) {
756                 searched += _(" begins with ") + "'" + firstletter_filter +"'";
757             }
758
759             if ( patron_search_form.find(".categorycode_filter").val() ) {
760                 searched += _(" with category ") + "'" + patron_search_form.find(".categorycode_filter option:selected").text() + "'";
761             }
762             if ( patron_search_form.find(".branchcode_filter").val() ) {
763                 searched += _(" in library ") + patron_search_form.find(".branchcode_filter option:selected").text();
764             }
765             if ( patron_search_form.find("select[name='sort1_filter']").val() ) {
766                 searched += _(" with sort1 ")
767                 if ( patron_search_form.find("select[name='sort1_filter']") ) {
768                     searched += patron_search_form.find("select[name='sort1_filter'] option:selected").text();
769                 }
770                 else {
771                     searched += paron_search_form.find("select[name='sort1_filter']").val();
772                 }
773             }
774             if ( patron_search_form.find("select[name='sort2_filter']").val() ) {
775                 searched += _(" with sort2 ")
776                 if ( patron_search_form.find("select[name='sort2_filter']") ) {
777                     searched += patron_search_form.find("select[name='sort2_filter'] option:selected").text();
778                 }
779                 else {
780                     searched += paron_search_form.find("select[name='sort2_filter']").val();
781                 }
782             }
783             patron_search_form.find(".searchpattern").text(searched);
784             patron_search_form.find(".searchpattern").parent().show();
785         }
786
787         function filter() {
788             let parent_block = $("#[% search_results_block_id | html %]");
789             let patron_search_form = get_patron_search_form();
790             [% IF redirect_if_attribute_equal %]
791                 let filter = patron_search_form.find(".search_patron_filter").val();
792                 if ( filter ) {
793                     $.ajax({
794                         data: { cardnumber: filter, _match: 'exact' },
795                         type: 'GET',
796                         url: patron_search_url,
797                         success: function(data) {
798                             if ( data.length == 1 ) {
799                                 let url = '[% redirect_url | url %]'.indexOf("?") != -1
800                                     ? '[% redirect_url | url %]&borrowernumber=' + data[0].patron_id
801                                     : '[% redirect_url | url %]?borrowernumber=' + data[0].patron_id;
802                                 document.location.href = url;
803                                 return false;
804                             }
805                         },
806                         error: function() {
807                             alert( _("An error occurred. Check the logs for details.") );
808                         }
809                     });
810                 }
811             [% END %]
812             parent_block.find(".firstletter_filter").val('');
813             $("#[% table_id | html %]_search_results").show();
814
815             let table_dt = patrons_table.DataTable();
816             [% FOR c IN columns %]
817                 [% SWITCH c %]
818                 [% CASE 'branch' %]
819                     let library_id = patron_search_form.find(".branchcode_filter").val() || "";
820                     patrons_table.find('thead tr:eq(1) th[data-filter="libraries"] select').val(library_id);
821                     table_dt.column([% loop.count - 1 %]).search(library_id ? '^'+library_id+'$' : '');
822                 [% CASE 'category' %]
823                     let category_id = patron_search_form.find(".categorycode_filter").val() || "";
824                     patrons_table.find('thead tr:eq(1) th[data-filter="categories"] select').val(category_id.toLowerCase());
825                     table_dt.column([% loop.count - 1 %]).search(category_id ? '^'+category_id+'$' : '');
826                 [% END %]
827             [% END %]
828             table_dt.search("");
829             first_draw = 1; // Only redirect if we are coming from here
830             table_dt.draw();
831             [% IF display_search_description %]
832                 update_search_description();
833             [% END %]
834             return false;
835         }
836
837         function clearFilters() {
838             let parent_block = $("#[% search_results_block_id | html %]");
839             let patron_search_form = get_patron_search_form();
840             patron_search_form.find(".searchfieldstype_filter option:first").prop("selected", true);
841             patron_search_form.find(".searchtype_filter option[value='[% searchtype | html %]']").prop("selected", true);
842             patron_search_form.find(".categorycode_filter option:first").prop("selected", true);
843             patron_search_form.find(".branchcode_filter option:first").prop("selected", true);
844             patron_search_form.find("select[name='sort1_filter']").val('').trigger("change");
845             patron_search_form.find("select[name='sort2_filter']").val('').trigger("change");
846             parent_block.find(".firstletter_filter").val('');
847             patron_search_form.find(".search_patron_filter").val('');
848             [% IF adjust_history %]
849                 /* remove any search string added by firstletter search */
850                 history.pushState( {}, null, window.location.href.split("?" )[0]);
851             [% END %]
852             $("#[% table_id | html %]_search_results").hide();
853             [% IF display_search_description %]
854                 update_search_description();
855             [% END %]
856         }
857
858         // User has clicked on a letter
859         function filterByFirstLetterSurname(letter, setstate ) {
860             let parent_block = $("#[% search_results_block_id | html %]");
861             parent_block.find(".firstletter_filter").val(letter);
862
863             $("#[% table_id | html %]_search_results").show();
864
865             [% IF adjust_history %]
866                 if ( setstate ) {
867                     history.pushState( null, null, "?firstletter=" + letter );
868                 }
869             [% END %]
870
871             patrons_table.DataTable().draw();
872             [% IF display_search_description %]
873                 update_search_description();
874             [% END %]
875         }
876
877         // modify parent window owner element
878         function modal_add_user(borrowernumber, borrowername) {
879             [%# Note that add_user could sent data instead of borrowername too %]
880             let parent_block = $("#[% search_results_block_id | html %]");
881             parent_block.find(".info").hide();
882             parent_block.find(".error").hide();
883             if ( add_user(borrowernumber, borrowername) < 0 ) {
884                 parent_block.find(".error").html(_("Patron '%s' is already in the list.").format(borrowername)).show();
885             } else {
886                 parent_block.find(".info").html(_("Patron '%s' added.").format(borrowername)).show();
887             }
888         }
889         function modal_select_user(borrowernumber, data) {
890             if ( document.getElementById("selected_patron_id") ) {
891                 document.getElementById("selected_patron_id").value = borrowernumber;
892             } else {
893                 [% IF callback %]
894                     [% callback | html %](borrowernumber, data);
895                 [% ELSE %]
896                     select_user(borrowernumber, data);
897                 [% END %]
898             }
899         }
900     }
901     </script>
902 [% END %]
903
904
905 [% BLOCK patron_search_modal %]
906     [% UNLESS patron_search_modal_id %]
907         [% patron_search_modal_id = "patron_search_modal" %]
908     [% END %]
909     [% UNLESS table_id %]
910         [% table_id = "memberresultst" %]
911     [% END %]
912
913     [% search_results_block_id = patron_search_modal_id _ '_searchresults' %]
914
915     <div id="[% patron_search_modal_id | html %]" class="modal modal-full" tabindex="-1" role="dialog" aria-labelledby="patronSearchLabel" aria-hidden="true" data-backdrop="">
916         <div class="modal-dialog" role="document">
917             <div class="modal-content">
918                 <div class="modal-header">
919                     <button type="button" class="closebtn" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
920                     <h4 class="modal-title" id="patronSearchLabel">[% modal_title | html %]</h4>
921                 </div>
922                 <div class="modal-body">
923                     [% PROCESS patron_search_filters filters => ['branch','category','sort1','sort2'] %]
924
925                     <div id="[% search_results_block_id | html %]"> <!-- FIXME removed style from #searchresults, is that bad? -->
926                         [% IF columns.grep('checkbox').size %]
927                             <div class="searchheader fh-fixedHeader" style="display:none;">
928                                 <div>
929                                     <a href="#" class="btn btn-link select_all"><i class="fa fa-check"></i> Select all</a>
930                                     |
931                                     <a href="#" class="btn btn-link clear_all"><i class="fa fa-remove"></i> Clear all</a>
932                                     <button class="add-selected" class="btn btn-sm btn-default" type="submit">Add selected patrons</button>
933                                 </div>
934                             </div>
935                         [% END %]
936                         [% PROCESS patron_search_table table_id => table_id, columns => columns %]
937                     </div>
938                 </div>
939                 <div class="modal-footer">
940                     <a href="#" class="btn btn-default cancel" data-dismiss="modal" aria-hidden="true">Close</a>
941                 </div>
942             </div>
943         </div>
944     </div>
945     <div id="patron_preview_modal" class="basicModal"></div>
946
947     <script>
948         $(document).ready(function() {
949             let parent_block = $("#[% search_results_block_id | html %]");
950             parent_block.find(".select_all").on("click",function(e){
951                 e.preventDefault();
952                 parent_block.find(".selection").prop("checked", true).change();
953             });
954             parent_block.find(".clear_all").on("click",function(e){
955                 e.preventDefault();
956                 parent_block.find(".selection").prop("checked", false).change();
957             });
958             parent_block.find(".searchheader").hide();
959             parent_block.find(".clear_search").on("click",function(e){$("#searchheader").hide();});
960
961             parent_block.find('.add-selected').on('click', function(e) {
962                 e.preventDefault();
963                 var counter = 0;
964                 parent_block.find('tr:has(.selection:checked) .add_user').each(function(){
965                     var borrowernumber = $(this).data('borrowernumber');
966                     var firstname = $(this).data('firstname');
967                     var surname = $(this).data('surname');
968                     add_user( borrowernumber, firstname + ' ' + surname );
969                     counter++;
970                 });
971                 parent_block.find('.info').html(_("%s Patrons added.").format(counter)).show();
972             });
973         });
974     </script>
975 [% END %]