]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/cat/bucket/record/app.js
LP1739293 Record merge horizontal; optional holdings
[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     // opens the record merge dialog
561     $scope.openRecordMergeDialog = function(records) {
562         $uibModal.open({
563             templateUrl: './cat/bucket/record/t_merge_records',
564             backdrop: 'static',
565             size: 'lg',
566             windowClass: 'eg-wide-modal',
567             controller:
568                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
569                 $scope.records = [];
570                 $scope.lead_id = 0;
571                 $scope.merge_profile = null;
572                 $scope.lead = { marc_xml : null };
573                 $scope.editing_inplace = false;
574                 $scope.showHoldings = false;
575                 angular.forEach(records, function(rec) {
576                     $scope.records.push({ id : rec.id });
577                 });
578                 $scope.ok = function() {
579                     $uibModalInstance.close({
580                         lead_id : $scope.lead_id,
581                         records : $scope.records,
582                         merge_profile : $scope.merge_profile,
583                         lead : $scope.lead
584                     });
585                 }
586                 $scope.cancel = function () { $uibModalInstance.dismiss() }
587
588                 $scope.merge_marc = function() {
589                     // need lead, at least one sub, and a merge profile
590                     if (!$scope.lead_id) return;
591                     if (!$scope.merge_profile) return;
592
593                     if (!$scope.records.length) {
594                         // if we got here, the last subordinate record
595                         // was likely removed, so let's refresh the
596                         // lead for the sake of a consistent display
597                         egCore.pcrud.retrieve('bre', $scope.lead_id)
598                         .then(function(rec) {
599                             $scope.lead.marc_xml = rec.marc();
600                         });
601                         return;
602                     }
603
604                     var recs = $scope.records.map(function(val) { return val.id; });
605                     recs.unshift($scope.lead_id);
606                     egCore.net.request(
607                         'open-ils.cat',
608                         'open-ils.cat.merge.biblio.per_profile',
609                         egCore.auth.token(),
610                         $scope.merge_profile,
611                         recs
612                     ).then(function(merged) {
613                         if (merged) $scope.lead.marc_xml = merged;
614                     });
615                 }
616                 $scope.$watch('merge_profile', function(newVal, oldVal) {
617                     if (newVal && newVal !== oldVal) {
618                         $scope.merge_marc();
619                     }
620                 });
621
622                 $scope.use_as_lead = function(rec) {
623                     if ($scope.lead_id) {
624                         $scope.records.push({ id : $scope.lead_id });
625                     }
626                     $scope.lead_id = rec.id;
627                     $scope.drop(rec);
628
629                     egCore.pcrud.retrieve('bre', $scope.lead_id)
630                     .then(function(rec) {
631                         $scope.lead.marc_xml = rec.marc();
632                         $scope.merge_marc();
633                     });
634                 }
635                 $scope.drop = function(rec) {
636                     angular.forEach($scope.records, function(val, i) {
637                         if (rec == $scope.records[i]) {
638                             $scope.records.splice(i, 1);
639                         }
640                     });
641                     $scope.merge_marc();
642                 }
643                 $scope.post_edit_inplace = function() {
644                     $scope.editing_inplace = false;
645                 }
646                 $scope.edit_lead_inplace = function() {
647                     $scope.editing_inplace = true;
648                 }
649                 $scope.edit_lead = function() {
650                     var lead = { marc_xml : $scope.lead.marc_xml };
651
652                     // passing the on-save callback this way is a
653                     // hack - this invocation of the MARC editor doesn't
654                     // need it, but for some reason using this stomps
655                     // over the callback set by the other MARC editor
656                     // instance
657                     var callback = $scope.post_edit_inplace;
658
659                     $uibModal.open({
660                         templateUrl: './cat/bucket/record/t_edit_lead_record',
661                         backdrop: 'static',
662                         size: 'lg',
663                         controller:
664                             ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
665                             $scope.focusMe = true;
666                             $scope.lead = lead;
667                             $scope.dirty_flag = false;
668                             $scope.ok = function() { $uibModalInstance.close() }
669                             $scope.cancel = function () { $uibModalInstance.dismiss() }
670                             $scope.on_save = callback;
671                         }]
672                     }).result.then(function() {
673                         $scope.lead.marc_xml = lead.marc_xml;
674                     });
675                 };
676             }]
677         }).result.then(function (args) {
678             if (!args.lead_id) return;
679             if (!args.records.length) return;
680
681             function update_bib() {
682                 if (args.merge_profile) {
683                     return egCore.pcrud.retrieve('bre', args.lead_id)
684                     .then(function(rec) {
685                         rec.marc(args.lead.marc_xml);
686                         rec.edit_date('now');
687                         rec.editor(egCore.auth.user().id());
688                         return egCore.pcrud.update(rec);
689                     });
690                 } else {
691                     return $q.when();
692                 }
693             }
694
695             update_bib().then(function() {
696                 egCore.net.request(
697                     'open-ils.cat',
698                     'open-ils.cat.biblio.records.merge',
699                     egCore.auth.token(),
700                     args.lead_id,
701                     args.records.map(function(val) { return val.id; })
702                 ).then(function() {
703                     $window.open(egCore.env.basePath + 'cat/catalog/record/' + args.lead_id);
704                 });
705             });
706         });
707     }
708
709     $scope.showRecords = function(records) {
710         // TODO: probably want to set a limit on the number of
711         //       new tabs one could choose to open at once
712         angular.forEach(records, function(rec) {
713             var url = egCore.env.basePath +
714                       'cat/catalog/record/' +
715                       rec.id;
716             $timeout(function() { $window.open(url, '_blank') });
717         });
718     }
719
720     $scope.batchEdit = function() {
721         var url = egCore.env.basePath +
722                   'cat/catalog/batchEdit/bucket/' + $scope.bucketId;
723         $timeout(function() { $window.open(url, '_blank') });
724     }
725
726     $scope.detachRecords = function(records) {
727         var promises = [];
728         angular.forEach(records, function(rec) {
729             var item = bucketSvc.currentBucket.items().filter(
730                 function(i) {
731                     return (i.target_biblio_record_entry() == rec.id)
732                 }
733             );
734             if (item.length)
735                 promises.push(bucketSvc.detachRecord(item[0].id()));
736         });
737
738         bucketSvc.bucketNeedsRefresh = true;
739         return $q.all(promises).then(drawBucket);
740     }
741
742     $scope.deleteRecordsFromCatalog = function(records) {
743         egConfirmDialog.open(
744             egCore.strings.CONFIRM_DELETE_RECORD_BUCKET_ITEMS_FROM_CATALOG,
745             '',
746             {}
747         ).result.then(function() {
748             var promises = [];
749             angular.forEach(records, function(rec) {
750                 promises.push(bucketSvc.deleteRecordFromCatalog(rec.id));
751             });
752             bucketSvc.bucketNeedsRefresh = true;
753             return $q.all(promises).then(function(results) {
754                 var failures = results.filter(function(result) {
755                     return egCore.evt.parse(result);
756                 }).map(function(result) {
757                     var evt = egCore.evt.parse(result);
758                     if (evt) {
759                         return { recordId: evt.payload, desc: evt.desc };
760                     }
761                 });
762                 if (failures.length) {
763                     $uibModal.open({
764                         templateUrl: './cat/bucket/record/t_records_not_deleted',
765                         backdrop: 'static',
766                         controller :
767                             ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
768                             $scope.failures = failures;
769                             $scope.ok = function() { $uibModalInstance.close() }
770                             $scope.cancel = function() { $uibModalInstance.dismiss() }
771                             }]
772                     });
773                 }
774                 drawBucket();
775             });
776         });
777     }
778
779     // fetch the bucket;  on error show the not-allowed message
780     if ($scope.bucketId) 
781         drawBucket()['catch'](function() { $scope.forbidden = true });
782 }])