837b5b356fa9bf9e3557800f735ef8af1dc57330
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / cat / item / app.js
1 /**
2  * Item Display
3  */
4
5 angular.module('egItemStatus', 
6     ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
7
8 .filter('boolText', function(){
9     return function (v) {
10         return v == 't';
11     }
12 })
13
14 .config(function($routeProvider, $locationProvider, $compileProvider) {
15     $locationProvider.html5Mode(true);
16     $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
17
18     var resolver = {delay : function(egStartup) {return egStartup.go()}};
19
20     // search page shows the list view by default
21     $routeProvider.when('/cat/item/search', {
22         templateUrl: './cat/item/t_list',
23         controller: 'ListCtrl',
24         resolve : resolver
25     });
26
27     // search page shows the list view by default
28     $routeProvider.when('/cat/item/search/:idList', {
29         templateUrl: './cat/item/t_list',
30         controller: 'ListCtrl',
31         resolve : resolver
32     });
33
34     $routeProvider.when('/cat/item/:id', {
35         templateUrl: './cat/item/t_view',
36         controller: 'ViewCtrl',
37         resolve : resolver
38     });
39
40     $routeProvider.when('/cat/item/:id/:tab', {
41         templateUrl: './cat/item/t_view',
42         controller: 'ViewCtrl',
43         resolve : resolver
44     });
45
46     // default page / bucket view
47     $routeProvider.otherwise({redirectTo : '/cat/item/search'});
48 })
49
50 .factory('itemSvc', 
51        ['egCore',
52 function(egCore) {
53
54     var service = {
55         copies : [], // copy barcode search results
56         index : 0 // search grid index
57     };
58
59     service.flesh = {   
60         flesh : 3, 
61         flesh_fields : {
62             acp : ['call_number','location','status','location','floating'],
63             acn : ['record','prefix','suffix'],
64             bre : ['simple_record','creator','editor']
65         },
66         select : { 
67             // avoid fleshing MARC on the bre
68             // note: don't add simple_record.. not sure why
69             bre : ['id','tcn_value','creator','editor'],
70         } 
71     }
72
73     // resolved with the last received copy
74     service.fetch = function(barcode, id, noListDupes) {
75         var promise;
76
77         if (barcode) {
78             promise = egCore.pcrud.search('acp', 
79                 {barcode : barcode, deleted : 'f'}, service.flesh);
80         } else {
81             promise = egCore.pcrud.retrieve('acp', id, service.flesh);
82         }
83
84         var lastRes;
85         return promise.then(
86             function() {return lastRes},
87             null, // error
88
89             // notify reads the stream of copies, one at a time.
90             function(copy) {
91
92                 var flatCopy;
93                 if (noListDupes) {
94                     // use the existing copy if possible
95                     flatCopy = service.copies.filter(
96                         function(c) {return c.id == copy.id()})[0];
97                 }
98
99                 if (!flatCopy) {
100                     flatCopy = egCore.idl.toHash(copy, true);
101                     flatCopy.index = service.index++;
102                     service.copies.unshift(flatCopy);
103                 }
104
105                 return lastRes = {
106                     copy : copy, 
107                     index : flatCopy.index
108                 }
109             }
110         );
111     }
112
113     return service;
114 }])
115
116 /**
117  * Search bar along the top of the page.
118  * Parent scope for list and detail views
119  */
120 .controller('SearchCtrl', 
121        ['$scope','$location','egCore','egGridDataProvider','itemSvc',
122 function($scope , $location , egCore , egGridDataProvider , itemSvc) {
123     $scope.args = {}; // search args
124
125     // sub-scopes (search / detail-view) apply their version 
126     // of retrieval function to $scope.context.search
127     // and display toggling via $scope.context.toggleDisplay
128     $scope.context = {
129         selectBarcode : true
130     };
131
132     $scope.toggleView = function($event) {
133         $scope.context.toggleDisplay();
134         $event.preventDefault(); // avoid form submission
135     }
136 }])
137
138 /**
139  * List view - grid stuff
140  */
141 .controller('ListCtrl', 
142        ['$scope','$q','$routeParams','$location','$timeout','egCore','egGridDataProvider','itemSvc',
143 function($scope , $q , $routeParams , $location , $timeout , egCore , egGridDataProvider , itemSvc) {
144     var copyId = [];
145     var cp_list = $routeParams.idList;
146     if (cp_list) {
147         copyId = cp_list.split(',');
148     }
149
150     $scope.context.page = 'list';
151
152     /*
153     var provider = egGridDataProvider.instance();
154     provider.get = function(offset, count) {
155     }
156     */
157
158     $scope.gridDataProvider = egGridDataProvider.instance({
159         get : function(offset, count) {
160             //return provider.arrayNotifier(itemSvc.copies, offset, count);
161             return this.arrayNotifier(itemSvc.copies, offset, count);
162         }
163     });
164
165     // If a copy was just displayed in the detail view, ensure it's
166     // focused in the list view.
167     var selected = false;
168     var copyGrid = $scope.gridControls = {
169         itemRetrieved : function(item) {
170             if (selected || !itemSvc.copy) return;
171             if (itemSvc.copy.id() == item.id) {
172                 copyGrid.selectItems([item.index]);
173                 selected = true;
174             }
175         }
176     };
177
178     $scope.$watch('barcodesFromFile', function(newVal, oldVal) {
179         if (newVal && newVal != oldVal) {
180             $scope.args.barcode = '';
181             var barcodes = [];
182
183             angular.forEach(newVal.split(/\n/), function(line) {
184                 if (!line) return;
185                 // scrub any trailing spaces or commas from the barcode
186                 line = line.replace(/(.*?)($|\s.*|,.*)/,'$1');
187                 barcodes.push(line);
188             });
189
190             itemSvc.fetch(barcodes).then(
191                 function() {
192                     copyGrid.refresh();
193                     copyGrid.selectItems([itemSvc.copies[0].index]);
194                 }
195             );
196         }
197     });
198
199     $scope.context.search = function(args) {
200         if (!args.barcode) return;
201         $scope.context.itemNotFound = false;
202         itemSvc.fetch(args.barcode).then(function(res) {
203             if (res) {
204                 copyGrid.refresh();
205                 copyGrid.selectItems([res.index]);
206                 $scope.args.barcode = '';
207             } else {
208                 $scope.context.itemNotFound = true;
209             }
210             $scope.context.selectBarcode = true;
211         })
212     }
213
214     $scope.context.toggleDisplay = function() {
215         var item = copyGrid.selectedItems()[0];
216         if (item) 
217             $location.path('/cat/item/' + item.id);
218     }
219
220     $scope.context.show_triggered_events = function() {
221         var item = copyGrid.selectedItems()[0];
222         if (item) 
223             $location.path('/cat/item/' + item.id + '/triggered_events');
224     }
225
226     if (copyId.length > 0) {
227         itemSvc.fetch(null,copyId).then(
228             function() {
229                 copyGrid.refresh();
230             }
231         );
232     }
233
234 }])
235
236 /**
237  * Detail view -- shows one copy
238  */
239 .controller('ViewCtrl', 
240        ['$scope','$q','$location','$routeParams','$timeout','$window','egCore','itemSvc','egBilling',
241 function($scope , $q , $location , $routeParams , $timeout , $window , egCore , itemSvc , egBilling) {
242     var copyId = $routeParams.id;
243     $scope.tab = $routeParams.tab || 'summary';
244     $scope.context.page = 'detail';
245     $scope.summaryRecord = null;
246
247     $scope.edit = false;
248     if ($scope.tab == 'edit') {
249         $scope.tab = 'summary';
250         $scope.edit = true;
251     }
252
253
254     // use the cached record info
255     if (itemSvc.copy)
256         $scope.recordId = itemSvc.copy.call_number().record().id();
257
258     function loadCopy(barcode) {
259         $scope.context.itemNotFound = false;
260
261         // Avoid re-fetching the same copy while jumping tabs.
262         // In addition to being quicker, this helps to avoid flickering
263         // of the top panel which is always visible in the detail view.
264         //
265         // 'barcode' represents the loading of a new item - refetch it
266         // regardless of whether it matches the current item.
267         if (!barcode && itemSvc.copy && itemSvc.copy.id() == copyId) {
268             $scope.copy = itemSvc.copy;
269             $scope.recordId = itemSvc.copy.call_number().record().id();
270             return $q.when();
271         }
272
273         delete $scope.copy;
274         delete itemSvc.copy;
275
276         var deferred = $q.defer();
277         itemSvc.fetch(barcode, copyId, true).then(function(res) {
278             $scope.context.selectBarcode = true;
279
280             if (!res) {
281                 copyId = null;
282                 $scope.context.itemNotFound = true;
283                 deferred.reject(); // avoid propagation of data fetch calls
284                 return;
285             }
286
287             var copy = res.copy;
288             itemSvc.copy = copy;
289
290
291             $scope.copy = copy;
292             $scope.recordId = copy.call_number().record().id();
293             $scope.args.barcode = '';
294
295             // locally flesh org units
296             copy.circ_lib(egCore.org.get(copy.circ_lib()));
297             copy.call_number().owning_lib(
298                 egCore.org.get(copy.call_number().owning_lib()));
299
300             var r = copy.call_number().record();
301             if (r.owner()) r.owner(egCore.org.get(r.owner())); 
302
303             // make boolean for auto-magic true/false display
304             angular.forEach(
305                 ['ref','opac_visible','holdable','circulate'],
306                 function(field) { copy[field](Boolean(copy[field]() == 't')) }
307             );
308
309             // finally, if this is a different copy, redirect.
310             // Note that we flesh first since the copy we just
311             // fetched will be used after the redirect.
312             if (copyId && copyId != copy.id()) {
313                 // if a new barcode is scanned in the detail view,
314                 // update the url to match the ID of the new copy
315                 $location.path('/cat/item/' + copy.id() + '/' + $scope.tab);
316                 deferred.reject(); // avoid propagation of data fetch calls
317                 return;
318             }
319             copyId = copy.id();
320
321             deferred.resolve();
322         });
323
324         return deferred.promise;
325     }
326
327     // if loadPrev load the two most recent circulations
328     function loadCurrentCirc(loadPrev) {
329         delete $scope.circ;
330         delete $scope.circ_summary;
331         delete $scope.prev_circ_summary;
332         if (!copyId) return;
333         
334         egCore.pcrud.search('circ', 
335             {target_copy : copyId},
336             {   flesh : 2,
337                 flesh_fields : {
338                     circ : [
339                         'usr',
340                         'workstation',                                         
341                         'checkin_workstation',                                 
342                         'duration_rule',                                       
343                         'max_fine_rule',                                       
344                         'recurring_fine_rule'   
345                     ],
346                     au : ['card']
347                 },
348                 order_by : {circ : 'xact_start desc'}, 
349                 limit :  1
350             }
351
352         ).then(null, null, function(circ) {
353             $scope.circ = circ;
354
355             // load the chain for this circ
356             egCore.net.request(
357                 'open-ils.circ',
358                 'open-ils.circ.renewal_chain.retrieve_by_circ.summary',
359                 egCore.auth.token(), $scope.circ.id()
360             ).then(function(summary) {
361                 $scope.circ_summary = summary.summary;
362             });
363
364             if (!loadPrev) return;
365
366             // load the chain for the previous circ, plus the user
367             egCore.net.request(
368                 'open-ils.circ',
369                 'open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary',
370                 egCore.auth.token(), $scope.circ.id()
371
372             ).then(null, null, function(summary) {
373                 $scope.prev_circ_summary = summary.summary;
374
375                 egCore.pcrud.retrieve('au', summary.usr,
376                     {flesh : 1, flesh_fields : {au : ['card']}})
377
378                 .then(function(user) {
379                     $scope.prev_circ_usr = user;
380                 });
381             });
382         });
383     }
384
385     var maxHistory;
386     function fetchMaxCircHistory() {
387         if (maxHistory) return $q.when(maxHistory);
388         return egCore.org.settings(
389             'circ.item_checkout_history.max')
390         .then(function(set) {
391             maxHistory = set['circ.item_checkout_history.max'] || 4;
392             return maxHistory;
393         });
394     }
395
396     $scope.addBilling = function(circ) {
397         egBilling.showBillDialog({
398             xact_id : circ.id(),
399             patron : circ.usr()
400         });
401     }
402
403     $scope.retrieveAllPatrons = function() {
404         var users = new Set();
405         angular.forEach($scope.circ_list.map(function(circ) { return circ.usr(); }),function(usr) {
406             users.add(usr);
407         });
408         users.forEach(function(usr) {
409             $timeout(function() {
410                 var url = $location.absUrl().replace(
411                     /\/cat\/.*/,
412                     '/circ/patron/' + usr.id() + '/checkout');
413                 $window.open(url, '_blank')
414             });
415         });
416     }
417
418     function loadCircHistory() {
419         $scope.circ_list = [];
420
421         var copy_org = 
422             itemSvc.copy.call_number().id() == -1 ?
423             itemSvc.copy.circ_lib().id() :
424             itemSvc.copy.call_number().owning_lib().id()
425
426         // there is an extra layer of permissibility over circ
427         // history views
428         egCore.perm.hasPermAt('VIEW_COPY_CHECKOUT_HISTORY', true)
429         .then(function(orgIds) {
430
431             if (orgIds.indexOf(copy_org) == -1) {
432                 console.log('User is not allowed to view circ history');
433                 return $q.when(0);
434             }
435
436             return fetchMaxCircHistory();
437
438         }).then(function(count) {
439
440             egCore.pcrud.search('circ', 
441                 {target_copy : copyId},
442                 {   flesh : 2,
443                     flesh_fields : {
444                         circ : [
445                             'usr',
446                             'workstation',                                         
447                             'checkin_workstation',                                 
448                             'recurring_fine_rule'   
449                         ],
450                         au : ['card']
451                     },
452                     order_by : {circ : 'xact_start desc'}, 
453                     limit :  count
454                 }
455
456             ).then(null, null, function(circ) {
457
458                 // flesh circ_lib locally
459                 circ.circ_lib(egCore.org.get(circ.circ_lib()));
460                 circ.checkin_lib(egCore.org.get(circ.checkin_lib()));
461                 $scope.circ_list.push(circ);
462             });
463         });
464     }
465
466
467     function loadCircCounts() {
468
469         delete $scope.circ_counts;
470         $scope.total_circs = 0;
471         $scope.total_circs_this_year = 0;
472         $scope.total_circs_prev_year = 0;
473         if (!copyId) return;
474
475         egCore.pcrud.search('circbyyr', 
476             {copy : copyId}, null, {atomic : true})
477
478         .then(function(counts) {
479             $scope.circ_counts = counts;
480
481             angular.forEach(counts, function(count) {
482                 $scope.total_circs += Number(count.count());
483             });
484
485             var this_year = counts.filter(function(c) {
486                 return c.year() == new Date().getFullYear();
487             });
488
489             $scope.total_circs_this_year = 
490                 this_year.length ? this_year[0].count() : 0;
491
492             var prev_year = counts.filter(function(c) {
493                 return c.year() == new Date().getFullYear() - 1;
494             });
495
496             $scope.total_circs_prev_year = 
497                 prev_year.length ? prev_year[0].count() : 0;
498
499         });
500     }
501
502     function loadHolds() {
503         delete $scope.hold;
504         if (!copyId) return;
505
506         egCore.pcrud.search('ahr', 
507             {   current_copy : copyId, 
508                 cancel_time : null, 
509                 fulfillment_time : null,
510                 capture_time : {'<>' : null}
511             }, {
512                 flesh : 2,
513                 flesh_fields : {
514                     ahr : ['requestor', 'usr'],
515                     au  : ['card']
516                 }
517             }
518         ).then(null, null, function(hold) {
519             $scope.hold = hold;
520             hold.pickup_lib(egCore.org.get(hold.pickup_lib()));
521             if (hold.current_shelf_lib()) {
522                 hold.current_shelf_lib(
523                     egCore.org.get(hold.current_shelf_lib()));
524             }
525             hold.behind_desk(Boolean(hold.behind_desk() == 't'));
526         });
527     }
528
529     function loadTransits() {
530         delete $scope.transit;
531         delete $scope.hold_transit;
532         if (!copyId) return;
533
534         egCore.pcrud.search('atc', 
535             {target_copy : copyId},
536             {order_by : {atc : 'source_send_time DESC'}}
537
538         ).then(null, null, function(transit) {
539             $scope.transit = transit;
540             transit.source(egCore.org.get(transit.source()));
541             transit.dest(egCore.org.get(transit.dest()));
542         })
543     }
544
545
546     // we don't need all data on all tabs, so fetch what's needed when needed.
547     function loadTabData() {
548         switch($scope.tab) {
549             case 'summary':
550                 loadCurrentCirc();
551                 loadCircCounts();
552                 break;
553
554             case 'circs':
555                 loadCurrentCirc(true);
556                 break;
557
558             case 'circ_list':
559                 loadCircHistory();
560                 break;
561
562             case 'holds':
563                 loadHolds()
564                 loadTransits();
565                 break;
566
567             case 'triggered_events':
568                 var url = $location.absUrl().replace(/\/staff.*/, '/actor/user/event_log');
569                 url += '?copy_id=' + encodeURIComponent(copyId);
570                 $scope.triggered_events_url = url;
571                 $scope.funcs = {};
572         }
573
574         if ($scope.edit) {
575             egCore.net.request(
576                 'open-ils.actor',
577                 'open-ils.actor.anon_cache.set_value',
578                 null, 'edit-these-copies', {
579                     record_id: $scope.recordId,
580                     copies: [copyId],
581                     hide_vols : true,
582                     hide_copies : false
583                 }
584             ).then(function(key) {
585                 if (key) {
586                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
587                     $window.location.href = url;
588                 } else {
589                     alert('Could not create anonymous cache key!');
590                 }
591             });
592         }
593
594         return;
595     }
596
597     $scope.context.toggleDisplay = function() {
598         $location.path('/cat/item/search');
599     }
600
601     // handle the barcode scan box, which will replace our current copy
602     $scope.context.search = function(args) {
603         loadCopy(args.barcode).then(loadTabData);
604     }
605
606     $scope.context.show_triggered_events = function() {
607         $location.path('/cat/item/' + copyId + '/triggered_events');
608     }
609
610     loadCopy().then(loadTabData);
611 }])