6e411ef595976ebdd22e1d20806daf59e220d541
[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', 'egUserMod'])
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','circ_modifier','age_protect'],
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
94                 egCore.pcrud.search('aihu', 
95                     {item : copy.id()}, {}, {idlist : true, atomic : true})
96                 .then(function(uses) { 
97                     copy._inHouseUseCount = uses.length;
98                 });
99
100                 if (noListDupes) {
101                     // use the existing copy if possible
102                     flatCopy = service.copies.filter(
103                         function(c) {return c.id == copy.id()})[0];
104                 }
105
106                 if (!flatCopy) {
107                     flatCopy = egCore.idl.toHash(copy, true);
108                     flatCopy.index = service.index++;
109                     service.copies.unshift(flatCopy);
110                 }
111
112                 return lastRes = {
113                     copy : copy, 
114                     index : flatCopy.index
115                 }
116             }
117         );
118     }
119
120     return service;
121 }])
122
123 /**
124  * Search bar along the top of the page.
125  * Parent scope for list and detail views
126  */
127 .controller('SearchCtrl', 
128        ['$scope','$location','egCore','egGridDataProvider','itemSvc',
129 function($scope , $location , egCore , egGridDataProvider , itemSvc) {
130     $scope.args = {}; // search args
131
132     // sub-scopes (search / detail-view) apply their version 
133     // of retrieval function to $scope.context.search
134     // and display toggling via $scope.context.toggleDisplay
135     $scope.context = {
136         selectBarcode : true
137     };
138
139     $scope.toggleView = function($event) {
140         $scope.context.toggleDisplay();
141         $event.preventDefault(); // avoid form submission
142     }
143 }])
144
145 /**
146  * List view - grid stuff
147  */
148 .controller('ListCtrl', 
149        ['$scope','$q','$routeParams','$location','$timeout','$window','egCore','egGridDataProvider','itemSvc','egUser','$uibModal','egCirc','egConfirmDialog',
150 function($scope , $q , $routeParams , $location , $timeout , $window , egCore , egGridDataProvider , itemSvc , egUser , $uibModal , egCirc , egConfirmDialog) {
151     var copyId = [];
152     var cp_list = $routeParams.idList;
153     if (cp_list) {
154         copyId = cp_list.split(',');
155     }
156
157     $scope.context.page = 'list';
158
159     /*
160     var provider = egGridDataProvider.instance();
161     provider.get = function(offset, count) {
162     }
163     */
164
165     $scope.gridDataProvider = egGridDataProvider.instance({
166         get : function(offset, count) {
167             //return provider.arrayNotifier(itemSvc.copies, offset, count);
168             return this.arrayNotifier(itemSvc.copies, offset, count);
169         }
170     });
171
172     // If a copy was just displayed in the detail view, ensure it's
173     // focused in the list view.
174     var selected = false;
175     var copyGrid = $scope.gridControls = {
176         itemRetrieved : function(item) {
177             if (selected || !itemSvc.copy) return;
178             if (itemSvc.copy.id() == item.id) {
179                 copyGrid.selectItems([item.index]);
180                 selected = true;
181             }
182         }
183     };
184
185     $scope.$watch('barcodesFromFile', function(newVal, oldVal) {
186         if (newVal && newVal != oldVal) {
187             $scope.args.barcode = '';
188             var barcodes = [];
189
190             angular.forEach(newVal.split(/\n/), function(line) {
191                 if (!line) return;
192                 // scrub any trailing spaces or commas from the barcode
193                 line = line.replace(/(.*?)($|\s.*|,.*)/,'$1');
194                 barcodes.push(line);
195             });
196
197             itemSvc.fetch(barcodes).then(
198                 function() {
199                     copyGrid.refresh();
200                     copyGrid.selectItems([itemSvc.copies[0].index]);
201                 }
202             );
203         }
204     });
205
206     $scope.context.search = function(args) {
207         if (!args.barcode) return;
208         $scope.context.itemNotFound = false;
209         itemSvc.fetch(args.barcode).then(function(res) {
210             if (res) {
211                 copyGrid.refresh();
212                 copyGrid.selectItems([res.index]);
213                 $scope.args.barcode = '';
214             } else {
215                 $scope.context.itemNotFound = true;
216             }
217             $scope.context.selectBarcode = true;
218         })
219     }
220
221     var add_barcode_to_list = function (b) {
222         $scope.context.search({barcode:b});
223     }
224
225     $scope.context.toggleDisplay = function() {
226         var item = copyGrid.selectedItems()[0];
227         if (item) 
228             $location.path('/cat/item/' + item.id);
229     }
230
231     $scope.context.show_triggered_events = function() {
232         var item = copyGrid.selectedItems()[0];
233         if (item) 
234             $location.path('/cat/item/' + item.id + '/triggered_events');
235     }
236
237     function gatherSelectedRecordIds () {
238         var rid_list = [];
239         angular.forEach(
240             copyGrid.selectedItems(),
241             function (item) {
242                 if (rid_list.indexOf(item['call_number.record.id']) == -1)
243                     rid_list.push(item['call_number.record.id'])
244             }
245         );
246         return rid_list;
247     }
248
249     function gatherSelectedVolumeIds (rid) {
250         var cn_id_list = [];
251         angular.forEach(
252             copyGrid.selectedItems(),
253             function (item) {
254                 if (rid && item['call_number.record.id'] != rid) return;
255                 if (cn_id_list.indexOf(item['call_number.id']) == -1)
256                     cn_id_list.push(item['call_number.id'])
257             }
258         );
259         return cn_id_list;
260     }
261
262     function gatherSelectedHoldingsIds (rid) {
263         var cp_id_list = [];
264         angular.forEach(
265             copyGrid.selectedItems(),
266             function (item) {
267                 if (rid && item['call_number.record.id'] != rid) return;
268                 cp_id_list.push(item.id)
269             }
270         );
271         return cp_id_list;
272     }
273
274     $scope.add_copies_to_bucket = function() {
275         var copy_list = gatherSelectedHoldingsIds();
276         if (copy_list.length == 0) return;
277
278         return $uibModal.open({
279             templateUrl: './cat/catalog/t_add_to_bucket',
280             animation: true,
281             size: 'md',
282             controller:
283                    ['$scope','$uibModalInstance',
284             function($scope , $uibModalInstance) {
285
286                 $scope.bucket_id = 0;
287                 $scope.newBucketName = '';
288                 $scope.allBuckets = [];
289
290                 egCore.net.request(
291                     'open-ils.actor',
292                     'open-ils.actor.container.retrieve_by_class.authoritative',
293                     egCore.auth.token(), egCore.auth.user().id(),
294                     'copy', 'staff_client'
295                 ).then(function(buckets) { $scope.allBuckets = buckets; });
296
297                 $scope.add_to_bucket = function() {
298                     var promises = [];
299                     angular.forEach(copy_list, function (cp) {
300                         var item = new egCore.idl.ccbi()
301                         item.bucket($scope.bucket_id);
302                         item.target_copy(cp);
303                         promises.push(
304                             egCore.net.request(
305                                 'open-ils.actor',
306                                 'open-ils.actor.container.item.create',
307                                 egCore.auth.token(), 'copy', item
308                             )
309                         );
310
311                         return $q.all(promises).then(function() {
312                             $uibModalInstance.close();
313                         });
314                     });
315                 }
316
317                 $scope.add_to_new_bucket = function() {
318                     var bucket = new egCore.idl.ccb();
319                     bucket.owner(egCore.auth.user().id());
320                     bucket.name($scope.newBucketName);
321                     bucket.description('');
322                     bucket.btype('staff_client');
323
324                     return egCore.net.request(
325                         'open-ils.actor',
326                         'open-ils.actor.container.create',
327                         egCore.auth.token(), 'copy', bucket
328                     ).then(function(bucket) {
329                         $scope.bucket_id = bucket;
330                         $scope.add_to_bucket();
331                     });
332                 }
333
334                 $scope.cancel = function() {
335                     $uibModalInstance.dismiss();
336                 }
337             }]
338         });
339     }
340
341     $scope.need_one_selected = function() {
342         var items = $scope.gridControls.selectedItems();
343         if (items.length == 1) return false;
344         return true;
345     };
346
347     $scope.make_copies_bookable = function() {
348
349         var copies_by_record = {};
350         var record_list = [];
351         angular.forEach(
352             copyGrid.selectedItems(),
353             function (item) {
354                 var record_id = item['call_number.record.id'];
355                 if (typeof copies_by_record[ record_id ] == 'undefined') {
356                     copies_by_record[ record_id ] = [];
357                     record_list.push( record_id );
358                 }
359                 copies_by_record[ record_id ].push(item.id);
360             }
361         );
362
363         var promises = [];
364         var combined_results = [];
365         angular.forEach(record_list, function(record_id) {
366             promises.push(
367                 egCore.net.request(
368                     'open-ils.booking',
369                     'open-ils.booking.resources.create_from_copies',
370                     egCore.auth.token(),
371                     copies_by_record[record_id]
372                 ).then(function(results) {
373                     if (results && results['brsrc']) {
374                         combined_results = combined_results.concat(results['brsrc']);
375                     }
376                 })
377             );
378         });
379
380         $q.all(promises).then(function() {
381             if (combined_results.length > 0) {
382                 $uibModal.open({
383                     template: '<eg-embed-frame url="booking_admin_url" handlers="funcs"></eg-embed-frame>',
384                     animation: true,
385                     size: 'md',
386                     controller:
387                            ['$scope','$location','egCore','$uibModalInstance',
388                     function($scope , $location , egCore , $uibModalInstance) {
389
390                         $scope.funcs = {
391                             ses : egCore.auth.token(),
392                             resultant_brsrc : combined_results.map(function(o) { return o[0]; })
393                         }
394
395                         var booking_path = '/eg/conify/global/booking/resource';
396
397                         $scope.booking_admin_url =
398                             $location.absUrl().replace(/\/eg\/staff.*/, booking_path);
399                     }]
400                 });
401             }
402         });
403     }
404
405     $scope.book_copies_now = function() {
406         var copies_by_record = {};
407         var record_list = [];
408         angular.forEach(
409             copyGrid.selectedItems(),
410             function (item) {
411                 var record_id = item['call_number.record.id'];
412                 if (typeof copies_by_record[ record_id ] == 'undefined') {
413                     copies_by_record[ record_id ] = [];
414                     record_list.push( record_id );
415                 }
416                 copies_by_record[ record_id ].push(item.id);
417             }
418         );
419
420         var promises = [];
421         var combined_brt = [];
422         var combined_brsrc = [];
423         angular.forEach(record_list, function(record_id) {
424             promises.push(
425                 egCore.net.request(
426                     'open-ils.booking',
427                     'open-ils.booking.resources.create_from_copies',
428                     egCore.auth.token(),
429                     copies_by_record[record_id]
430                 ).then(function(results) {
431                     if (results && results['brt']) {
432                         combined_brt = combined_brt.concat(results['brt']);
433                     }
434                     if (results && results['brsrc']) {
435                         combined_brsrc = combined_brsrc.concat(results['brsrc']);
436                     }
437                 })
438             );
439         });
440
441         $q.all(promises).then(function() {
442             if (combined_brt.length > 0 || combined_brsrc.length > 0) {
443                 $uibModal.open({
444                     template: '<eg-embed-frame url="booking_admin_url" handlers="funcs"></eg-embed-frame>',
445                     animation: true,
446                     size: 'md',
447                     controller:
448                            ['$scope','$location','egCore','$uibModalInstance',
449                     function($scope , $location , egCore , $uibModalInstance) {
450
451                         $scope.funcs = {
452                             ses : egCore.auth.token(),
453                             bresv_interface_opts : {
454                                 booking_results : {
455                                      brt : combined_brt
456                                     ,brsrc : combined_brsrc
457                                 }
458                             }
459                         }
460
461                         var booking_path = '/eg/booking/reservation';
462
463                         $scope.booking_admin_url =
464                             $location.absUrl().replace(/\/eg\/staff.*/, booking_path);
465
466                     }]
467                 });
468             }
469         });
470     }
471
472     $scope.requestItems = function() {
473         var copy_list = gatherSelectedHoldingsIds();
474         if (copy_list.length == 0) return;
475
476         return $uibModal.open({
477             templateUrl: './cat/catalog/t_request_items',
478             animation: true,
479             controller:
480                    ['$scope','$uibModalInstance','egUser',
481             function($scope , $uibModalInstance , egUser) {
482                 $scope.user = null;
483                 $scope.first_user_fetch = true;
484
485                 $scope.hold_data = {
486                     hold_type : 'C',
487                     copy_list : copy_list,
488                     pickup_lib: egCore.org.get(egCore.auth.user().ws_ou()),
489                     user      : egCore.auth.user().id()
490                 };
491
492                 egUser.get( $scope.hold_data.user ).then(function(u) {
493                     $scope.user = u;
494                     $scope.barcode = u.card().barcode();
495                     $scope.user_name = egUser.format_name(u);
496                     $scope.hold_data.user = u.id();
497                 });
498
499                 $scope.user_name = '';
500                 $scope.barcode = '';
501                 $scope.$watch('barcode', function (n) {
502                     if (!$scope.first_user_fetch) {
503                         egUser.getByBarcode(n).then(function(u) {
504                             $scope.user = u;
505                             $scope.user_name = egUser.format_name(u);
506                             $scope.hold_data.user = u.id();
507                         }, function() {
508                             $scope.user = null;
509                             $scope.user_name = '';
510                             delete $scope.hold_data.user;
511                         });
512                     }
513                     $scope.first_user_fetch = false;
514                 });
515
516                 $scope.ok = function(h) {
517                     var args = {
518                         patronid  : h.user,
519                         hold_type : h.hold_type,
520                         pickup_lib: h.pickup_lib.id(),
521                         depth     : 0
522                     };
523
524                     egCore.net.request(
525                         'open-ils.circ',
526                         'open-ils.circ.holds.test_and_create.batch.override',
527                         egCore.auth.token(), args, h.copy_list
528                     );
529
530                     $uibModalInstance.close();
531                 }
532
533                 $scope.cancel = function($event) {
534                     $uibModalInstance.dismiss();
535                     $event.preventDefault();
536                 }
537             }]
538         });
539     }
540
541     $scope.replaceBarcodes = function() {
542         angular.forEach(copyGrid.selectedItems(), function (cp) {
543             $uibModal.open({
544                 templateUrl: './cat/share/t_replace_barcode',
545                 animation: true,
546                 controller:
547                            ['$scope','$uibModalInstance',
548                     function($scope , $uibModalInstance) {
549                         $scope.isModal = true;
550                         $scope.focusBarcode = false;
551                         $scope.focusBarcode2 = true;
552                         $scope.barcode1 = cp.barcode;
553
554                         $scope.updateBarcode = function() {
555                             $scope.copyNotFound = false;
556                             $scope.updateOK = false;
557
558                             egCore.pcrud.search('acp',
559                                 {deleted : 'f', barcode : $scope.barcode1})
560                             .then(function(copy) {
561
562                                 if (!copy) {
563                                     $scope.focusBarcode = true;
564                                     $scope.copyNotFound = true;
565                                     return;
566                                 }
567
568                                 $scope.copyId = copy.id();
569                                 copy.barcode($scope.barcode2);
570
571                                 egCore.pcrud.update(copy).then(function(stat) {
572                                     $scope.updateOK = stat;
573                                     $scope.focusBarcode = true;
574                                     if (stat) add_barcode_to_list(copy.barcode());
575                                 });
576
577                             });
578                             $uibModalInstance.close();
579                         }
580
581                         $scope.cancel = function($event) {
582                             $uibModalInstance.dismiss();
583                             $event.preventDefault();
584                         }
585                     }
586                 ]
587             });
588         });
589     }
590
591     $scope.attach_to_peer_bib = function() {
592         if (copyGrid.selectedItems().length == 0) return;
593
594         egCore.hatch.getItem('eg.cat.marked_conjoined_record').then(function(target_record) {
595             if (!target_record) return;
596
597             return $uibModal.open({
598                 templateUrl: './cat/catalog/t_conjoined_selector',
599                 animation: true,
600                 controller:
601                        ['$scope','$uibModalInstance',
602                 function($scope , $uibModalInstance) {
603                     $scope.update = false;
604
605                     $scope.peer_type = null;
606                     $scope.peer_type_list = [];
607
608                     get_peer_types = function() {
609                         if (egCore.env.bpt)
610                             return $q.when(egCore.env.bpt.list);
611
612                         return egCore.pcrud.retrieveAll('bpt', null, {atomic : true})
613                         .then(function(list) {
614                             egCore.env.absorbList(list, 'bpt');
615                             return list;
616                         });
617                     }
618
619                     get_peer_types().then(function(list){
620                         $scope.peer_type_list = list;
621                     });
622
623                     $scope.ok = function(type) {
624                         var promises = [];
625
626                         angular.forEach(copyGrid.selectedItems(), function (cp) {
627                             var n = new egCore.idl.bpbcm();
628                             n.isnew(true);
629                             n.peer_record(target_record);
630                             n.target_copy(cp.id);
631                             n.peer_type(type);
632                             promises.push(egCore.pcrud.create(n).then(function(){add_barcode_to_list(cp.barcode)}));
633                         });
634
635                         return $q.all(promises).then(function(){$uibModalInstance.close()});
636                     }
637
638                     $scope.cancel = function($event) {
639                         $uibModalInstance.dismiss();
640                         $event.preventDefault();
641                     }
642                 }]
643             });
644         });
645     }
646
647     $scope.selectedHoldingsCopyDelete = function () {
648         var copy_list = gatherSelectedHoldingsIds();
649         if (copy_list.length == 0) return;
650
651         var copy_objects = [];
652         egCore.pcrud.search('acp',
653             {deleted : 'f', id : copy_list},
654             { flesh : 1, flesh_fields : { acp : ['call_number'] } }
655         ).then(function(copy) {
656             copy_objects.push(copy);
657         }).then(function() {
658
659             var cnHash = {};
660             var perCnCopies = {};
661
662             var cn_count = 0;
663             var cp_count = 0;
664
665             angular.forEach(
666                 copy_objects,
667                 function (cp) {
668                     cp.isdeleted(1);
669                     cp_count++;
670                     var cn_id = cp.call_number().id();
671                     if (!cnHash[cn_id]) {
672                         cnHash[cn_id] = cp.call_number();
673                         perCnCopies[cn_id] = [cp];
674                     } else {
675                         perCnCopies[cn_id].push(cp);
676                     }
677                     cp.call_number(cn_id); // prevent loops in JSON-ification
678                 }
679             );
680
681             angular.forEach(perCnCopies, function (v, k) {
682                 cnHash[k].copies(v);
683             });
684
685             cnList = [];
686             angular.forEach(cnHash, function (v, k) {
687                 cnList.push(v);
688             });
689
690             if (cnList.length == 0) return;
691
692             var flags = {};
693
694             egConfirmDialog.open(
695                 egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES,
696                 egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES_MESSAGE,
697                 {copies : cp_count, volumes : cn_count}
698             ).result.then(function() {
699                 egCore.net.request(
700                     'open-ils.cat',
701                     'open-ils.cat.asset.volume.fleshed.batch.update.override',
702                     egCore.auth.token(), cnList, 1, flags
703                 ).then(function(){
704                     angular.forEach(copyGrid.selectedItems(), function(cp){add_barcode_to_list(cp.barcode)});
705                 });
706             });
707         });
708     }
709
710     $scope.selectedHoldingsItemStatusTgrEvt= function() {
711         var item = copyGrid.selectedItems()[0];
712         if (item)
713             $location.path('/cat/item/' + item.id + '/triggered_events');
714     }
715
716     $scope.selectedHoldingsItemStatusHolds= function() {
717         var item = copyGrid.selectedItems()[0];
718         if (item)
719             $location.path('/cat/item/' + item.id + '/holds');
720     }
721
722     $scope.cancel_transit = function () {
723         var initial_list = copyGrid.selectedItems();
724         angular.forEach(copyGrid.selectedItems(), function(cp) {
725             egCirc.find_copy_transit(null, {copy_barcode:cp.barcode})
726                 .then(function(t) { return egCirc.abort_transit(t.id())    })
727                 .then(function()  { return add_barcode_to_list(cp.barcode) });
728         });
729     }
730
731     $scope.selectedHoldingsDamaged = function () {
732         var initial_list = copyGrid.selectedItems();
733         egCirc.mark_damaged(gatherSelectedHoldingsIds()).then(function(){
734             angular.forEach(initial_list, function(cp){add_barcode_to_list(cp.barcode)});
735         });
736     }
737
738     $scope.selectedHoldingsMissing = function () {
739         var initial_list = copyGrid.selectedItems();
740         egCirc.mark_missing(gatherSelectedHoldingsIds()).then(function(){
741             angular.forEach(initial_list, function(cp){add_barcode_to_list(cp.barcode)});
742         });
743     }
744
745     $scope.checkin = function () {
746         angular.forEach(copyGrid.selectedItems(), function (cp) {
747             egCirc.checkin({copy_barcode:cp.barcode}).then(
748                 function() { add_barcode_to_list(cp.barcode) }
749             );
750         });
751     }
752
753     $scope.renew = function () {
754         angular.forEach(copyGrid.selectedItems(), function (cp) {
755             egCirc.renew({copy_barcode:cp.barcode}).then(
756                 function() { add_barcode_to_list(cp.barcode) }
757             );
758         });
759     }
760
761
762     var spawnHoldingsAdd = function (vols,copies){
763         angular.forEach(gatherSelectedRecordIds(), function (r) {
764             var raw = [];
765             if (copies) { // just a copy on existing volumes
766                 angular.forEach(gatherSelectedVolumeIds(r), function (v) {
767                     raw.push( {callnumber : v} );
768                 });
769             } else if (vols) {
770                 angular.forEach(
771                     gatherSelectedHoldingsIds(r),
772                     function (i) {
773                         angular.forEach(copyGrid.selectedItems(), function(item) {
774                             if (i == item.id) raw.push({owner : item['call_number.owning_lib']});
775                         });
776                     }
777                 );
778             }
779
780             if (raw.length == 0) raw.push({});
781
782             egCore.net.request(
783                 'open-ils.actor',
784                 'open-ils.actor.anon_cache.set_value',
785                 null, 'edit-these-copies', {
786                     record_id: r,
787                     raw: raw,
788                     hide_vols : false,
789                     hide_copies : false
790                 }
791             ).then(function(key) {
792                 if (key) {
793                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
794                     $timeout(function() { $window.open(url, '_blank') });
795                 } else {
796                     alert('Could not create anonymous cache key!');
797                 }
798             });
799         });
800     }
801     $scope.selectedHoldingsVolCopyAdd = function () { spawnHoldingsAdd(true,false) }
802     $scope.selectedHoldingsCopyAdd = function () { spawnHoldingsAdd(false,true) }
803
804     $scope.showBibHolds = function () {
805         angular.forEach(gatherSelectedRecordIds(), function (r) {
806             var url = egCore.env.basePath + 'cat/catalog/record/' + r + '/holds';
807             $timeout(function() { $window.open(url, '_blank') });
808         });
809     }
810
811     var spawnHoldingsEdit = function (hide_vols,hide_copies){
812         angular.forEach(gatherSelectedRecordIds(), function (r) {
813             egCore.net.request(
814                 'open-ils.actor',
815                 'open-ils.actor.anon_cache.set_value',
816                 null, 'edit-these-copies', {
817                     record_id: r,
818                     copies: gatherSelectedHoldingsIds(r),
819                     raw: {},
820                     hide_vols : hide_vols,
821                     hide_copies : hide_copies
822                 }
823             ).then(function(key) {
824                 if (key) {
825                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
826                     $timeout(function() { $window.open(url, '_blank') });
827                 } else {
828                     alert('Could not create anonymous cache key!');
829                 }
830             });
831         });
832     }
833     $scope.selectedHoldingsVolCopyEdit = function () { spawnHoldingsEdit(false,false) }
834     $scope.selectedHoldingsVolEdit = function () { spawnHoldingsEdit(false,true) }
835     $scope.selectedHoldingsCopyEdit = function () { spawnHoldingsEdit(true,false) }
836
837     // this "transfers" selected copies to a new owning library,
838     // auto-creating volumes and deleting unused volumes as required.
839     $scope.changeItemOwningLib = function() {
840         var xfer_target = egCore.hatch.getLocalItem('eg.cat.volume_transfer_target');
841         var items = copyGrid.selectedItems();
842         if (!xfer_target || !items.length) {
843             return;
844         }
845         var vols_to_move   = {};
846         var copies_to_move = {};
847         angular.forEach(items, function(item) {
848             if (item['call_number.owning_lib'] != xfer_target) {
849                 if (item['call_number.id'] in vols_to_move) {
850                     copies_to_move[item['call_number.id']].push(item.id);
851                 } else {
852                     vols_to_move[item['call_number.id']] = {
853                         label       : item['call_number.label'],
854                         label_class : item['call_number.label_class'],
855                         record      : item['call_number.record.id'],
856                         prefix      : item['call_number.prefix.id'],
857                         suffix      : item['call_number.suffix.id']
858                     };
859                     copies_to_move[item['call_number.id']] = new Array;
860                     copies_to_move[item['call_number.id']].push(item.id);
861                 }
862             }
863         });
864
865         var promises = [];
866         angular.forEach(vols_to_move, function(vol) {
867             promises.push(egCore.net.request(
868                 'open-ils.cat',
869                 'open-ils.cat.call_number.find_or_create',
870                 egCore.auth.token(),
871                 vol.label,
872                 vol.record,
873                 xfer_target,
874                 vol.prefix,
875                 vol.suffix,
876                 vol.label_class
877             ).then(function(resp) {
878                 var evt = egCore.evt.parse(resp);
879                 if (evt) return;
880                 return egCore.net.request(
881                     'open-ils.cat',
882                     'open-ils.cat.transfer_copies_to_volume',
883                     egCore.auth.token(),
884                     resp.acn_id,
885                     copies_to_move[vol.id]
886                 );
887             }));
888         });
889
890         angular.forEach(
891             copyGrid.selectedItems(),
892             function(cp){
893                 promises.push(
894                     function(){ add_barcode_to_list(cp.barcode) }
895                 )
896             }
897         );
898         $q.all(promises);
899     }
900
901     $scope.transferItems = function (){
902         var xfer_target = egCore.hatch.getLocalItem('eg.cat.item_transfer_target');
903         var copy_ids = gatherSelectedHoldingsIds();
904         if (xfer_target && copy_ids.length > 0) {
905             egCore.net.request(
906                 'open-ils.cat',
907                 'open-ils.cat.transfer_copies_to_volume',
908                 egCore.auth.token(),
909                 xfer_target,
910                 copy_ids
911             ).then(
912                 function(resp) { // oncomplete
913                     var evt = egCore.evt.parse(resp);
914                     egConfirmDialog.open(
915                         egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_TITLE,
916                         egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_BODY,
917                         {'evt_desc': evt.desc}
918                     ).result.then(function() {
919                         egCore.net.request(
920                             'open-ils.cat',
921                             'open-ils.cat.transfer_copies_to_volume.override',
922                             egCore.auth.token(),
923                             xfer_target,
924                             copy_ids,
925                             { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
926                         );
927                     });
928                 },
929                 null, // onerror
930                 null // onprogress
931             ).then(function() {
932                     angular.forEach(copyGrid.selectedItems(), function(cp){add_barcode_to_list(cp.barcode)});
933             });
934         }
935     }
936
937     $scope.print_list = function() {
938         var print_data = { copies : copyGrid.allItems() };
939
940         if (print_data.copies.length == 0) return $q.when();
941
942         return egCore.print.print({
943             template : 'item_status',
944             scope : print_data
945         });
946     }
947
948     if (copyId.length > 0) {
949         itemSvc.fetch(null,copyId).then(
950             function() {
951                 copyGrid.refresh();
952             }
953         );
954     }
955
956 }])
957
958 /**
959  * Detail view -- shows one copy
960  */
961 .controller('ViewCtrl', 
962        ['$scope','$q','$location','$routeParams','$timeout','$window','egCore','itemSvc','egBilling',
963 function($scope , $q , $location , $routeParams , $timeout , $window , egCore , itemSvc , egBilling) {
964     var copyId = $routeParams.id;
965     $scope.tab = $routeParams.tab || 'summary';
966     $scope.context.page = 'detail';
967     $scope.summaryRecord = null;
968
969     $scope.edit = false;
970     if ($scope.tab == 'edit') {
971         $scope.tab = 'summary';
972         $scope.edit = true;
973     }
974
975
976     // use the cached record info
977     if (itemSvc.copy)
978         $scope.recordId = itemSvc.copy.call_number().record().id();
979
980     function loadCopy(barcode) {
981         $scope.context.itemNotFound = false;
982
983         // Avoid re-fetching the same copy while jumping tabs.
984         // In addition to being quicker, this helps to avoid flickering
985         // of the top panel which is always visible in the detail view.
986         //
987         // 'barcode' represents the loading of a new item - refetch it
988         // regardless of whether it matches the current item.
989         if (!barcode && itemSvc.copy && itemSvc.copy.id() == copyId) {
990             $scope.copy = itemSvc.copy;
991             $scope.recordId = itemSvc.copy.call_number().record().id();
992             return $q.when();
993         }
994
995         delete $scope.copy;
996         delete itemSvc.copy;
997
998         var deferred = $q.defer();
999         itemSvc.fetch(barcode, copyId, true).then(function(res) {
1000             $scope.context.selectBarcode = true;
1001
1002             if (!res) {
1003                 copyId = null;
1004                 $scope.context.itemNotFound = true;
1005                 deferred.reject(); // avoid propagation of data fetch calls
1006                 return;
1007             }
1008
1009             var copy = res.copy;
1010             itemSvc.copy = copy;
1011
1012
1013             $scope.copy = copy;
1014             $scope.recordId = copy.call_number().record().id();
1015             $scope.args.barcode = '';
1016
1017             // locally flesh org units
1018             copy.circ_lib(egCore.org.get(copy.circ_lib()));
1019             copy.call_number().owning_lib(
1020                 egCore.org.get(copy.call_number().owning_lib()));
1021
1022             var r = copy.call_number().record();
1023             if (r.owner()) r.owner(egCore.org.get(r.owner())); 
1024
1025             // make boolean for auto-magic true/false display
1026             angular.forEach(
1027                 ['ref','opac_visible','holdable','circulate'],
1028                 function(field) { copy[field](Boolean(copy[field]() == 't')) }
1029             );
1030
1031             // finally, if this is a different copy, redirect.
1032             // Note that we flesh first since the copy we just
1033             // fetched will be used after the redirect.
1034             if (copyId && copyId != copy.id()) {
1035                 // if a new barcode is scanned in the detail view,
1036                 // update the url to match the ID of the new copy
1037                 $location.path('/cat/item/' + copy.id() + '/' + $scope.tab);
1038                 deferred.reject(); // avoid propagation of data fetch calls
1039                 return;
1040             }
1041             copyId = copy.id();
1042
1043             deferred.resolve();
1044         });
1045
1046         return deferred.promise;
1047     }
1048
1049     // if loadPrev load the two most recent circulations
1050     function loadCurrentCirc(loadPrev) {
1051         delete $scope.circ;
1052         delete $scope.circ_summary;
1053         delete $scope.prev_circ_summary;
1054         delete $scope.prev_circ_usr;
1055         if (!copyId) return;
1056         
1057         egCore.pcrud.search('circ', 
1058             {target_copy : copyId},
1059             {   flesh : 2,
1060                 flesh_fields : {
1061                     circ : [
1062                         'usr',
1063                         'workstation',                                         
1064                         'checkin_workstation',                                 
1065                         'duration_rule',                                       
1066                         'max_fine_rule',                                       
1067                         'recurring_fine_rule'   
1068                     ],
1069                     au : ['card']
1070                 },
1071                 order_by : {circ : 'xact_start desc'}, 
1072                 limit :  1
1073             }
1074
1075         ).then(null, null, function(circ) {
1076             $scope.circ = circ;
1077
1078             // load the chain for this circ
1079             egCore.net.request(
1080                 'open-ils.circ',
1081                 'open-ils.circ.renewal_chain.retrieve_by_circ.summary',
1082                 egCore.auth.token(), $scope.circ.id()
1083             ).then(function(summary) {
1084                 $scope.circ_summary = summary.summary;
1085             });
1086
1087             if (!loadPrev) return;
1088
1089             // load the chain for the previous circ, plus the user
1090             egCore.net.request(
1091                 'open-ils.circ',
1092                 'open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary',
1093                 egCore.auth.token(), $scope.circ.id()
1094
1095             ).then(null, null, function(summary) {
1096                 $scope.prev_circ_summary = summary.summary;
1097
1098                 if (summary.usr) { // aged circs have no 'usr'.
1099                     egCore.pcrud.retrieve('au', summary.usr,
1100                         {flesh : 1, flesh_fields : {au : ['card']}})
1101
1102                     .then(function(user) { $scope.prev_circ_usr = user });
1103                 }
1104             });
1105         });
1106     }
1107
1108     var maxHistory;
1109     function fetchMaxCircHistory() {
1110         if (maxHistory) return $q.when(maxHistory);
1111         return egCore.org.settings(
1112             'circ.item_checkout_history.max')
1113         .then(function(set) {
1114             maxHistory = set['circ.item_checkout_history.max'] || 4;
1115             return maxHistory;
1116         });
1117     }
1118
1119     $scope.addBilling = function(circ) {
1120         egBilling.showBillDialog({
1121             xact_id : circ.id(),
1122             patron : circ.usr()
1123         });
1124     }
1125
1126     $scope.retrieveAllPatrons = function() {
1127         var users = new Set();
1128         angular.forEach($scope.circ_list.map(function(circ) { return circ.usr(); }),function(usr) {
1129             // aged circs have no 'usr'.
1130             if (usr) users.add(usr);
1131         });
1132         users.forEach(function(usr) {
1133             $timeout(function() {
1134                 var url = $location.absUrl().replace(
1135                     /\/cat\/.*/,
1136                     '/circ/patron/' + usr.id() + '/checkout');
1137                 $window.open(url, '_blank')
1138             });
1139         });
1140     }
1141
1142     function loadCircHistory() {
1143         $scope.circ_list = [];
1144
1145         var copy_org = 
1146             itemSvc.copy.call_number().id() == -1 ?
1147             itemSvc.copy.circ_lib().id() :
1148             itemSvc.copy.call_number().owning_lib().id()
1149
1150         // there is an extra layer of permissibility over circ
1151         // history views
1152         egCore.perm.hasPermAt('VIEW_COPY_CHECKOUT_HISTORY', true)
1153         .then(function(orgIds) {
1154
1155             if (orgIds.indexOf(copy_org) == -1) {
1156                 console.log('User is not allowed to view circ history');
1157                 return $q.when(0);
1158             }
1159
1160             return fetchMaxCircHistory();
1161
1162         }).then(function(count) {
1163
1164             egCore.pcrud.search('combcirc', 
1165                 {target_copy : copyId},
1166                 {   flesh : 2,
1167                     flesh_fields : {
1168                         combcirc : [
1169                             'usr',
1170                             'workstation',                                         
1171                             'checkin_workstation',                                 
1172                             'recurring_fine_rule'   
1173                         ],
1174                         au : ['card']
1175                     },
1176                     order_by : {combcirc : 'xact_start desc'}, 
1177                     limit :  count
1178                 }
1179
1180             ).then(null, null, function(circ) {
1181
1182                 // flesh circ_lib locally
1183                 circ.circ_lib(egCore.org.get(circ.circ_lib()));
1184                 circ.checkin_lib(egCore.org.get(circ.checkin_lib()));
1185                 $scope.circ_list.push(circ);
1186             });
1187         });
1188     }
1189
1190
1191     function loadCircCounts() {
1192
1193         delete $scope.circ_counts;
1194         $scope.total_circs = 0;
1195         $scope.total_circs_this_year = 0;
1196         $scope.total_circs_prev_year = 0;
1197         if (!copyId) return;
1198
1199         egCore.pcrud.search('circbyyr', 
1200             {copy : copyId}, null, {atomic : true})
1201
1202         .then(function(counts) {
1203             $scope.circ_counts = counts;
1204
1205             angular.forEach(counts, function(count) {
1206                 $scope.total_circs += Number(count.count());
1207             });
1208
1209             var this_year = counts.filter(function(c) {
1210                 return c.year() == new Date().getFullYear();
1211             });
1212
1213             $scope.total_circs_this_year = 
1214                 this_year.length ? this_year[0].count() : 0;
1215
1216             var prev_year = counts.filter(function(c) {
1217                 return c.year() == new Date().getFullYear() - 1;
1218             });
1219
1220             $scope.total_circs_prev_year = 
1221                 prev_year.length ? prev_year[0].count() : 0;
1222
1223         });
1224     }
1225
1226     function loadHolds() {
1227         delete $scope.hold;
1228         if (!copyId) return;
1229
1230         egCore.pcrud.search('ahr', 
1231             {   current_copy : copyId, 
1232                 cancel_time : null, 
1233                 fulfillment_time : null,
1234                 capture_time : {'<>' : null}
1235             }, {
1236                 flesh : 2,
1237                 flesh_fields : {
1238                     ahr : ['requestor', 'usr'],
1239                     au  : ['card']
1240                 }
1241             }
1242         ).then(null, null, function(hold) {
1243             $scope.hold = hold;
1244             hold.pickup_lib(egCore.org.get(hold.pickup_lib()));
1245             if (hold.current_shelf_lib()) {
1246                 hold.current_shelf_lib(
1247                     egCore.org.get(hold.current_shelf_lib()));
1248             }
1249             hold.behind_desk(Boolean(hold.behind_desk() == 't'));
1250         });
1251     }
1252
1253     function loadTransits() {
1254         delete $scope.transit;
1255         delete $scope.hold_transit;
1256         if (!copyId) return;
1257
1258         egCore.pcrud.search('atc', 
1259             {target_copy : copyId},
1260             {order_by : {atc : 'source_send_time DESC'}}
1261
1262         ).then(null, null, function(transit) {
1263             $scope.transit = transit;
1264             transit.source(egCore.org.get(transit.source()));
1265             transit.dest(egCore.org.get(transit.dest()));
1266         })
1267     }
1268
1269
1270     // we don't need all data on all tabs, so fetch what's needed when needed.
1271     function loadTabData() {
1272         switch($scope.tab) {
1273             case 'summary':
1274                 loadCurrentCirc();
1275                 loadCircCounts();
1276                 break;
1277
1278             case 'circs':
1279                 loadCurrentCirc(true);
1280                 break;
1281
1282             case 'circ_list':
1283                 loadCircHistory();
1284                 break;
1285
1286             case 'holds':
1287                 loadHolds()
1288                 loadTransits();
1289                 break;
1290
1291             case 'triggered_events':
1292                 var url = $location.absUrl().replace(/\/staff.*/, '/actor/user/event_log');
1293                 url += '?copy_id=' + encodeURIComponent(copyId);
1294                 $scope.triggered_events_url = url;
1295                 $scope.funcs = {};
1296         }
1297
1298         if ($scope.edit) {
1299             egCore.net.request(
1300                 'open-ils.actor',
1301                 'open-ils.actor.anon_cache.set_value',
1302                 null, 'edit-these-copies', {
1303                     record_id: $scope.recordId,
1304                     copies: [copyId],
1305                     hide_vols : true,
1306                     hide_copies : false
1307                 }
1308             ).then(function(key) {
1309                 if (key) {
1310                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
1311                     $window.location.href = url;
1312                 } else {
1313                     alert('Could not create anonymous cache key!');
1314                 }
1315             });
1316         }
1317
1318         return;
1319     }
1320
1321     $scope.context.toggleDisplay = function() {
1322         $location.path('/cat/item/search');
1323     }
1324
1325     // handle the barcode scan box, which will replace our current copy
1326     $scope.context.search = function(args) {
1327         loadCopy(args.barcode).then(loadTabData);
1328     }
1329
1330     $scope.context.show_triggered_events = function() {
1331         $location.path('/cat/item/' + copyId + '/triggered_events');
1332     }
1333
1334     loadCopy().then(loadTabData);
1335 }])