8aaa4db80b413dd87ac499ce5d437e08546c4811
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / cat / bucket / record / app.js
1 /**
2  * Catalog Record Buckets
3  *
4  * Known Issues
5  *
6  * add-all actions only add visible/fetched items.
7  * remove all from bucket UI leaves busted pagination 
8  *   -- apply a refresh after item removal?
9  * problems with bucket view fetching by record ID instead of bucket item:
10  *   -- dupe bibs always sort to the bottom
11  *   -- dupe bibs result in more records displayed per page than requested
12  *   -- item 'pos' ordering is not honored on initial load.
13  */
14
15 angular.module('egCatRecordBuckets', 
16     ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod', 'egMarcMod', 'egHoldingsMod'])
17
18 .config(function($routeProvider, $locationProvider, $compileProvider) {
19     $locationProvider.html5Mode(true);
20     $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|mailto|blob):/); // grid export
21         
22     var resolver = {delay : function(egStartup) {return egStartup.go()}};
23
24     $routeProvider.when('/cat/bucket/record/search/:id', {
25         templateUrl: './cat/bucket/record/t_search',
26         controller: 'SearchCtrl',
27         resolve : resolver
28     });
29     
30     $routeProvider.when('/cat/bucket/record/search', {
31         templateUrl: './cat/bucket/record/t_search',
32         controller: 'SearchCtrl',
33         resolve : resolver
34     });
35
36     $routeProvider.when('/cat/bucket/record/pending/:id', {
37         templateUrl: './cat/bucket/record/t_pending',
38         controller: 'PendingCtrl',
39         resolve : resolver
40     });
41
42     $routeProvider.when('/cat/bucket/record/pending', {
43         templateUrl: './cat/bucket/record/t_pending',
44         controller: 'PendingCtrl',
45         resolve : resolver
46     });
47
48     $routeProvider.when('/cat/bucket/record/view/:id', {
49         templateUrl: './cat/bucket/record/t_view',
50         controller: 'ViewCtrl',
51         resolve : resolver
52     });
53
54     $routeProvider.when('/cat/bucket/record/view', {
55         templateUrl: './cat/bucket/record/t_view',
56         controller: 'ViewCtrl',
57         resolve : resolver
58     });
59
60     // default page / bucket view
61     $routeProvider.otherwise({redirectTo : '/cat/bucket/record/view'});
62 })
63
64 /**
65  * bucketSvc allows us to communicate between the search,
66  * pending, and view controllers.  It also allows us to cache
67  * data for each so that data reloads are not needed on every 
68  * tab click (i.e. route persistence).
69  */
70 .factory('bucketSvc', ['$q','egCore', function($q,  egCore) { 
71
72     var service = {
73         allBuckets : [], // un-fleshed user buckets
74         queryString : '', // last run query
75         queryRecords : [], // last run query results
76         currentBucket : null, // currently viewed bucket
77
78         // per-page list collections
79         searchList  : [],
80         pendingList : [],
81         viewList  : [],
82
83         // fetches all staff/biblio buckets for the authenticated user
84         // this function may only be called after startup.
85         fetchUserBuckets : function(force) {
86             if (this.allBuckets.length && !force) return;
87             var self = this;
88             return egCore.net.request(
89                 'open-ils.actor',
90                 'open-ils.actor.container.retrieve_by_class.authoritative',
91                 egCore.auth.token(), egCore.auth.user().id(), 
92                 'biblio', ['staff_client', 'vandelay_queue']
93             ).then(function(buckets) { self.allBuckets = buckets });
94         },
95
96         createBucket : function(name, desc) {
97             var deferred = $q.defer();
98             var bucket = new egCore.idl.cbreb();
99             bucket.owner(egCore.auth.user().id());
100             bucket.name(name);
101             bucket.description(desc || '');
102             bucket.btype('staff_client');
103
104             egCore.net.request(
105                 'open-ils.actor',
106                 'open-ils.actor.container.create',
107                 egCore.auth.token(), 'biblio', bucket
108             ).then(function(resp) {
109                 if (resp) {
110                     if (typeof resp == 'object') {
111                         console.error('bucket create error: ' + js2JSON(resp));
112                         deferred.reject();
113                     } else {
114                         deferred.resolve(resp);
115                     }
116                 }
117             });
118
119             return deferred.promise;
120         },
121
122         // edit the current bucket.  since we edit the 
123         // local object, there's no need to re-fetch.
124         editBucket : function(args) {
125             var bucket = service.currentBucket;
126             bucket.name(args.name);
127             bucket.description(args.desc);
128             bucket.pub(args.pub);
129             return egCore.net.request(
130                 'open-ils.actor',
131                 'open-ils.actor.container.update',
132                 egCore.auth.token(), 'biblio', bucket
133             );
134         }
135     }
136
137     // returns 1 if full refresh is needed
138     // returns 2 if list refresh only is needed
139     service.bucketRefreshLevel = function(id) {
140         if (!service.currentBucket) return 1;
141         if (service.bucketNeedsRefresh) {
142             service.bucketNeedsRefresh = false;
143             service.currentBucket = null;
144             return 1;
145         }
146         if (service.currentBucket.id() != id) return 1;
147         return 2;
148     }
149
150     // returns a promise, resolved with bucket, rejected if bucket is
151     // not fetch-able
152     service.fetchBucket = function(id) {
153         var refresh = service.bucketRefreshLevel(id);
154         if (refresh == 2) return $q.when(service.currentBucket);
155
156         var deferred = $q.defer();
157
158         egCore.net.request(
159             'open-ils.actor',
160             'open-ils.actor.container.flesh.authoritative',
161             egCore.auth.token(), 'biblio', id
162         ).then(function(bucket) {
163             var evt = egCore.evt.parse(bucket);
164             if (evt) {
165                 console.debug(evt);
166                 deferred.reject(evt);
167                 return;
168             }
169             egCore.pcrud.retrieve(
170                 'au', bucket.owner(),
171                 {flesh : 1, flesh_fields : {au : ["card"]}}
172             ).then(function(patron) {
173                 // On the off chance no barcode is present (it's not 
174                 // required) use the patron username as the identifier.
175                 bucket._owner_ident = patron.card() ? 
176                     patron.card().barcode() : patron.usrname();
177                 bucket._owner_name = patron.family_name();
178                 bucket._owner_ou = egCore.org.get(patron.home_ou()).shortname();
179             });
180
181             service.currentBucket = bucket;
182             deferred.resolve(bucket);
183         });
184
185         return deferred.promise;
186     }
187
188     // deletes a single container item from a bucket by container item ID.
189     // promise is rejected on failure
190     service.detachRecord = function(itemId) {
191         var deferred = $q.defer();
192         egCore.net.request(
193             'open-ils.actor',
194             'open-ils.actor.container.item.delete',
195             egCore.auth.token(), 'biblio', itemId
196         ).then(function(resp) { 
197             var evt = egCore.evt.parse(resp);
198             if (evt) {
199                 console.error(evt);
200                 deferred.reject(evt);
201                 return;
202             }
203             console.log('detached bucket item ' + itemId);
204             deferred.resolve(resp);
205         });
206
207         return deferred.promise;
208     }
209
210     service.deleteRecordFromCatalog = function(recordId) {
211         var deferred = $q.defer();
212
213         egCore.net.request(
214             'open-ils.cat',
215             'open-ils.cat.biblio.record_entry.delete',
216             egCore.auth.token(), recordId
217         ).then(function(resp) { 
218             // rather than rejecting the promise in the
219             // case of a failure, we'll let the caller
220             // look for errors -- doing this because AngularJS
221             // does not have a native $q.allSettled() yet.
222             deferred.resolve(resp);
223         });
224         
225         return deferred.promise;
226     }
227
228     // delete bucket by ID.
229     // resolved w/ response on successful delete,
230     // rejected otherwise.
231     service.deleteBucket = function(id) {
232         var deferred = $q.defer();
233         egCore.net.request(
234             'open-ils.actor',
235             'open-ils.actor.container.full_delete',
236             egCore.auth.token(), 'biblio', id
237         ).then(function(resp) {
238             var evt = egCore.evt.parse(resp);
239             if (evt) {
240                 console.error(evt);
241                 deferred.reject(evt);
242                 return;
243             }
244             deferred.resolve(resp);
245         });
246         return deferred.promise;
247     }
248
249     return service;
250 }])
251
252 /**
253  * Top-level controller.  
254  * Hosts functions needed by all controllers.
255  */
256 .controller('RecordBucketCtrl',
257        ['$scope','$location','$q','$timeout','$uibModal',
258         '$window','egCore','bucketSvc',
259 function($scope,  $location,  $q,  $timeout,  $uibModal,  
260          $window,  egCore,  bucketSvc) {
261
262     $scope.bucketSvc = bucketSvc;
263     $scope.bucket = function() { return bucketSvc.currentBucket }
264
265     // tabs: search, pending, view
266     $scope.setTab = function(tab) { 
267         $scope.tab = tab;
268
269         // for bucket selector; must be called after route resolve
270         bucketSvc.fetchUserBuckets(); 
271     };
272
273     $scope.loadBucketFromMenu = function(item, bucket) {
274         if (bucket) return $scope.loadBucket(bucket.id());
275     }
276
277     $scope.loadBucket = function(id) {
278         $location.path(
279             '/cat/bucket/record/' + 
280                 $scope.tab + '/' + encodeURIComponent(id));
281     }
282
283     $scope.addToBucket = function(recs) {
284         if (recs.length == 0) return;
285         bucketSvc.bucketNeedsRefresh = true;
286
287         angular.forEach(recs,
288             function(rec) {
289                 var item = new egCore.idl.cbrebi();
290                 item.bucket(bucketSvc.currentBucket.id());
291                 item.target_biblio_record_entry(rec.id);
292                 egCore.net.request(
293                     'open-ils.actor',
294                     'open-ils.actor.container.item.create', 
295                     egCore.auth.token(), 'biblio', item
296                 ).then(function(resp) {
297
298                     // HACK: add the IDs of the added items so that the size
299                     // of the view list will grow (and update any UI looking at
300                     // the list size).  The data stored is inconsistent, but since
301                     // we are forcing a bucket refresh on the next rendering of 
302                     // the view pane, the list will be repaired.
303                     bucketSvc.currentBucket.items().push(resp);
304                 });
305             }
306         );
307     }
308
309     $scope.openCreateBucketDialog = function() {
310         $uibModal.open({
311             templateUrl: './cat/bucket/share/t_bucket_create',
312             backdrop: 'static',
313             controller: 
314                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
315                 $scope.focusMe = true;
316                 $scope.ok = function(args) { $uibModalInstance.close(args) }
317                 $scope.cancel = function () { $uibModalInstance.dismiss() }
318             }]
319         }).result.then(function (args) {
320             if (!args || !args.name) return;
321             bucketSvc.createBucket(args.name, args.desc).then(
322                 function(id) {
323                     if (!id) return;
324                     bucketSvc.viewList = [];
325                     bucketSvc.allBuckets = []; // reset
326                     bucketSvc.currentBucket = null;
327                     $location.path(
328                         '/cat/bucket/record/' + $scope.tab + '/' + id);
329                 }
330             );
331         });
332     }
333
334     $scope.openEditBucketDialog = function() {
335         $uibModal.open({
336             templateUrl: './cat/bucket/share/t_bucket_edit',
337             backdrop: 'static',
338             controller: 
339                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
340                 $scope.focusMe = true;
341                 $scope.args = {
342                     name : bucketSvc.currentBucket.name(),
343                     desc : bucketSvc.currentBucket.description(),
344                     pub : bucketSvc.currentBucket.pub() == 't'
345                 };
346                 $scope.ok = function(args) { 
347                     if (!args) return;
348                     $scope.actionPending = true;
349                     args.pub = args.pub ? 't' : 'f';
350                     // close the dialog after edit has completed
351                     bucketSvc.editBucket(args).then(
352                         function() { $uibModalInstance.close() });
353                 }
354                 $scope.cancel = function () { $uibModalInstance.dismiss() }
355             }]
356         })
357     }
358
359
360     // opens the delete confirmation and deletes the current
361     // bucket if the user confirms.
362     $scope.openDeleteBucketDialog = function() {
363         $uibModal.open({
364             templateUrl: './cat/bucket/share/t_bucket_delete',
365             backdrop: 'static',
366             controller : 
367                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
368                 $scope.bucket = function() { return bucketSvc.currentBucket }
369                 $scope.ok = function() { $uibModalInstance.close() }
370                 $scope.cancel = function() { $uibModalInstance.dismiss() }
371             }]
372         }).result.then(function () {
373             bucketSvc.deleteBucket(bucketSvc.currentBucket.id())
374             .then(function() {
375                 bucketSvc.allBuckets = [];
376                 $location.path('/cat/bucket/record/view');
377             });
378         });
379     }
380
381     // retrieves the requested bucket by ID
382     $scope.openSharedBucketDialog = function() {
383         $uibModal.open({
384             templateUrl: './cat/bucket/share/t_load_shared',
385             backdrop: 'static',
386             controller : 
387                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
388                 $scope.focusMe = true;
389                 $scope.ok = function(args) { 
390                     if (args && args.id) {
391                         $uibModalInstance.close(args.id) 
392                     }
393                 }
394                 $scope.cancel = function() { $uibModalInstance.dismiss() }
395             }]
396         }).result.then(function(id) {
397             // RecordBucketCtrl $scope is not inherited by the
398             // modal, so we need to call loadBucket from the 
399             // promise resolver.
400             $scope.loadBucket(id);
401         });
402     }
403
404     // opens the record export dialog
405     $scope.openExportBucketDialog = function() {
406         $uibModal.open({
407             templateUrl: './cat/bucket/record/t_bucket_export',
408             backdrop: 'static',
409             controller : 
410                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
411                 $scope.args = {format : 'XML', encoding : 'UTF-8'}; // defaults
412                 $scope.ok = function(args) { $uibModalInstance.close(args) }
413                 $scope.cancel = function() { $uibModalInstance.dismiss() }
414             }]
415         }).result.then(function (args) {
416             if (!args) return;
417             args.containerid = bucketSvc.currentBucket.id();
418
419             var url = '/exporter?containerid=' + args.containerid + 
420                 '&format=' + args.format + '&encoding=' + args.encoding;
421
422             if (args.holdings) url += '&holdings=1';
423
424             // TODO: improve auth cookie handling so this isn't necessary.
425             // today the cookie path is too specific (/eg/staff) for non-staff
426             // UIs to access it.  See services/auth.js
427             url += '&ses=' + egCore.auth.token(); 
428
429             $timeout(function() { $window.open(url) });
430         });
431     }
432 }])
433
434 .controller('SearchCtrl',
435        ['$scope','$routeParams','egCore','bucketSvc',
436 function($scope,  $routeParams,  egCore , bucketSvc) {
437
438     $scope.setTab('search');
439     $scope.focusMe = true;
440     var idQueryHash = {};
441
442     function generateQuery() {
443         if (bucketSvc.queryRecords.length)
444             return {id : bucketSvc.queryRecords};
445         else 
446             return null;
447     }
448
449     $scope.gridControls = {
450         setQuery : function() {return generateQuery()},
451         setSort : function() {return ['id']}
452     }
453
454     // add selected items directly to the pending list
455     $scope.addToPending = function(recs) {
456         angular.forEach(recs, function(rec) {
457             if (bucketSvc.pendingList.filter( // remove dupes
458                 function(r) {return r.id == rec.id}).length) return;
459             bucketSvc.pendingList.push(rec);
460         });
461     }
462
463     $scope.search = function() {
464         $scope.searchList = [];
465         $scope.searchInProgress = true;
466         bucketSvc.queryRecords = [];
467
468         egCore.net.request(
469             'open-ils.search',
470             'open-ils.search.biblio.multiclass.query.staff', {   
471                 limit : 500 // meh
472             }, bucketSvc.queryString, true
473         ).then(function(resp) {
474             bucketSvc.queryRecords = 
475                 resp.ids.map(function(id){return id[0]});
476             $scope.gridControls.setQuery(generateQuery());
477         })['finally'](function() {
478             $scope.searchInProgress = false;
479         });
480     }
481
482     if ($routeParams.id && 
483         (!bucketSvc.currentBucket || 
484             bucketSvc.currentBucket.id() != $routeParams.id)) {
485         // user has accessed this page cold with a bucket ID.
486         // fetch the bucket for display, then set the totalCount
487         // (also for display), but avoid fully fetching the bucket,
488         // since it's premature, in this UI.
489         bucketSvc.fetchBucket($routeParams.id);
490     }
491 }])
492
493 .controller('PendingCtrl',
494        ['$scope','$routeParams','bucketSvc','egGridDataProvider',
495 function($scope,  $routeParams,  bucketSvc , egGridDataProvider) {
496     $scope.setTab('pending');
497
498     var provider = egGridDataProvider.instance({});
499     provider.get = function(offset, count) {
500         return provider.arrayNotifier(
501             bucketSvc.pendingList, offset, count);
502     }
503     $scope.gridDataProvider = provider;
504
505     $scope.resetPendingList = function() {
506         bucketSvc.pendingList = [];
507         $scope.gridDataProvider.refresh();
508     }
509     
510
511     if ($routeParams.id && 
512         (!bucketSvc.currentBucket || 
513             bucketSvc.currentBucket.id() != $routeParams.id)) {
514         // user has accessed this page cold with a bucket ID.
515         // fetch the bucket for display, then set the totalCount
516         // (also for display), but avoid fully fetching the bucket,
517         // since it's premature, in this UI.
518         bucketSvc.fetchBucket($routeParams.id);
519     }
520 }])
521
522 .controller('ViewCtrl',
523        ['$scope','$q','$routeParams','bucketSvc','egCore','$window',
524         '$timeout','egConfirmDialog','$uibModal','egHolds',
525 function($scope,  $q , $routeParams,  bucketSvc,  egCore,  $window,
526          $timeout,  egConfirmDialog,  $uibModal,  egHolds) {
527
528     $scope.setTab('view');
529     $scope.bucketId = $routeParams.id;
530
531     var query;
532     $scope.gridControls = {
533         setQuery : function(q) {
534             if (q) query = q;
535             return query;
536         }
537     };
538
539     function drawBucket() {
540         return bucketSvc.fetchBucket($scope.bucketId).then(
541             function(bucket) {
542                 var ids = bucket.items().map(
543                     function(i){return i.target_biblio_record_entry()}
544                 );
545                 if (ids.length) {
546                     $scope.gridControls.setQuery({id : ids});
547                 } else {
548                     $scope.gridControls.setQuery({});
549                 }
550             }
551         );
552     }
553
554     // runs the transfer title holds action
555     $scope.transfer_holds_to_marked = function(records) {
556         var bib_ids = records.map(function(val) { return val.id; })
557         egHolds.transfer_all_bib_holds_to_marked_title(bib_ids);
558     }
559
560     // Refresh and update a single bib record.
561     // Returns a promise.
562     function updateOneRecord(recId, marcXml) {
563
564         return egCore.net.request(
565             'open-ils.cat',
566             'open-ils.cat.biblio.record.xml.update',
567             egCore.auth.token(), recId, marcXml
568         ).then(function(result) {
569             var evt = egCore.evt.parse(result);
570             if (evt) {
571                 alert(evt);
572                 return $q.reject(evt);
573             } else {
574                 return result; // bib record
575             }
576         });
577     }
578
579     // opens the record merge dialog
580     $scope.openRecordMergeDialog = function(records) {
581         $uibModal.open({
582             templateUrl: './cat/bucket/record/t_merge_records',
583             backdrop: 'static',
584             size: 'lg',
585             windowClass: 'eg-wide-modal',
586             controller:
587                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
588                 $scope.records = [];
589                 $scope.lead_id = 0;
590                 $scope.merge_profile = null;
591                 $scope.lead = { marc_xml : null };
592                 $scope.editing_inplace = false;
593                 $scope.showHoldings = false;
594                 angular.forEach(records, function(rec) {
595                     $scope.records.push({ id : rec.id });
596                 });
597                 $scope.ok = function() {
598                     $uibModalInstance.close({
599                         lead_id : $scope.lead_id,
600                         records : $scope.records,
601                         merge_profile : $scope.merge_profile,
602                         lead : $scope.lead
603                     });
604                 }
605                 $scope.cancel = function () { $uibModalInstance.dismiss() }
606
607                 $scope.merge_marc = function() {
608                     // need lead, at least one sub, and a merge profile
609                     if (!$scope.lead_id) return;
610                     if (!$scope.merge_profile) return;
611
612                     if (!$scope.records.length) {
613                         // if we got here, the last subordinate record
614                         // was likely removed, so let's refresh the
615                         // lead for the sake of a consistent display
616                         egCore.pcrud.retrieve('bre', $scope.lead_id)
617                         .then(function(rec) {
618                             $scope.lead.marc_xml = rec.marc();
619                         });
620                         return;
621                     }
622
623                     var recs = $scope.records.map(function(val) { return val.id; });
624                     recs.unshift($scope.lead_id);
625                     egCore.net.request(
626                         'open-ils.cat',
627                         'open-ils.cat.merge.biblio.per_profile',
628                         egCore.auth.token(),
629                         $scope.merge_profile,
630                         recs
631                     ).then(function(merged) {
632                         if (merged) $scope.lead.marc_xml = merged;
633                     });
634                 }
635                 $scope.$watch('merge_profile', function(newVal, oldVal) {
636                     if (newVal && newVal !== oldVal) {
637                         $scope.merge_marc();
638                     }
639                 });
640
641                 $scope.use_as_lead = function(rec) {
642                     if ($scope.lead_id) {
643                         $scope.records.push({ id : $scope.lead_id });
644                     }
645                     $scope.lead_id = rec.id;
646                     $scope.drop(rec);
647
648                     egCore.pcrud.retrieve('bre', $scope.lead_id)
649                     .then(function(rec) {
650                         $scope.lead.marc_xml = rec.marc();
651                         $scope.merge_marc();
652                     });
653                 }
654                 $scope.drop = function(rec) {
655                     angular.forEach($scope.records, function(val, i) {
656                         if (rec == $scope.records[i]) {
657                             $scope.records.splice(i, 1);
658                         }
659                     });
660                     $scope.merge_marc();
661                 }
662                 $scope.post_edit_inplace = function() {
663                     $scope.editing_inplace = false;
664                     updateOneRecord($scope.lead_id, $scope.lead.marc_xml);
665                 }
666
667                 $scope.edit_lead_inplace = function() {
668                     $scope.editing_inplace = true;
669                 }
670                 $scope.edit_lead = function() {
671                     var lead = { marc_xml : $scope.lead.marc_xml };
672                     var parentScope = $scope;
673
674                     $uibModal.open({
675                         templateUrl: './cat/bucket/record/t_edit_lead_record',
676                         backdrop: 'static',
677                         size: 'lg',
678                         controller:
679                             ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
680                             $scope.focusMe = true;
681                             $scope.lead = lead;
682                             $scope.dirty_flag = false;
683                             $scope.ok = function() { $uibModalInstance.close() }
684                             $scope.cancel = function () { $uibModalInstance.dismiss() }
685                             $scope.on_save = function() {
686                                 parentScope.lead.marc_xml = $scope.lead.marc_xml;
687                                 parentScope.post_edit_inplace();
688                             }
689                         }]
690                     }).result.then(function() {
691                         $scope.lead.marc_xml = lead.marc_xml;
692                     });
693                 };
694             }]
695         }).result.then(function (args) {
696             if (!args.lead_id) return;
697             if (!args.records.length) return;
698
699             function update_bib() {
700                 if (args.merge_profile) {
701                     return updateOneRecord(args.lead_id, args.lead.marc_xml);
702                 } else {
703                     return $q.when();
704                 }
705             }
706
707             update_bib().then(function() {
708                 egCore.net.request(
709                     'open-ils.cat',
710                     'open-ils.cat.biblio.records.merge',
711                     egCore.auth.token(),
712                     args.lead_id,
713                     args.records.map(function(val) { return val.id; })
714                 ).then(function() {
715                     $window.open(egCore.env.basePath + 'cat/catalog/record/' + args.lead_id);
716                 });
717             });
718         });
719     }
720
721     $scope.showRecords = function(records) {
722         // TODO: probably want to set a limit on the number of
723         //       new tabs one could choose to open at once
724         angular.forEach(records, function(rec) {
725             var url = egCore.env.basePath +
726                       'cat/catalog/record/' +
727                       rec.id;
728             $timeout(function() { $window.open(url, '_blank') });
729         });
730     }
731
732     $scope.batchEdit = function() {
733         var url = egCore.env.basePath +
734                   'cat/catalog/batchEdit/bucket/' + $scope.bucketId;
735         $timeout(function() { $window.open(url, '_blank') });
736     }
737
738     $scope.detachRecords = function(records) {
739         var promises = [];
740         angular.forEach(records, function(rec) {
741             var item = bucketSvc.currentBucket.items().filter(
742                 function(i) {
743                     return (i.target_biblio_record_entry() == rec.id)
744                 }
745             );
746             if (item.length)
747                 promises.push(bucketSvc.detachRecord(item[0].id()));
748         });
749
750         bucketSvc.bucketNeedsRefresh = true;
751         return $q.all(promises).then(drawBucket);
752     }
753
754     $scope.moveToPending = function(records) {
755         angular.forEach(records, function(rec) {
756             if (bucketSvc.pendingList.filter( // remove dupes
757                 function(r) {return r.id == rec.id}).length) return;
758             bucketSvc.pendingList.push(rec);
759         });
760         $scope.detachRecords(records);
761     }
762
763     $scope.deleteRecordsFromCatalog = function(records) {
764         egConfirmDialog.open(
765             egCore.strings.CONFIRM_DELETE_RECORD_BUCKET_ITEMS_FROM_CATALOG,
766             '',
767             {}
768         ).result.then(function() {
769             var promises = [];
770             angular.forEach(records, function(rec) {
771                 promises.push(bucketSvc.deleteRecordFromCatalog(rec.id));
772             });
773             bucketSvc.bucketNeedsRefresh = true;
774             return $q.all(promises).then(function(results) {
775                 var failures = results.filter(function(result) {
776                     return egCore.evt.parse(result);
777                 }).map(function(result) {
778                     var evt = egCore.evt.parse(result);
779                     if (evt) {
780                         return { recordId: evt.payload, desc: evt.desc };
781                     }
782                 });
783                 if (failures.length) {
784                     $uibModal.open({
785                         templateUrl: './cat/bucket/record/t_records_not_deleted',
786                         backdrop: 'static',
787                         controller :
788                             ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
789                             $scope.failures = failures;
790                             $scope.ok = function() { $uibModalInstance.close() }
791                             $scope.cancel = function() { $uibModalInstance.dismiss() }
792                             }]
793                     });
794                 }
795                 drawBucket();
796             });
797         });
798     }
799
800     $scope.need_multiple_selected = function() {
801         var items = $scope.gridControls.selectedItems();
802         if (items.length > 1) return false;
803         return true;
804     }
805
806     // fetch the bucket;  on error show the not-allowed message
807     if ($scope.bucketId) 
808         drawBucket()['catch'](function() { $scope.forbidden = true });
809 }])