LP#1712854: Disable all server-side sorting, but provide a stub for later, if we...
[Evergreen.git] / Open-ILS / web / js / ui / default / staff / circ / holds / app.js
1 angular.module('egHoldsApp', 
2     ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
3
4 .config(function($routeProvider, $locationProvider, $compileProvider) {
5     $locationProvider.html5Mode(true);
6     $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
7
8     var resolver = {delay : 
9         ['egStartup', function(egStartup) {return egStartup.go()}]}
10
11     $routeProvider.when('/circ/holds/shelf', {
12         templateUrl: './circ/holds/t_shelf',
13         controller: 'HoldsShelfCtrl',
14         resolve : resolver
15     });
16
17     $routeProvider.when('/circ/holds/shelf/:hold_id', {
18         templateUrl: './circ/holds/t_shelf',
19         controller: 'HoldsShelfCtrl',
20         resolve : resolver
21     });
22
23     $routeProvider.when('/circ/holds/pull', {
24         templateUrl: './circ/holds/t_pull',
25         controller: 'HoldsPullListCtrl',
26         resolve : resolver
27     });
28
29     $routeProvider.when('/circ/holds/pull/:hold_id', {
30         templateUrl: './circ/holds/t_pull',
31         controller: 'HoldsPullListCtrl',
32         resolve : resolver
33     });
34
35     $routeProvider.otherwise({redirectTo : '/circ/holds/shelf'});
36 })
37
38
39 .controller('HoldsShelfCtrl',
40        ['$scope','$q','$routeParams','$window','$location','egCore','egHolds','egHoldGridActions','egCirc','egGridDataProvider','egProgressDialog',
41 function($scope , $q , $routeParams , $window , $location , egCore , egHolds , egHoldGridActions , egCirc , egGridDataProvider , egProgressDialog)  {
42     $scope.detail_hold_id = $routeParams.hold_id;
43
44     var holds = [];
45     var clear_mode = false;
46     $scope.gridControls = {};
47     $scope.grid_actions = egHoldGridActions;
48
49     var provider = egGridDataProvider.instance({});
50     $scope.gridDataProvider = provider;
51
52     function refresh_page() {
53         hold_count = 0;
54         holds = [];
55         all_holds = [];
56         provider.refresh();
57     }
58     // called after any egHoldGridActions action occurs
59     $scope.grid_actions.refresh = refresh_page;
60
61     provider.get = function(offset, count) {
62
63         // if in clear mode...
64         if (clear_mode && holds.length) {
65             if (!all_holds.legnth) all_holds = holds;
66             holds = holds.filter(function(h) { return h.hold.clear_me });
67             hold_count = holds.length;
68             return provider.arrayNotifier(holds, offset, count);
69         } else if (all_holds.length) {
70             holds = all_holds;
71             hold_count = holds.length;
72             all_holds = [];
73         }
74
75         // see if we have the requested range cached
76         if (holds[offset]) {
77             return provider.arrayNotifier(holds, offset, count);
78         }
79
80         hold_count = 0;
81         holds = [];
82         var restrictions = {
83                 is_staff_request  : 'true',
84                 last_captured_hold: 'true',
85                 capture_time      : { not : null },
86                 cs_id             : 8, // on holds shelf
87                 cp_deleted        : 'f',
88                 fulfillment_time  : null,
89                 current_shelf_lib : $scope.pickup_ou.id()
90         };
91
92         var order_by = [{ shelf_expire_time : null }];
93
94         // NOTE: Server sorting is currently disabled entirely by the 
95         // first clause in this 'if'.   This is perfectly fine because
96         // clientsort always runs inside the arrayNotifier implementation
97         // in the egGrid code.   However, in order to retain the memory
98         // of sorting constraints placed on us by the current server-side
99         // code, an initial "cannot sort these" array and test is added
100         // here.  An alternate implementation might be to map fields to
101         // query positions, thus allowing positional ORDER BY clauses.
102         // With as many fields as the wide hold object has, this is
103         // non-trivial at the moment.
104         if (false && provider.sort && provider.sort.length) {
105             // A list of fields we can't sort on the server side.  That's ok, because
106             // the grid is marked clientsort, so it always re-sorts in the browser.
107             var cannot_sort = [
108                 'relative_queue_position',
109                 'default_estimated_wait',
110                 'min_estimated_wait',
111                 'potentials',
112                 'other_holds',
113                 'total_wait_time',
114                 'notification_count',
115                 'last_notification_time',
116                 'is_staff_hold',
117                 'copy_location_order_position',
118                 'hold_status',
119                 'clear_me',
120                 'usr_alias_or_display_name',
121                 'usr_display_name',
122                 'usr_alias_or_first_given_name'
123             ];
124
125             order_by = [];
126             angular.forEach(provider.sort, function (c) {
127                 if (!angular.isObject(c)) {
128                     if (c.match(/^hold\./)) {
129                         var i = c.replace('hold.','');
130                         if (cannot_sort.includes(i)) return;
131                         var ob = {};
132                         ob[i] = null;
133                         order_by.push(ob);
134                     }
135                 } else {
136                     var i = Object.keys(c)[0];
137                     var direction = c[i];
138                     if (i.match(/^hold\./)) {
139                         i = i.replace('hold.','');
140                         if (cannot_sort.includes(i)) return;
141                         var ob = {}
142                         ob[i] = {dir:direction};
143                         order_by.push(ob);
144                     }
145                 }
146             });
147         }
148
149         egProgressDialog.open({max : 1, value : 0});
150         var first = true;
151         return egHolds.fetch_wide_holds(
152             restrictions,
153             order_by
154         ).then(function () {
155                 return provider.arrayNotifier(holds, offset, count);
156             },
157             null,
158             function(hold_data) { 
159                 if (first) {
160                     hold_count = hold_data;
161                     first = false;
162                     egProgressDialog.update({max:hold_count});
163                 } else {
164                     egProgressDialog.increment();
165                     var new_item = { id : hold_data.id, hold : hold_data };
166                     new_item.status_string =
167                         egCore.strings['HOLD_STATUS_' + hold_data.hold_status]
168                         || hold_data.hold_status;
169
170                     if (clear_mode) {
171                         if (hold_data.clear_me) holds.push(new_item);
172                         all_holds.push(new_item);
173                     } else {
174                         holds.push(new_item);
175                     }
176                 }
177             }
178         ).finally(egProgressDialog.close);
179     }
180
181     // re-draw the grid when user changes the org selector
182     $scope.pickup_ou = egCore.org.get(egCore.auth.user().ws_ou());
183     $scope.$watch('pickup_ou', function(newVal, oldVal) {
184         if (newVal && newVal != oldVal) 
185             refresh_page();
186     });
187
188     $scope.detail_view = function(action, user_data, items) {
189         if (h = items[0]) {
190             $location.path('/circ/holds/shelf/' + h.hold.id);
191         }
192     }
193
194     $scope.list_view = function(items) {
195         $location.path('/circ/holds/shelf');
196     }
197
198     // when the detail hold is fetched (and updated), update the bib
199     // record summary display record id.
200     $scope.set_hold = function(hold_data) {
201         $scope.detail_hold_record_id = hold_data.hold.record_id;
202     }
203
204     // manage active vs. clearable holds display
205     var clearing = false; // true if actively clearing holds (below)
206     $scope.is_clearing = function() { return clearing };
207     $scope.active_mode = function() {return !clear_mode}
208     $scope.clear_mode = function() {return clear_mode}
209     $scope.show_clearable = function() { clear_mode = true; provider.refresh() }
210     $scope.show_active = function() { clear_mode = false; provider.refresh() }
211     $scope.disable_clear = function() { return clearing || !clear_mode }
212
213     // udpate the in-grid hold with the clear-shelf cached response info.
214     function handle_clear_cache_resp(resp) {
215         if (!angular.isArray(resp)) resp = [resp];
216         angular.forEach(resp, function(info) {
217             if (info.action) {
218                 var grid_item = holds.filter(function(item) {
219                     return item.hold.id == info.hold_details.id
220                 })[0];
221
222                 var all_hold_item = all_holds.filter(function(item) {
223                     return item.hold.id == info.hold_details.id
224                 })[0];
225
226                 // there will be no grid item if the hold is off-page
227                 if (grid_item) {
228                     grid_item.post_clear = 
229                         egCore.strings['CLEAR_SHELF_ACTION_' + info.action];
230                     all_hold_item.post_clear = 
231                         egCore.strings['CLEAR_SHELF_ACTION_' + info.action];
232                 }
233             }
234         });
235     }
236
237     $scope.clear_holds = function() {
238         clearing = true;
239         $scope.clear_progress = {max : 0, value : 0};
240
241         // we want to see all processed holds, so (effectively) remove
242         // the grid limit.
243         $scope.gridControls.setLimit(1000, true); 
244
245         // initiate clear shelf and grab cache key
246         egCore.net.request(
247             'open-ils.circ',
248             'open-ils.circ.hold.clear_shelf.process',
249             egCore.auth.token(), $scope.pickup_ou.id(),
250             null, 1
251
252         // request responses from the clear shelf cache
253         ).then(
254             
255             // clear shelf done; fetch the cached results.
256             function(resp) {
257                 clearing = false;
258                 egCore.net.request(
259                     'open-ils.circ',
260                     'open-ils.circ.hold.clear_shelf.get_cache',
261                     egCore.auth.token(), resp.cache_key, 1
262                 ).then(null, null, handle_clear_cache_resp);
263             }, 
264
265             null,
266
267             // handle streamed clear_shelf progress updates
268             function(resp) {
269                 if (resp.maximum) 
270                     $scope.clear_progress.max = resp.maximum;
271                 if (resp.progress)
272                     $scope.clear_progress.value = resp.progress;
273             }
274
275         );
276     }
277
278     function map_prefix_to_subhash (h,pf) {
279         var newhash = {};
280         angular.forEach(Object.keys(h), function (k) {
281             if (k.startsWith(pf)) {
282                 var nk = k.substr(pf.length);
283                 newhash[nk] = h[k];
284             }
285         });
286         return newhash;
287     }
288
289     $scope.print_shelf_list = function() {
290         var print_holds = [];
291         angular.forEach(holds, function(hold_data) {
292             var phold = {};
293             print_holds.push(phold);
294
295             phold.status_string = hold_data.status_string;
296
297             phold.patron_first = hold_data.hold.usr_first_given_name;
298             phold.patron_last = hold_data.hold.usr_family_name;
299             phold.patron_alias = hold_data.hold.usr_alias;
300             phold.patron_barcode = hold_data.hold.ucard_barcode;
301
302             phold.title = hold_data.hold.title;
303             phold.author = hold_data.hold.author;
304
305             phold.hold = hold_data.hold;
306             phold.copy = map_prefix_to_subhash(hold_data.hold, 'cp_');
307             phold.volume = map_prefix_to_subhash(hold_data.hold, 'cn_');
308             phold.part = map_prefix_to_subhash(hold_data.hold, 'p_');
309         });
310
311         console.log(print_holds);
312
313         return egCore.print.print({
314             context : 'default', 
315             template : 'hold_shelf_list', 
316             scope : {holds : print_holds}
317         });
318     }
319
320     refresh_page();
321
322 }])
323
324 .controller('HoldsPullListCtrl',
325        ['$scope','$q','$routeParams','$window','$location','egCore',
326         'egHolds','egCirc','egHoldGridActions','egProgressDialog',
327 function($scope , $q , $routeParams , $window , $location , egCore , 
328          egHolds , egCirc , egHoldGridActions , egProgressDialog) {
329
330     $scope.detail_hold_id = $routeParams.hold_id;
331
332     var cached_details = {};
333     var details_needed = {};
334
335     egCore.strings.setPageTitle(egCore.strings['PULL_LIST_TITLE']);
336
337     $scope.gridControls = {
338         setQuery : function() {
339             return {'copy_circ_lib_id' : egCore.auth.user().ws_ou()}
340         },
341         setSort : function() {
342             return ['copy_location_order_position','call_number_sort_key']
343         },
344         collectStarted : function(offset) {
345             // Launch an indeterminate -> semi-determinate progress
346             // modal.  Using a determinate modal that starts counting
347             // on the post-grid holds data retrieval results in a modal
348             // that's stuck at 0% for most of its life, which is aggravating.
349             egProgressDialog.open();
350         },
351         itemRetrieved : function(item) {
352             egProgressDialog.increment();
353             if (!cached_details[item.id]) {
354                 details_needed[item.id] = item;
355             }
356         },
357         allItemsRetrieved : function() {
358             flesh_holds().finally(egProgressDialog.close);
359         }
360     }
361
362
363     // Fetches hold detail data for each hold in the grid and links
364     // the detail data to the related grid item so egHoldGridActions 
365     // and friends have access to holds data they understand.
366     // Only fetch not-yet-cached data.
367     function flesh_holds() {
368         egProgressDialog.increment();
369
370         // Start by fleshing hold details from our cached data.
371         var items = $scope.gridControls.allItems();
372         angular.forEach(items, function(item) {
373             if (!cached_details[item.id]) return $q.when();
374             angular.forEach(cached_details[item.id], 
375                 function(val, key) { item[key] = val })
376         });
377
378         // Exit if all needed details were already cached
379         if (Object.keys(details_needed).length == 0) return $q.when();
380
381         return egCore.net.request(
382             'open-ils.circ',
383             'open-ils.circ.hold.details.batch.retrieve.authoritative',
384             egCore.auth.token(), Object.keys(details_needed), {
385                 include_usr : true
386             }
387
388         ).then(null, null, function(hold_info) {
389             egProgressDialog.increment();
390
391             // check if this is a staff-created hold
392             // i.e., requestor is not the same as the user
393             hold_info['_is_staff_hold'] = hold_info.hold.requestor() != hold_info.hold.usr().id();
394
395             var hold_id = hold_info.hold.id();
396             cached_details[hold_id] = hold_info;
397             var item = details_needed[hold_id];
398             delete details_needed[hold_id];
399
400             // flesh the grid item from the blob of hold data.
401             angular.forEach(hold_info, 
402                 function(val, key) { item[key] = val });
403
404         });
405     }
406
407     $scope.grid_actions = egHoldGridActions;
408     $scope.grid_actions.refresh = function() {
409         cached_details = {}; // un-cache details after edit actions.
410         $scope.gridControls.refresh();
411     }
412
413     $scope.detail_view = function(action, user_data, items) {
414         if (h = items[0]) {
415             $location.path('/circ/holds/pull/' + h.hold.id());
416         }
417     }
418
419     $scope.list_view = function(items) {
420         $location.path('/circ/holds/pull');
421     }
422
423     // when the detail hold is fetched (and updated), update the bib
424     // record summary display record id.
425     $scope.set_hold = function(hold_data) {
426         $scope.detail_hold_record_id = hold_data.mvr.doc_id();
427     }
428
429     // By default, this action is hidded from the UI, but leaving it
430     // here in case it's needed in the future
431     $scope.print_list_alt = function() {
432         var url = '/opac/extras/circ/alt_holds_print.html';
433         var win = $window.open(url, '_blank');
434         win.ses = function() {return egCore.auth.token()};
435         win.open();
436         win.focus();
437     }
438
439     $scope.print_full_list = function() {
440         var print_holds = [];
441         egProgressDialog.open({value : 0});
442
443         // collect the full list of holds
444         egCore.net.request(
445             'open-ils.circ',
446             'open-ils.circ.hold_pull_list.fleshed.stream',
447             egCore.auth.token(), 10000, 0
448         ).then(
449             function() {
450                 console.debug('printing ' + print_holds.length + ' holds');
451
452                 // holds fetched, send to print
453                 egCore.print.print({
454                     context : 'default', 
455                     template : 'hold_pull_list', 
456                     scope : {holds : print_holds}
457                 });
458             },
459             null, 
460             function(hold_data) {
461                 egProgressDialog.increment();
462                 egHolds.local_flesh(hold_data);
463                 print_holds.push(hold_data);
464                 hold_data.title = hold_data.mvr.title();
465                 hold_data.author = hold_data.mvr.author();
466                 hold_data.hold = egCore.idl.toHash(hold_data.hold);
467                 hold_data.copy = egCore.idl.toHash(hold_data.copy);
468                 hold_data.volume = egCore.idl.toHash(hold_data.volume);
469                 hold_data.part = egCore.idl.toHash(hold_data.part);
470             }
471         ).finally(egProgressDialog.close);
472     }
473
474 }])
475