8 [% BLOCK patron_search_filters_simple %]
9 <form id="patron_search_form">
10 <div class="hint">Enter patron card number or partial name:</div>
11 <input type="text" size="40" id="search_patron_filter" class="focus" autocomplete="off" />
12 <input type="submit" value="Search" />
16 [% BLOCK patron_search_filters %]
17 <form id="patron_search_form">
18 <fieldset class="brief">
19 <h3>Search for patron</h3>
22 <label for="search_patron_filter">Search:</label>
23 <input type="text" id="search_patron_filter" value="[% search_filter | html %]" class="focus" />
26 [% FOR f IN filters %]
30 <label for="branchcode_filter">Library:</label>
31 <select id="branchcode_filter">
32 [% SET libraries = Branches.all( only_from_group => 1 ) %]
33 [% IF libraries.size != 1 %]
34 <option value="">Any</option>
36 [% FOREACH l IN libraries %]
37 <option value="[% l.branchcode | html %]">[% l.branchname | html %]</option>
43 <label for="categorycode_filter">Category:</label>
44 <select id="categorycode_filter">
45 <option value="">Any</option>
46 [% FOREACH category IN categories %]
47 <option value="[% category.categorycode | html %]">[% category.description | html %]</option>
51 [% CASE 'search_field' %]
53 <label for="searchfieldstype_filter">Search field:</label>
54 <select name="searchfieldstype" id="searchfieldstype_filter">
55 [% pref_fields = Koha.Preference('DefaultPatronSearchFields').split(',') %]
56 [% default_fields = [ 'surname,firstname,othernames,cardnumber,userid', 'surname', 'cardnumber', 'email', 'borrowernumber', 'userid', 'phone', 'address', 'dateofbirth', 'sort1', 'sort2' ] %]
57 [% search_options = default_fields.merge(pref_fields).unique %]
58 [% FOREACH s_o IN search_options %]
59 [% display_name = PROCESS patron_fields name=s_o %]
60 [% NEXT IF !display_name %]
61 [% IF searchfieldstype == s_o %]
62 <option selected="selected" value=[% s_o | html %]>[% display_name | $raw %]</option>
64 <option value=[% s_o | html %]>[% display_name | $raw %]</option>
69 [% CASE 'search_type' %]
71 <label for="searchtype_filter">Search type:</label>
72 <select name="searchtype" id="searchtype_filter">
73 [% IF searchtype == "start_with" %]
74 <option value='start_with' selected="selected">Starts with</option>
75 <option value="contain">Contains</option>
77 <option value='start_with'>Starts with</option>
78 <option value="contain" selected="selected">Contains</option>
85 <fieldset class="action">
86 <input type="submit" value="Search" />
87 <input type="button" value="Clear" id="clear_search" />
93 [% BLOCK patron_search_table %]
95 [% IF filter == 'suggestions_managers' %]
96 <div class="hint">Only staff with superlibrarian or suggestions_manage permissions are returned in the search results</div>
97 [% ELSIF filter == 'orders_managers' %]
98 <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>
99 [% ELSIF filter == 'funds_owners' OR filter == 'funds_users' %]
100 <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>
105 [% SET alphabet = Koha.Preference('alphabet').split(' ') %]
106 [% UNLESS alphabet.size %]
107 [% alphabet = ['A' .. 'Z'] %]
109 [% FOREACH letter IN alphabet %]
110 <a href="#" class="filterByLetter">[% letter | html %]</a>
115 <h3 style="display: none;">Patrons found for: <span id="searchpattern"></span></h3>
117 <div id="[% table_id | html %]_search_results" style="display:none;">
119 <div id="info" class="dialog message" style="display: none;"></div>
120 <div id="error" class="dialog alert" style="display: none;"></div>
122 <input type="hidden" id="firstletter_filter" value="" />
123 [% IF open_on_row_click %]
124 <table id="[% table_id | html %]" class="selections-table">
126 <table id="[% table_id | html %]">
130 [% FOR column IN columns %]
132 [% CASE 'checkbox' %]<th class="noExport"></th>
133 [% CASE 'cardnumber' %]<th>Card</th>
134 [% CASE 'dateofbirth' %]<th>Date of birth</th>
135 [% CASE 'name' %]<th>Name</th>
136 [% CASE 'name-address' %]<th>Name</th>
137 [% CASE 'address' %]<th>Address</th>
138 [% CASE 'address-library' %]<th>Address</th>
139 [% CASE 'branch' %]<th data-filter="libraries">Library</th>
140 [% CASE 'category' %]<th data-filter="categories">Category</th>
141 [% CASE 'dateexpiry' %]<th>Expires on</td>
142 [% CASE 'borrowernotes' %]<th>Notes</th>
143 [% CASE 'phone' %]<th>Phone</th>
144 [% CASE 'checkouts' %]<th>Checkouts</th>
145 [% CASE 'account_balance' %]<th>Fines</th>
146 [% CASE 'action' %]<th> </th>
155 <!-- Patron preview modal -->
156 <div class="modal" id="patronPreview" tabindex="-1" role="dialog" aria-labelledby="patronPreviewLabel">
157 <div class="modal-dialog" role="document">
158 <div class="modal-content">
159 <div class="modal-header">
160 <button type="button" class="closebtn" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
161 <h4 class="modal-title" id="patronPreviewLabel"></h4>
163 <div class="modal-body">
165 <img src="[% interface | html %]/[% theme | html %]/img/spinner-small.gif" alt="" /> Loading
168 <div class="modal-footer">
169 <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
177 [% BLOCK patron_search_js %]
179 [% IF redirect_if_one_result && !redirect_url %]
180 <script>console.log("Wrong call of patron_searh_js - missing redirect_url");</script>
183 let categories = [% To.json(categories) | $raw %];
184 let categories_map = categories.reduce((map, c) => {
185 map[c.categorycode] = c;
188 let libraries = [% To.json(libraries) | $raw %];
189 let libraries_map = libraries.reduce((map, l) => {
190 map[l.branchcode] = l;
194 [% IF Koha.Preference('ExtendedPatronAttributes') && extended_attribute_types %]
195 let extended_attribute_types = [% To.json(extended_attribute_types || []) | $raw %];
200 [% INCLUDE 'datatables.inc' %]
201 [% INCLUDE 'js-date-format.inc' %]
202 [% INCLUDE 'js-patron-get-age.inc' %]
203 [% INCLUDE 'js-patron-format.inc' %]
204 [% INCLUDE 'js-patron-format-address.inc' %]
205 [% IF sticky_header %]
206 [% Asset.js("lib/hc-sticky.js") | $raw %]
213 var singleBranchMode = '[% singleBranchMode | html %]';
214 let logged_in_library_id = "[% Branches.GetLoggedInBranchcode | html %]";
216 /* popstate event triggered by forward and back button. Need to refresh search */
217 window.addEventListener('popstate', (event) => {
218 getSearchByLocation( false );
221 $(document).ready(function(){
226 // Build the aLengthMenu
228 [% Koha.Preference('PatronsPerPage') | html %], 10, 20, 50, 100, -1
230 jQuery.unique(aLengthMenu);
231 aLengthMenu.sort(function( a, b ){
232 // Put "All" at the end
235 } else if ( b == -1 ) {
238 return parseInt(a) < parseInt(b) ? -1 : 1;}
240 var aLengthMenuLabel = [];
241 $(aLengthMenu).each(function(){
243 // Label for -1 is "All"
244 aLengthMenuLabel.push(_("All"));
246 aLengthMenuLabel.push(this);
250 let additional_filters = {
252 let start_with = $("#firstletter_filter").val()
253 if (!start_with) return "";
254 return { "like": start_with + "%" }
257 let filter = $("#search_patron_filter").val();
258 if (!filter) return "";
261 let search_type = $("#searchtype_filter").val() || "contain";
262 let search_fields = $("#searchfieldstype_filter").val();
263 if ( !search_fields ) {
264 search_fields = "[% Koha.Preference('DefaultPatronSearchFields') || 'surname,firstname,othernames,cardnumber,userid' | html %]";
266 search_fields.split(',').forEach(function(e,i){
267 filters.push({["me."+e]:{"like":"%"+filter+(search_type == "contain" ? "%" : "" )}});
269 [% IF Koha.Preference('ExtendedPatronAttributes') && extended_attribute_types %]
271 "extended_attributes.value": { "like": "%" + filter + (search_type == "contain" ? "%" : "" )},
272 "extended_attributes.code": extended_attribute_types
278 [% UNLESS default_sort_column %]
279 [% default_sort_column = "name" %]
281 [% SET order_column_index = 0 %]
282 [% SET embed = ['extended_attributes'] %]
283 patrons_table = $("#[% table_id | html %]").kohaTable({
286 [% CASE 'suggestions_managers' %]
287 "url": '/api/v1/suggestions/managers',
288 [% CASE 'baskets_managers' %]
289 "url": '/api/v1/acquisitions/baskets/managers',
290 [% CASE 'funds_owners' %]
291 "url": '/api/v1/acquisitions/funds/owners',
292 [% CASE 'funds_users' %]
293 "url": '/api/v1/acquisitions/funds/users',
295 "url": '/api/v1/patrons',
297 "dataSrc": function ( json ) {
298 [% IF redirect_if_one_result %]
299 // redirect if there is only 1 result.
300 if ( first_draw && json.recordsFiltered == 1 ) {
301 let url = '[% redirect_url | url %]'.indexOf("?") != -1
302 ? '[% redirect_url | url %]&borrowernumber=' + json.data[0].patron_id
303 : '[% redirect_url | url %]?borrowernumber=' + json.data[0].patron_id;
304 document.location.href = url;
312 [% IF open_on_row_click OR preview_on_name_click %]
313 "drawCallback": function( settings ) {
314 var api = this.api();
315 var data = api.data();
316 if ( data.length == 0 ) return;
318 [% IF open_on_row_click %]
319 $.each($(this).find("tbody tr"), function(index, tr) {
320 let url = "[% on_click_url | url %]&borrowernumber=" + data[index].patron_id;
321 $(tr).off('click').on('click', function() {
322 document.location.href = url;
323 }).addClass('clickable');
324 $(tr).find("a.patron_name").attr('href', url);
327 [% IF preview_on_name_click %]
328 $.each($(this).find("tbody tr"), function(index, tr) {
329 $(tr).find("a.patron_name").addClass("patron_preview");
336 [% FOR column IN columns %]
337 [% IF default_sort_column == column %]
338 [% order_column_index = loop.count - 1%]
341 [% CASE 'checkbox' %]
346 "render": function( data, type, row, meta ) {
347 return "<label for='check" + data + "' class='content_hidden'>" + _("Select patron") + "</label><input type='checkbox' id='check" + data + "' class='selection' name='borrowernumber' value='" + data + "' />";
350 [% CASE 'cardnumber' %]
352 "data": "cardnumber",
355 "render": function( data, type, row, meta ) {
356 let patron_id = encodeURIComponent(row.patron_id);
357 [% IF !open_on_row_click AND CAN_user_circulate_circulate_remaining_permissions %]
358 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>";
360 return escape_str(data);
365 [% CASE 'dateofbirth' %]
367 "data": "date_of_birth",
370 "render": function( data, type, row, meta ) {
371 return data ? escape_str($date(data) + " (" + _("%s years").format($get_age(data)) + ")") : "";
376 "data": "me.street_number:me.address:me.address2:me.city:me.state:me.postal_code:me.country",
379 "render": function( data, type, row, meta ) {
380 let r = '<div class="address"><ul>';
381 r += $format_address(row, { no_line_break: 1 });
386 [% CASE 'address-library' %]
388 "data": "me.street_number:me.address:me.address2:me.city:me.state:me.postal_code:me.country",
391 "render": function( data, type, row, meta ) {
392 let r = '<div class="address"><ul>';
393 r += $format_address(row, { no_line_break: 1 });
395 r += " " + escape_str(libraries_map[row.library_id].branchname);
399 [% CASE 'name-address' %]
401 "data": "me.firstname:me.surname:me.othernames:me.street_number:me.address:me.address2:me.city:me.state:me.postal_code:me.country",
404 "render": function( data, type, row, meta ) {
405 let patron_id = encodeURIComponent(row.patron_id);
407 [% IF ! open_on_row_click %]
408 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>";
410 r += $patron_to_html(row, { invert_name: 1 });
413 r += '<div class="address"><ul>';
414 r += $format_address(row, { no_line_break: 1 });
417 r += "<li>" + _("Email: ") + "<a href='mailto:" + encodeURIComponent(row.email) + "'>" + escape_str(row.email) + "</a></li>";
424 [% CASE 'name-address' %]
426 "data": "me.firstname:me.surname:me.othernames:me.street_number:me.address:me.address2:me.city:me.state:me.postal_code:me.country",
429 "render": function( data, type, row, meta ) {
430 let patron_id = encodeURIComponent(row.patron_id);
432 [% IF ! open_on_row_click %]
433 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>";
435 r += $patron_to_html(row, { invert_name: 1 });
438 r += '<div class="address"><ul>';
439 r += $format_address(row, { no_line_break: 1 });
442 r += "<li>" + _("Email: ") + "<a href='mailto:" + encodeURIComponent(row.email) + "'>" + escape_str(row.email) + "</a></li>";
451 "data": "me.firstname:me.surname:me.othernames",
454 "render": function( data, type, row, meta ) {
455 let patron_id = encodeURIComponent(row.patron_id);
456 [% IF ! open_on_row_click %]
457 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>";
459 return $patron_to_html(row, { invert_name: 1 });
465 "data": "library_id",
468 "render": function( data, type, row, meta ) {
469 let library_name = libraries_map[data].branchname
470 if( !singleBranchMode && data == logged_in_library_id ) {
471 return "<span class=\"currentlibrary\">" + escape_str(library_name) + "</span>";
473 return escape_str(library_name);
477 [% CASE 'category' %]
479 "data": "category_id",
482 "render": function( data, type, row, meta ) {
483 return escape_str(categories_map[data].description);
486 [% CASE 'dateexpiry' %]
488 "data": "expiry_date",
491 "render": function( data, type, row, meta ) {
492 return data ? escape_str($date(data)) : "";
495 [% CASE 'borrowernotes' %]
497 "data": "staff_notes",
500 [%# We don't escape here, we allow html tag in staff notes %]
507 "render": function( data, type, row, meta ) {
508 return escape_str(data);
511 [% CASE 'checkouts' %][% embed.push('checkouts+count', 'overdues+count') %]
516 "render": function( data, type, row, meta ) {
517 if ( row.overdues_count ) {
518 return "<span class='overdue'><strong>"+row.overdues_count + "</strong></span>";
520 return "0 / " + row.checkouts_count;
524 [% CASE 'account_balance' %][% embed.push('account_balance') %]
529 "render": function( data, type, row, meta ) {
530 let r = "<span style='text-align: right; display: block;'><a href=\"/cgi-bin/koha/members/boraccount.pl?borrowernumber="+row.patron_id+"\">";
531 let balance_str = row.account_balance || 0;
532 balance_str = balance_str.escapeHtml().format_price();
533 if ( row.account_balance < 0 ) {
534 // FIXME Format price here
535 r += "<span class='credit'>" + balance_str + "</span>";
536 } else if ( row.account_balance > 0 ) {
537 r += "<span class='debit'><strong>" + balance_str + "</strong></span>"
548 "data": function( row, type, val, meta ) {
550 let patron_id = encodeURIComponent(row.patron_id);
551 let action_node = "";
552 [% FOR action IN actions %]
555 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)+'\' />';
557 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 + '" />';
559 action_node += '<a href="/cgi-bin/koha/members/memberentry.pl?op=modify&destination=circ&borrowernumber=' + patron_id + '" class="btn btn-default btn-xs"><i class="fa fa-pencil"></i> Edit</a>';
560 [% CASE 'checkout' %]
561 [% IF CAN_user_circulate_circulate_remaining_permissions %]
562 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>';
572 [% UNLESS loop.last %],[% END %]
575 'embed': [% To.json(embed) | $raw %],
576 "order": [[ [% order_column_index | html %], "asc" ]],
578 'lengthMenu': [aLengthMenu, aLengthMenuLabel],
579 'sPaginationType': 'full_numbers',
580 "pageLength": [% Koha.Preference('PatronsPerPage') | html %],
581 [% IF sticky_header %]
582 "initComplete": function(settings, json) {
583 Sticky = $("#[% sticky_header | html %]");
585 stickTo: "#[% sticky_to | html %]",
586 stickyClass: "floating"
590 }, typeof table_settings !== 'undefined' ? table_settings : null, 1, additional_filters);
592 $("#patron_search_form").on('submit', filter);
593 $(".filterByLetter").on("click",function(e){
595 filterByFirstLetterSurname($(this).text(), true);
597 $("body").on("click",".add_user",function(e){
599 var borrowernumber = $(this).data("borrowernumber");
600 var firstname = $(this).data("firstname");
601 var surname = $(this).data("surname");
602 add_user( borrowernumber, firstname + " " + surname );
605 $("body").on("click",".select_user",function(e){
607 var borrowernumber = $(this).data("borrowernumber");
608 var borrower_data = $("#borrower_data"+borrowernumber).val();
609 select_user( borrowernumber, JSON.parse(borrower_data) );
612 $("body").on("click",".patron_preview", function( e ){
614 var borrowernumber = $(this).data("borrowernumber");
615 var page = "/cgi-bin/koha/members/moremember.pl?print=brief&borrowernumber=" + borrowernumber;
616 $("#patronPreview .modal-body").load( page + " div.container-fluid" );
617 $('#patronPreview').modal({show:true});
620 $("#patronPreview").on('hidden.bs.modal', function (e) {
621 $("#patronPreview .modal-body").html("<img src=\"[% interface | html %]/[% theme | html %]/img/spinner-small.gif\" alt=\"\" /> Loading");
624 $("#clear_search").on("click",function(e){
627 $("#searchpattern").parent().hide();
630 if ( $("#search_patron_filter").val().length > 0 ) {
631 $("#patron_search_form").submit();
634 /* Initial page load does not trigger the popstate event, so we explicitly call this */
635 getSearchByLocation( false );
639 function getSearchByLocation( setstate ){
640 /* Check to see if the URL contains a search parameter */
641 if( location.search != ""){
642 var params = new URLSearchParams( location.search );
643 var firstletter = params.get("firstletter");
644 /* Check to see if search is a first letter param */
646 /* Trigger function to return search results by letter */
647 filterByFirstLetterSurname( firstletter, setstate );
652 function update_search_description(){
653 var searched = $("#searchfieldstype_filter").find("option:selected").text();
654 if ( $("#search_patron_filter").val() ) {
655 if ( $("#searchtype_filter").val() == 'start_with' ) {
656 searched += _(" starting with ");
658 searched += _(" containing ");
660 searched += "'" + $("#search_patron_filter").val() + "'";
662 if ( $("#firstletter_filter").val() ) {
663 searched += _(" begins with ") + "'" + $("#firstletter_filter").val() +"'";
665 if ( $("#categorycode_filter").val() ) {
666 searched += _(" with category ") + "'" + $("#categorycode_filter").find("option:selected").text() + "'";
668 if ( $("#branchcode_filter").val() ) {
669 searched += _(" in library ") + $("#branchcode_filter").find("option:selected").text();
671 $("#searchpattern").text(searched);
672 $("#searchpattern").parent().show();
676 $("#firstletter_filter").val('');
677 $("#[% table_id | html %]_search_results").show();
679 let table_dt = patrons_table.DataTable();
680 [% FOR c IN columns %]
683 library_id = $("#branchcode_filter").val() || "";
684 patrons_table.find('thead tr:eq(1) th[data-filter="libraries"] select').val(library_id);
685 table_dt.column([% loop.count - 1 %]).search(library_id ? '^'+library_id+'$' : '');
686 [% CASE 'category' %]
687 let category_id = $("#categorycode_filter").val() || "";
688 patrons_table.find('thead tr:eq(1) th[data-filter="categories"] select').val(category_id);
689 table_dt.column([% loop.count - 1 %]).search(category_id ? '^'+category_id+'$' : '');
693 first_draw = 1; // Only redirect if we are coming from here
695 update_search_description();
699 function clearFilters() {
700 $("#searchfieldstype_filter option:first").prop("selected", true);
701 $("#searchtype_filter option[value='contain']").prop("selected", true);
702 $("#categorycode_filter option:first").prop("selected", true);
703 $("#branchcode_filter option:first").prop("selected", true);
704 $("#firstletter_filter").val('');
705 $("#search_patron_filter").val('');
706 /* remove any search string added by firstletter search */
707 history.pushState( {}, null, window.location.href.split("?" )[0]);
708 $("#[% table_id | html %]_search_results").hide();
709 update_search_description();
712 // User has clicked on a letter
713 function filterByFirstLetterSurname(letter, setstate ) {
714 $("#firstletter_filter").val(letter);
716 $("#[% table_id | html %]_search_results").show();
719 history.pushState( null, null, "?firstletter=" + letter );
722 patrons_table.DataTable().draw();
723 update_search_description();
726 // modify parent window owner element
727 function add_user(borrowernumber, borrowername) {
728 var p = window.opener;
729 // In one place (serials/routing.tt), the page is reload on every add
730 // We have to wait for the page to be there
731 function wait_for_opener () {
732 if ( ! $(opener.document).find('body').size() ) {
733 setTimeout(wait_for_opener, 500);
735 [%# Note that add_user could sent data instead of borrowername too %]
738 if ( p.add_user(borrowernumber, borrowername) < 0 ) {
739 $("#error").html(_("Patron '%s' is already in the list.").format(borrowername));
742 $("#info").html(_("Patron '%s' added.").format(borrowername));
749 function select_user(borrowernumber, data) {
750 var p = window.opener;
752 p.[% callback | html %](borrowernumber, data);
754 p.select_user(borrowernumber, data);