]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/cat/bucket/record/app.js
webstaff: go to lead record after merge from bucket
[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?|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'
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             service.currentBucket = bucket;
170             deferred.resolve(bucket);
171         });
172
173         return deferred.promise;
174     }
175
176     // deletes a single container item from a bucket by container item ID.
177     // promise is rejected on failure
178     service.detachRecord = function(itemId) {
179         var deferred = $q.defer();
180         egCore.net.request(
181             'open-ils.actor',
182             'open-ils.actor.container.item.delete',
183             egCore.auth.token(), 'biblio', itemId
184         ).then(function(resp) { 
185             var evt = egCore.evt.parse(resp);
186             if (evt) {
187                 console.error(evt);
188                 deferred.reject(evt);
189                 return;
190             }
191             console.log('detached bucket item ' + itemId);
192             deferred.resolve(resp);
193         });
194
195         return deferred.promise;
196     }
197
198     service.deleteRecordFromCatalog = function(recordId) {
199         var deferred = $q.defer();
200
201         egCore.net.request(
202             'open-ils.cat',
203             'open-ils.cat.biblio.record_entry.delete',
204             egCore.auth.token(), recordId
205         ).then(function(resp) { 
206             // rather than rejecting the promise in the
207             // case of a failure, we'll let the caller
208             // look for errors -- doing this because AngularJS
209             // does not have a native $q.allSettled() yet.
210             deferred.resolve(resp);
211         });
212         
213         return deferred.promise;
214     }
215
216     // delete bucket by ID.
217     // resolved w/ response on successful delete,
218     // rejected otherwise.
219     service.deleteBucket = function(id) {
220         var deferred = $q.defer();
221         egCore.net.request(
222             'open-ils.actor',
223             'open-ils.actor.container.full_delete',
224             egCore.auth.token(), 'biblio', id
225         ).then(function(resp) {
226             var evt = egCore.evt.parse(resp);
227             if (evt) {
228                 console.error(evt);
229                 deferred.reject(evt);
230                 return;
231             }
232             deferred.resolve(resp);
233         });
234         return deferred.promise;
235     }
236
237     return service;
238 }])
239
240 /**
241  * Top-level controller.  
242  * Hosts functions needed by all controllers.
243  */
244 .controller('RecordBucketCtrl',
245        ['$scope','$location','$q','$timeout','$modal',
246         '$window','egCore','bucketSvc',
247 function($scope,  $location,  $q,  $timeout,  $modal,  
248          $window,  egCore,  bucketSvc) {
249
250     $scope.bucketSvc = bucketSvc;
251     $scope.bucket = function() { return bucketSvc.currentBucket }
252
253     // tabs: search, pending, view
254     $scope.setTab = function(tab) { 
255         $scope.tab = tab;
256
257         // for bucket selector; must be called after route resolve
258         bucketSvc.fetchUserBuckets(); 
259     };
260
261     $scope.loadBucketFromMenu = function(item, bucket) {
262         if (bucket) return $scope.loadBucket(bucket.id());
263     }
264
265     $scope.loadBucket = function(id) {
266         $location.path(
267             '/cat/bucket/record/' + 
268                 $scope.tab + '/' + encodeURIComponent(id));
269     }
270
271     $scope.addToBucket = function(recs) {
272         if (recs.length == 0) return;
273         bucketSvc.bucketNeedsRefresh = true;
274
275         angular.forEach(recs,
276             function(rec) {
277                 var item = new egCore.idl.cbrebi();
278                 item.bucket(bucketSvc.currentBucket.id());
279                 item.target_biblio_record_entry(rec.id);
280                 egCore.net.request(
281                     'open-ils.actor',
282                     'open-ils.actor.container.item.create', 
283                     egCore.auth.token(), 'biblio', item
284                 ).then(function(resp) {
285
286                     // HACK: add the IDs of the added items so that the size
287                     // of the view list will grow (and update any UI looking at
288                     // the list size).  The data stored is inconsistent, but since
289                     // we are forcing a bucket refresh on the next rendering of 
290                     // the view pane, the list will be repaired.
291                     bucketSvc.currentBucket.items().push(resp);
292                 });
293             }
294         );
295     }
296
297     $scope.openCreateBucketDialog = function() {
298         $modal.open({
299             templateUrl: './cat/bucket/record/t_bucket_create',
300             controller: 
301                 ['$scope', '$modalInstance', function($scope, $modalInstance) {
302                 $scope.focusMe = true;
303                 $scope.ok = function(args) { $modalInstance.close(args) }
304                 $scope.cancel = function () { $modalInstance.dismiss() }
305             }]
306         }).result.then(function (args) {
307             if (!args || !args.name) return;
308             bucketSvc.createBucket(args.name, args.desc).then(
309                 function(id) {
310                     if (!id) return;
311                     bucketSvc.viewList = [];
312                     bucketSvc.allBuckets = []; // reset
313                     bucketSvc.currentBucket = null;
314                     $location.path(
315                         '/cat/bucket/record/' + $scope.tab + '/' + id);
316                 }
317             );
318         });
319     }
320
321     $scope.openEditBucketDialog = function() {
322         $modal.open({
323             templateUrl: './cat/bucket/record/t_bucket_edit',
324             controller: 
325                 ['$scope', '$modalInstance', function($scope, $modalInstance) {
326                 $scope.focusMe = true;
327                 $scope.args = {
328                     name : bucketSvc.currentBucket.name(),
329                     desc : bucketSvc.currentBucket.description(),
330                     pub : bucketSvc.currentBucket.pub() == 't'
331                 };
332                 $scope.ok = function(args) { 
333                     if (!args) return;
334                     $scope.actionPending = true;
335                     args.pub = args.pub ? 't' : 'f';
336                     // close the dialog after edit has completed
337                     bucketSvc.editBucket(args).then(
338                         function() { $modalInstance.close() });
339                 }
340                 $scope.cancel = function () { $modalInstance.dismiss() }
341             }]
342         })
343     }
344
345
346     // opens the delete confirmation and deletes the current
347     // bucket if the user confirms.
348     $scope.openDeleteBucketDialog = function() {
349         $modal.open({
350             templateUrl: './cat/bucket/record/t_bucket_delete',
351             controller : 
352                 ['$scope', '$modalInstance', function($scope, $modalInstance) {
353                 $scope.bucket = function() { return bucketSvc.currentBucket }
354                 $scope.ok = function() { $modalInstance.close() }
355                 $scope.cancel = function() { $modalInstance.dismiss() }
356             }]
357         }).result.then(function () {
358             bucketSvc.deleteBucket(bucketSvc.currentBucket.id())
359             .then(function() {
360                 bucketSvc.allBuckets = [];
361                 $location.path('/cat/bucket/record/view');
362             });
363         });
364     }
365
366     // retrieves the requested bucket by ID
367     $scope.openSharedBucketDialog = function() {
368         $modal.open({
369             templateUrl: './cat/bucket/record/t_load_shared',
370             controller : 
371                 ['$scope', '$modalInstance', function($scope, $modalInstance) {
372                 $scope.focusMe = true;
373                 $scope.ok = function(args) { 
374                     if (args && args.id) {
375                         $modalInstance.close(args.id) 
376                     }
377                 }
378                 $scope.cancel = function() { $modalInstance.dismiss() }
379             }]
380         }).result.then(function(id) {
381             // RecordBucketCtrl $scope is not inherited by the
382             // modal, so we need to call loadBucket from the 
383             // promise resolver.
384             $scope.loadBucket(id);
385         });
386     }
387
388     // opens the record export dialog
389     $scope.openExportBucketDialog = function() {
390         $modal.open({
391             templateUrl: './cat/bucket/record/t_bucket_export',
392             controller : 
393                 ['$scope', '$modalInstance', function($scope, $modalInstance) {
394                 $scope.args = {format : 'XML', encoding : 'UTF-8'}; // defaults
395                 $scope.ok = function(args) { $modalInstance.close(args) }
396                 $scope.cancel = function() { $modalInstance.dismiss() }
397             }]
398         }).result.then(function (args) {
399             if (!args) return;
400             args.containerid = bucketSvc.currentBucket.id();
401
402             var url = '/exporter?containerid=' + args.containerid + 
403                 '&format=' + args.format + '&encoding=' + args.encoding;
404
405             if (args.holdings) url += '&holdings=1';
406
407             // TODO: improve auth cookie handling so this isn't necessary.
408             // today the cookie path is too specific (/eg/staff) for non-staff
409             // UIs to access it.  See services/auth.js
410             url += '&ses=' + egCore.auth.token(); 
411
412             $timeout(function() { $window.open(url) });
413         });
414     }
415 }])
416
417 .controller('SearchCtrl',
418        ['$scope','$routeParams','egCore','bucketSvc',
419 function($scope,  $routeParams,  egCore , bucketSvc) {
420
421     $scope.setTab('search');
422     $scope.focusMe = true;
423     var idQueryHash = {};
424
425     function generateQuery() {
426         if (bucketSvc.queryRecords.length)
427             return {id : bucketSvc.queryRecords};
428         else 
429             return null;
430     }
431
432     $scope.gridControls = {
433         setQuery : function() {return generateQuery()},
434         setSort : function() {return ['id']}
435     }
436
437     // add selected items directly to the pending list
438     $scope.addToPending = function(recs) {
439         angular.forEach(recs, function(rec) {
440             if (bucketSvc.pendingList.filter( // remove dupes
441                 function(r) {return r.id == rec.id}).length) return;
442             bucketSvc.pendingList.push(rec);
443         });
444     }
445
446     $scope.search = function() {
447         $scope.searchList = [];
448         $scope.searchInProgress = true;
449         bucketSvc.queryRecords = [];
450
451         egCore.net.request(
452             'open-ils.search',
453             'open-ils.search.biblio.multiclass.query.staff', {   
454                 limit : 500 // meh
455             }, bucketSvc.queryString, true
456         ).then(function(resp) {
457             bucketSvc.queryRecords = 
458                 resp.ids.map(function(id){return id[0]});
459             $scope.gridControls.setQuery(generateQuery());
460         })['finally'](function() {
461             $scope.searchInProgress = false;
462         });
463     }
464
465     if ($routeParams.id && 
466         (!bucketSvc.currentBucket || 
467             bucketSvc.currentBucket.id() != $routeParams.id)) {
468         // user has accessed this page cold with a bucket ID.
469         // fetch the bucket for display, then set the totalCount
470         // (also for display), but avoid fully fetching the bucket,
471         // since it's premature, in this UI.
472         bucketSvc.fetchBucket($routeParams.id);
473     }
474 }])
475
476 .controller('PendingCtrl',
477        ['$scope','$routeParams','bucketSvc','egGridDataProvider',
478 function($scope,  $routeParams,  bucketSvc , egGridDataProvider) {
479     $scope.setTab('pending');
480
481     var provider = egGridDataProvider.instance({});
482     provider.get = function(offset, count) {
483         return provider.arrayNotifier(
484             bucketSvc.pendingList, offset, count);
485     }
486     $scope.gridDataProvider = provider;
487
488     $scope.resetPendingList = function() {
489         bucketSvc.pendingList = [];
490         $scope.gridDataProvider.refresh();
491     }
492     
493
494     if ($routeParams.id && 
495         (!bucketSvc.currentBucket || 
496             bucketSvc.currentBucket.id() != $routeParams.id)) {
497         // user has accessed this page cold with a bucket ID.
498         // fetch the bucket for display, then set the totalCount
499         // (also for display), but avoid fully fetching the bucket,
500         // since it's premature, in this UI.
501         bucketSvc.fetchBucket($routeParams.id);
502     }
503 }])
504
505 .controller('ViewCtrl',
506        ['$scope','$q','$routeParams','bucketSvc','egCore','$window',
507         '$timeout','egConfirmDialog','$modal','egHolds',
508 function($scope,  $q , $routeParams,  bucketSvc,  egCore,  $window,
509          $timeout,  egConfirmDialog,  $modal,  egHolds) {
510
511     $scope.setTab('view');
512     $scope.bucketId = $routeParams.id;
513
514     var query;
515     $scope.gridControls = {
516         setQuery : function(q) {
517             if (q) query = q;
518             return query;
519         }
520     };
521
522     function drawBucket() {
523         return bucketSvc.fetchBucket($scope.bucketId).then(
524             function(bucket) {
525                 var ids = bucket.items().map(
526                     function(i){return i.target_biblio_record_entry()}
527                 );
528                 if (ids.length) {
529                     $scope.gridControls.setQuery({id : ids});
530                 } else {
531                     $scope.gridControls.setQuery({});
532                 }
533             }
534         );
535     }
536
537     // runs the transfer title holds action
538     $scope.transfer_holds_to_marked = function(records) {
539         var bib_ids = records.map(function(val) { return val.id; })
540         egHolds.transfer_all_bib_holds_to_marked_title(bib_ids);
541     }
542
543     // opens the record merge dialog
544     $scope.openRecordMergeDialog = function(records) {
545         $modal.open({
546             templateUrl: './cat/bucket/record/t_merge_records',
547             size: 'lg',
548             windowClass: 'eg-wide-modal',
549             controller:
550                 ['$scope', '$modalInstance', function($scope, $modalInstance) {
551                 $scope.records = [];
552                 $scope.lead_id = 0;
553                 angular.forEach(records, function(rec) {
554                     $scope.records.push({ id : rec.id });
555                 });
556                 $scope.ok = function() {
557                     $modalInstance.close({
558                         lead_id : $scope.lead_id,
559                         records : $scope.records
560                     });
561                 }
562                 $scope.cancel = function () { $modalInstance.dismiss() }
563                 $scope.use_as_lead = function(rec) {
564                     if ($scope.lead_id) {
565                         $scope.records.push({ id : $scope.lead_id });
566                     }
567                     $scope.lead_id = rec.id;
568                     $scope.drop(rec);
569                 }
570                 $scope.drop = function(rec) {
571                     angular.forEach($scope.records, function(val, i) {
572                         if (rec == $scope.records[i]) {
573                             $scope.records.splice(i, 1);
574                         }
575                     });
576                 }
577                 $scope.edit_lead = function() {
578                     var lead_id = $scope.lead_id;
579                     $modal.open({
580                         templateUrl: './cat/bucket/record/t_edit_lead_record',
581                         size: 'lg',
582                         controller:
583                             ['$scope', '$modalInstance', function($scope, $modalInstance) {
584                             $scope.focusMe = true;
585                             $scope.record_id = lead_id;
586                             $scope.dirty_flag = false;
587                             $scope.ok = function() { $modalInstance.close() }
588                             $scope.cancel = function () { $modalInstance.dismiss() }
589                         }]
590                     }).result.then(function() {
591                         // TODO: need a way to force a refresh of the egRecordHtml, as
592                         // the record ID does not change
593                     });
594                 };
595             }]
596         }).result.then(function (args) {
597             if (!args.lead_id) return;
598             if (!args.records.length) return;
599             egCore.net.request(
600                 'open-ils.cat',
601                 'open-ils.cat.biblio.records.merge',
602                 egCore.auth.token(),
603                 args.lead_id,
604                 args.records.map(function(val) { return val.id; })
605             ).then(function() {
606                 $window.location.href =
607                     egCore.env.basePath + 'cat/catalog/record/' + args.lead_id;
608             });
609         });
610     }
611
612     $scope.showRecords = function(records) {
613         // TODO: probably want to set a limit on the number of
614         //       new tabs one could choose to open at once
615         angular.forEach(records, function(rec) {
616             var url = egCore.env.basePath +
617                       'cat/catalog/record/' +
618                       rec.id;
619             $timeout(function() { $window.open(url, '_blank') });
620         });
621     }
622
623     $scope.batchEdit = function() {
624         var url = egCore.env.basePath +
625                   'cat/catalog/batchEdit/bucket/' + $scope.bucketId;
626         $timeout(function() { $window.open(url, '_blank') });
627     }
628
629     $scope.detachRecords = function(records) {
630         var promises = [];
631         angular.forEach(records, function(rec) {
632             var item = bucketSvc.currentBucket.items().filter(
633                 function(i) {
634                     return (i.target_biblio_record_entry() == rec.id)
635                 }
636             );
637             if (item.length)
638                 promises.push(bucketSvc.detachRecord(item[0].id()));
639         });
640
641         bucketSvc.bucketNeedsRefresh = true;
642         return $q.all(promises).then(drawBucket);
643     }
644
645     $scope.deleteRecordsFromCatalog = function(records) {
646         egConfirmDialog.open(
647             egCore.strings.CONFIRM_DELETE_RECORD_BUCKET_ITEMS_FROM_CATALOG,
648             '',
649             {}
650         ).result.then(function() {
651             var promises = [];
652             angular.forEach(records, function(rec) {
653                 promises.push(bucketSvc.deleteRecordFromCatalog(rec.id));
654             });
655             bucketSvc.bucketNeedsRefresh = true;
656             return $q.all(promises).then(function(results) {
657                 var failures = results.filter(function(result) {
658                     return egCore.evt.parse(result);
659                 }).map(function(result) {
660                     var evt = egCore.evt.parse(result);
661                     if (evt) {
662                         return { recordId: evt.payload, desc: evt.desc };
663                     }
664                 });
665                 if (failures.length) {
666                     $modal.open({
667                         templateUrl: './cat/bucket/record/t_records_not_deleted',
668                         controller :
669                             ['$scope', '$modalInstance', function($scope, $modalInstance) {
670                             $scope.failures = failures;
671                             $scope.ok = function() { $modalInstance.close() }
672                             $scope.cancel = function() { $modalInstance.dismiss() }
673                             }]
674                     });
675                 }
676                 drawBucket();
677             });
678         });
679     }
680
681     // fetch the bucket;  on error show the not-allowed message
682     if ($scope.bucketId) 
683         drawBucket()['catch'](function() { $scope.forbidden = true });
684 }])