]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/cat/bucket/copy/app.js
ebc4bb96c1bf0419f6614c5c8475a39aa6c714e3
[Evergreen.git] / Open-ILS / web / js / ui / default / staff / cat / bucket / copy / app.js
1 /**
2  * Copy 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('egCatCopyBuckets', 
16     ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod'])
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/copy/pending/:id', {
25         templateUrl: './cat/bucket/copy/t_pending',
26         controller: 'PendingCtrl',
27         resolve : resolver
28     });
29
30     $routeProvider.when('/cat/bucket/copy/pending', {
31         templateUrl: './cat/bucket/copy/t_pending',
32         controller: 'PendingCtrl',
33         resolve : resolver
34     });
35
36     $routeProvider.when('/cat/bucket/copy/view/:id', {
37         templateUrl: './cat/bucket/copy/t_view',
38         controller: 'ViewCtrl',
39         resolve : resolver
40     });
41
42     $routeProvider.when('/cat/bucket/copy/view', {
43         templateUrl: './cat/bucket/copy/t_view',
44         controller: 'ViewCtrl',
45         resolve : resolver
46     });
47
48     // default page / bucket view
49     $routeProvider.otherwise({redirectTo : '/cat/bucket/copy/view'});
50 })
51
52 /**
53  * bucketSvc allows us to communicate between the pending
54  * and view controllers.  It also allows us to cache
55  * data for each so that data reloads are not needed on every 
56  * tab click (i.e. route persistence).
57  */
58 .factory('bucketSvc', ['$q','egCore', function($q,  egCore) { 
59
60     var service = {
61         allBuckets : [], // un-fleshed user buckets
62         barcodeString : '', // last scanned barcode
63         barcodeRecords : [], // last scanned barcode results
64         currentBucket : null, // currently viewed bucket
65
66         // per-page list collections
67         pendingList : [],
68         viewList  : [],
69
70         // fetches all staff/copy buckets for the authenticated user
71         // this function may only be called after startup.
72         fetchUserBuckets : function(force) {
73             if (this.allBuckets.length && !force) return;
74             var self = this;
75             return egCore.net.request(
76                 'open-ils.actor',
77                 'open-ils.actor.container.retrieve_by_class.authoritative',
78                 egCore.auth.token(), egCore.auth.user().id(), 
79                 'copy', 'staff_client'
80             ).then(function(buckets) { self.allBuckets = buckets });
81         },
82
83         createBucket : function(name, desc) {
84             var deferred = $q.defer();
85             var bucket = new egCore.idl.ccb();
86             bucket.owner(egCore.auth.user().id());
87             bucket.name(name);
88             bucket.description(desc || '');
89             bucket.btype('staff_client');
90
91             egCore.net.request(
92                 'open-ils.actor',
93                 'open-ils.actor.container.create',
94                 egCore.auth.token(), 'copy', bucket
95             ).then(function(resp) {
96                 if (resp) {
97                     if (typeof resp == 'object') {
98                         console.error('bucket create error: ' + js2JSON(resp));
99                         deferred.reject();
100                     } else {
101                         deferred.resolve(resp);
102                     }
103                 }
104             });
105
106             return deferred.promise;
107         },
108
109         // edit the current bucket.  since we edit the 
110         // local object, there's no need to re-fetch.
111         editBucket : function(args) {
112             var bucket = service.currentBucket;
113             bucket.name(args.name);
114             bucket.description(args.desc);
115             bucket.pub(args.pub);
116             return egCore.net.request(
117                 'open-ils.actor',
118                 'open-ils.actor.container.update',
119                 egCore.auth.token(), 'copy', bucket
120             );
121         }
122     }
123
124     // returns 1 if full refresh is needed
125     // returns 2 if list refresh only is needed
126     service.bucketRefreshLevel = function(id) {
127         if (!service.currentBucket) return 1;
128         if (service.bucketNeedsRefresh) {
129             service.bucketNeedsRefresh = false;
130             service.currentBucket = null;
131             return 1;
132         }
133         if (service.currentBucket.id() != id) return 1;
134         return 2;
135     }
136
137     // returns a promise, resolved with bucket, rejected if bucket is
138     // not fetch-able
139     service.fetchBucket = function(id) {
140         var refresh = service.bucketRefreshLevel(id);
141         if (refresh == 2) return $q.when(service.currentBucket);
142
143         var deferred = $q.defer();
144
145         egCore.net.request(
146             'open-ils.actor',
147             'open-ils.actor.container.flesh.authoritative',
148             egCore.auth.token(), 'copy', id
149         ).then(function(bucket) {
150             var evt = egCore.evt.parse(bucket);
151             if (evt) {
152                 console.debug(evt);
153                 deferred.reject(evt);
154                 return;
155             }
156             service.currentBucket = bucket;
157             deferred.resolve(bucket);
158         });
159
160         return deferred.promise;
161     }
162
163     // deletes a single container item from a bucket by container item ID.
164     // promise is rejected on failure
165     service.detachCopy = function(itemId) {
166         var deferred = $q.defer();
167         egCore.net.request(
168             'open-ils.actor',
169             'open-ils.actor.container.item.delete',
170             egCore.auth.token(), 'copy', itemId
171         ).then(function(resp) { 
172             var evt = egCore.evt.parse(resp);
173             if (evt) {
174                 console.error(evt);
175                 deferred.reject(evt);
176                 return;
177             }
178             console.log('detached bucket item ' + itemId);
179             deferred.resolve(resp);
180         });
181
182         return deferred.promise;
183     }
184
185     // delete bucket by ID.
186     // resolved w/ response on successful delete,
187     // rejected otherwise.
188     service.deleteBucket = function(id) {
189         var deferred = $q.defer();
190         egCore.net.request(
191             'open-ils.actor',
192             'open-ils.actor.container.full_delete',
193             egCore.auth.token(), 'copy', id
194         ).then(function(resp) {
195             var evt = egCore.evt.parse(resp);
196             if (evt) {
197                 console.error(evt);
198                 deferred.reject(evt);
199                 return;
200             }
201             deferred.resolve(resp);
202         });
203         return deferred.promise;
204     }
205
206     return service;
207 }])
208
209 /**
210  * Top-level controller.  
211  * Hosts functions needed by all controllers.
212  */
213 .controller('CopyBucketCtrl',
214        ['$scope','$location','$q','$timeout','$modal',
215         '$window','egCore','bucketSvc',
216 function($scope,  $location,  $q,  $timeout,  $modal,  
217          $window,  egCore,  bucketSvc) {
218
219     $scope.bucketSvc = bucketSvc;
220     $scope.bucket = function() { return bucketSvc.currentBucket }
221
222     // tabs: search, pending, view
223     $scope.setTab = function(tab) { 
224         $scope.tab = tab;
225
226         // for bucket selector; must be called after route resolve
227         bucketSvc.fetchUserBuckets(); 
228     };
229
230     $scope.loadBucketFromMenu = function(item, bucket) {
231         if (bucket) return $scope.loadBucket(bucket.id());
232     }
233
234     $scope.loadBucket = function(id) {
235         $location.path(
236             '/cat/bucket/copy/' + 
237                 $scope.tab + '/' + encodeURIComponent(id));
238     }
239
240     $scope.addToBucket = function(recs) {
241         if (recs.length == 0) return;
242         bucketSvc.bucketNeedsRefresh = true;
243
244         angular.forEach(recs,
245             function(rec) {
246                 var item = new egCore.idl.ccbi();
247                 item.bucket(bucketSvc.currentBucket.id());
248                 item.target_copy(rec.id);
249                 egCore.net.request(
250                     'open-ils.actor',
251                     'open-ils.actor.container.item.create', 
252                     egCore.auth.token(), 'copy', item
253                 ).then(function(resp) {
254
255                     // HACK: add the IDs of the added items so that the size
256                     // of the view list will grow (and update any UI looking at
257                     // the list size).  The data stored is inconsistent, but since
258                     // we are forcing a bucket refresh on the next rendering of 
259                     // the view pane, the list will be repaired.
260                     bucketSvc.currentBucket.items().push(resp);
261                 });
262             }
263         );
264     }
265
266     $scope.openCreateBucketDialog = function() {
267         $modal.open({
268             templateUrl: './cat/bucket/copy/t_bucket_create',
269             controller: 
270                 ['$scope', '$modalInstance', function($scope, $modalInstance) {
271                 $scope.focusMe = true;
272                 $scope.ok = function(args) { $modalInstance.close(args) }
273                 $scope.cancel = function () { $modalInstance.dismiss() }
274             }]
275         }).result.then(function (args) {
276             if (!args || !args.name) return;
277             bucketSvc.createBucket(args.name, args.desc).then(
278                 function(id) {
279                     if (!id) return;
280                     bucketSvc.viewList = [];
281                     bucketSvc.allBuckets = []; // reset
282                     bucketSvc.currentBucket = null;
283                     $location.path(
284                         '/cat/bucket/copy/' + $scope.tab + '/' + id);
285                 }
286             );
287         });
288     }
289
290     $scope.openEditBucketDialog = function() {
291         $modal.open({
292             templateUrl: './cat/bucket/copy/t_bucket_edit',
293             controller: 
294                 ['$scope', '$modalInstance', function($scope, $modalInstance) {
295                 $scope.focusMe = true;
296                 $scope.args = {
297                     name : bucketSvc.currentBucket.name(),
298                     desc : bucketSvc.currentBucket.description(),
299                     pub : bucketSvc.currentBucket.pub() == 't'
300                 };
301                 $scope.ok = function(args) { 
302                     if (!args) return;
303                     $scope.actionPending = true;
304                     args.pub = args.pub ? 't' : 'f';
305                     // close the dialog after edit has completed
306                     bucketSvc.editBucket(args).then(
307                         function() { $modalInstance.close() });
308                 }
309                 $scope.cancel = function () { $modalInstance.dismiss() }
310             }]
311         })
312     }
313
314     // opens the delete confirmation and deletes the current
315     // bucket if the user confirms.
316     $scope.openDeleteBucketDialog = function() {
317         $modal.open({
318             templateUrl: './cat/bucket/copy/t_bucket_delete',
319             controller : 
320                 ['$scope', '$modalInstance', function($scope, $modalInstance) {
321                 $scope.bucket = function() { return bucketSvc.currentBucket }
322                 $scope.ok = function() { $modalInstance.close() }
323                 $scope.cancel = function() { $modalInstance.dismiss() }
324             }]
325         }).result.then(function () {
326             bucketSvc.deleteBucket(bucketSvc.currentBucket.id())
327             .then(function() {
328                 bucketSvc.allBuckets = [];
329                 $location.path('/cat/bucket/copy/view');
330             });
331         });
332     }
333
334     // retrieves the requested bucket by ID
335     $scope.openSharedBucketDialog = function() {
336         $modal.open({
337             templateUrl: './cat/bucket/copy/t_load_shared',
338             controller :
339                 ['$scope', '$modalInstance', function($scope, $modalInstance) {
340                 $scope.focusMe = true;
341                 $scope.ok = function(args) {
342                     if (args && args.id) {
343                         $modalInstance.close(args.id)
344                     }
345                 }
346                 $scope.cancel = function() { $modalInstance.dismiss() }
347             }]
348         }).result.then(function(id) {
349             // RecordBucketCtrl $scope is not inherited by the
350             // modal, so we need to call loadBucket from the
351             // promise resolver.
352             $scope.loadBucket(id);
353         });
354     }
355
356 }])
357
358 .controller('PendingCtrl',
359        ['$scope','$routeParams','bucketSvc','egGridDataProvider', 'egCore',
360 function($scope,  $routeParams,  bucketSvc , egGridDataProvider,   egCore) {
361     $scope.setTab('pending');
362
363     var query;
364     $scope.gridControls = {
365         setQuery : function(q) {
366             if (bucketSvc.pendingList.length)
367                 return {id : bucketSvc.pendingList};
368             else
369             return null;
370         }
371     }
372
373     $scope.search = function() {
374         bucketSvc.barcodeRecords = [];
375
376         egCore.pcrud.search(
377             'acp',
378             {barcode : bucketSvc.barcodeString, deleted : 'f'},
379             {}
380         ).then(null, null, function(copy) {
381             bucketSvc.pendingList.push(copy.id());
382             $scope.gridControls.setQuery({id : bucketSvc.pendingList});
383         });
384     }
385
386     $scope.resetPendingList = function() {
387         bucketSvc.pendingList = [];
388         $scope.gridControls.setQuery({});
389     }
390     
391     if ($routeParams.id && 
392         (!bucketSvc.currentBucket || 
393             bucketSvc.currentBucket.id() != $routeParams.id)) {
394         // user has accessed this page cold with a bucket ID.
395         // fetch the bucket for display, then set the totalCount
396         // (also for display), but avoid fully fetching the bucket,
397         // since it's premature, in this UI.
398         bucketSvc.fetchBucket($routeParams.id);
399     }
400     $scope.gridControls.setQuery();
401 }])
402
403 .controller('ViewCtrl',
404        ['$scope','$q','$routeParams','$timeout','$window','$modal','bucketSvc','egCore','egUser',
405         'egConfirmDialog',
406 function($scope,  $q , $routeParams , $timeout , $window , $modal , bucketSvc , egCore , egUser ,
407          egConfirmDialog) {
408
409     $scope.setTab('view');
410     $scope.bucketId = $routeParams.id;
411
412     var query;
413     $scope.gridControls = {
414         setQuery : function(q) {
415             if (q) query = q;
416             return query;
417         }
418     };
419
420     function drawBucket() {
421         return bucketSvc.fetchBucket($scope.bucketId).then(
422             function(bucket) {
423                 var ids = bucket.items().map(
424                     function(i){return i.target_copy()}
425                 );
426                 if (ids.length) {
427                     $scope.gridControls.setQuery({id : ids});
428                 } else {
429                     $scope.gridControls.setQuery({});
430                 }
431             }
432         );
433     }
434
435     $scope.detachCopies = function(copies) {
436         var promises = [];
437         angular.forEach(copies, function(rec) {
438             var item = bucketSvc.currentBucket.items().filter(
439                 function(i) {
440                     return (i.target_copy() == rec.id)
441                 }
442             );
443             if (item.length)
444                 promises.push(bucketSvc.detachCopy(item[0].id()));
445         });
446
447         bucketSvc.bucketNeedsRefresh = true;
448         return $q.all(promises).then(drawBucket);
449     }
450
451     $scope.spawnHoldingsEdit = function (copies) {
452         var rec_hash = {};
453         angular.forEach($scope.gridControls.selectedItems(), function (i) {
454             var rec = i['call_number.record.id'];
455             if (!rec_hash[rec]) rec_hash[rec] = [];
456             rec_hash[rec].push(i.id);
457         })
458
459         angular.forEach(rec_hash, function(cp_list,r) {
460             egCore.net.request(
461                 'open-ils.actor',
462                 'open-ils.actor.anon_cache.set_value',
463                 null, 'edit-these-copies', {
464                     record_id: r,
465                     copies: cp_list,
466                     hide_vols : true,
467                     hide_copies : false
468                 }
469             ).then(function(key) {
470                 if (key) {
471                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
472                     $timeout(function() { $window.open(url, '_blank') });
473                 } else {
474                     alert('Could not create anonymous cache key!');
475                 }
476             });
477         });
478     }
479
480     $scope.requestItems = function() {
481         var copy_list = $scope.gridControls.selectedItems().map(
482             function (i) {
483                 i.id;
484             }
485         );
486
487         if (copy_list.length == 0) return;
488
489         return $modal.open({
490             templateUrl: './cat/catalog/t_request_items',
491             animation: true,
492             controller:
493                    ['$scope','$modalInstance',
494             function($scope , $modalInstance) {
495                 $scope.user = null;
496                 $scope.first_user_fetch = true;
497
498                 $scope.hold_data = {
499                     hold_type : 'C',
500                     copy_list : copy_list,
501                     pickup_lib: egCore.org.get(egCore.auth.user().ws_ou()),
502                     user      : egCore.auth.user().id()
503                 };
504
505                 egUser.get( $scope.hold_data.user ).then(function(u) {
506                     $scope.user = u;
507                     $scope.barcode = u.card().barcode();
508                     $scope.user_name = egUser.format_name(u);
509                     $scope.hold_data.user = u.id();
510                 });
511
512                 $scope.user_name = '';
513                 $scope.barcode = '';
514                 $scope.$watch('barcode', function (n) {
515                     if (!$scope.first_user_fetch) {
516                         egUser.getByBarcode(n).then(function(u) {
517                             $scope.user = u;
518                             $scope.user_name = egUser.format_name(u);
519                             $scope.hold_data.user = u.id();
520                         }, function() {
521                             $scope.user = null;
522                             $scope.user_name = '';
523                             delete $scope.hold_data.user;
524                         });
525                     }
526                     $scope.first_user_fetch = false;
527                 });
528
529                 $scope.ok = function(h) {
530                     var args = {
531                         patronid  : h.user,
532                         hold_type : h.hold_type,
533                         pickup_lib: h.pickup_lib.id(),
534                         depth     : 0
535                     };
536
537                     egCore.net.request(
538                         'open-ils.circ',
539                         'open-ils.circ.holds.test_and_create.batch.override',
540                         egCore.auth.token(), args, h.copy_list
541                     );
542
543                     $modalInstance.close();
544                 }
545
546                 $scope.cancel = function($event) {
547                     $modalInstance.dismiss();
548                     $event.preventDefault();
549                 }
550             }]
551         });
552     }
553
554     $scope.deleteCopiesFromCatalog = function(copies) {
555         egConfirmDialog.open(
556             egCore.strings.CONFIRM_DELETE_COPY_BUCKET_ITEMS_FROM_CATALOG,
557             '', {}
558         ).result.then(function() {
559             var fleshed_copies = [];
560             var promises = [];
561             angular.forEach(copies, function(i) {
562                 promises.push(
563                     egCore.net.request(
564                         'open-ils.search',
565                         'open-ils.search.asset.copy.fleshed2.retrieve',
566                         i.id
567                     ).then(function(copy) {
568                         copy.ischanged(1);
569                         copy.isdeleted(1);
570                         fleshed_copies.push(copy);
571                     })
572                 );
573             });
574             $q.all(promises).then(function() {
575                 egCore.net.request(
576                     'open-ils.cat',
577                     'open-ils.cat.asset.copy.fleshed.batch.update',
578                     egCore.auth.token(), fleshed_copies, true
579                 ).then(function(resp) {
580                     var evt = egCore.evt.parse(resp);
581                     if (evt) {
582                         egConfirmDialog.open(
583                             egCore.strings.OVERRIDE_DELETE_COPY_BUCKET_ITEMS_FROM_CATALOG_TITLE,
584                             egCore.strings.OVERRIDE_DELETE_COPY_BUCKET_ITEMS_FROM_CATALOG_BODY,
585                             {'evt_desc': evt.desc}
586                         ).result.then(function() {
587                             egCore.net.request(
588                                 'open-ils.cat',
589                                 'open-ils.cat.asset.copy.fleshed.batch.update.override',
590                                 egCore.auth.token(), fleshed_copies, true,
591                                 { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
592                             ).then(function(resp) {
593                                 bucketSvc.bucketNeedsRefresh = true;
594                                 drawBucket();
595                             });
596                         });
597                     }
598                     bucketSvc.bucketNeedsRefresh = true;
599                     drawBucket();
600                 });
601             });
602         });
603     }
604
605     $scope.transferCopies = function(copies) {
606         var xfer_target = egCore.hatch.getLocalItem('eg.cat.item_transfer_target');
607         var copy_ids = copies.map(
608             function(curr,idx,arr) {
609                 return curr.id;
610             }
611         );
612         if (xfer_target) {
613             egCore.net.request(
614                 'open-ils.cat',
615                 'open-ils.cat.transfer_copies_to_volume',
616                 egCore.auth.token(),
617                 xfer_target,
618                 copy_ids
619             ).then(
620                 function(resp) { // oncomplete
621                     var evt = egCore.evt.parse(resp);
622                     if (evt) {
623                         egConfirmDialog.open(
624                             egCore.strings.OVERRIDE_TRANSFER_COPY_BUCKET_ITEMS_TO_MARKED_VOLUME_TITLE,
625                             egCore.strings.OVERRIDE_TRANSFER_COPY_BUCKET_ITEMS_TO_MARKED_VOLUME_BODY,
626                             {'evt_desc': evt.desc}
627                         ).result.then(function() {
628                             egCore.net.request(
629                                 'open-ils.cat',
630                                 'open-ils.cat.transfer_copies_to_volume.override',
631                                 egCore.auth.token(),
632                                 xfer_target,
633                                 copy_ids,
634                                 { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
635                             ).then(function(resp) {
636                                 bucketSvc.bucketNeedsRefresh = true;
637                                 drawBucket();
638                             });
639                         });
640                     } else {
641                         bucketSvc.bucketNeedsRefresh = true;
642                         drawBucket();
643                     }
644                 },
645                 null, // onerror
646                 null // onprogress
647             )
648         }
649     }
650
651     // fetch the bucket;  on error show the not-allowed message
652     if ($scope.bucketId) 
653         drawBucket()['catch'](function() { $scope.forbidden = true });
654 }])