]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/circ/services/holds.js
LP#1621178: fix fleshing of copy status for available items
[Evergreen.git] / Open-ILS / web / js / ui / default / staff / circ / services / holds.js
1 /**
2  * Holds, yo
3  */
4
5 angular.module('egCoreMod')
6
7 .factory('egHolds',
8
9        ['$uibModal','$q','egCore','egConfirmDialog','egAlertDialog',
10 function($uibModal , $q , egCore , egConfirmDialog , egAlertDialog) {
11
12     var service = {};
13
14     service.fetch_holds = function(hold_ids) {
15         var deferred = $q.defer();
16
17         // FIXME: large batches using .authoritative result in many 
18         // stranded cstore backends on the server.  Needs investigation.
19         // For now, collect holds in a series of small batches.
20         // Fetch them serially both to avoid the above problem and
21         // to maintain order.
22         var batch_size = 5;
23         var index = 0;
24
25         function one_batch() {
26             var ids = hold_ids.slice(index, index + batch_size)
27                 .filter(function(id) {return Boolean(id)}) // avoid nulls
28
29             console.debug('egHolds.fetch_holds => ' + ids);
30             index += batch_size;
31
32             if (!ids.length) {
33                 deferred.resolve();
34                 return;
35             }
36
37             egCore.net.request(
38                 'open-ils.circ',
39                 'open-ils.circ.hold.details.batch.retrieve.authoritative',
40                 egCore.auth.token(), ids
41
42             ).then(
43                 one_batch,  // kick off the next batch
44                 null, 
45                 function(hold_data) {
46                     var hold = hold_data.hold;
47                     hold_data.id = hold.id();
48                     service.local_flesh(hold_data);
49                     deferred.notify(hold_data);
50                 }
51             );
52         }
53
54         one_batch(); // kick it off
55         return deferred.promise;
56     }
57
58
59     service.cancel_holds = function(hold_ids) {
60        
61         return $uibModal.open({
62             templateUrl : './circ/share/t_cancel_hold_dialog',
63             controller : 
64                 ['$scope', '$uibModalInstance', 'cancel_reasons',
65                 function($scope, $uibModalInstance, cancel_reasons) {
66                     $scope.args = {
67                         cancel_reason : 5,
68                         cancel_reasons : cancel_reasons,
69                         num_holds : hold_ids.length
70                     };
71                     
72                     $scope.cancel = function($event) {
73                         $uibModalInstance.dismiss();
74                         $event.preventDefault();
75                     }
76
77                     $scope.ok = function() {
78
79                         function cancel_one() {
80                             var hold_id = hold_ids.pop();
81                             if (!hold_id) {
82                                 $uibModalInstance.close();
83                                 return;
84                             }
85                             egCore.net.request(
86                                 'open-ils.circ', 'open-ils.circ.hold.cancel',
87                                 egCore.auth.token(), hold_id,
88                                 $scope.args.cancel_reason,
89                                 $scope.args.note
90                             ).then(function(resp) {
91                                 if (evt = egCore.evt.parse(resp)) {
92                                     egCore.audio.play(
93                                         'warning.hold.cancel_failed');
94                                     console.error('unable to cancel hold: ' 
95                                         + evt.toString());
96                                 }
97                                 cancel_one();
98                             });
99                         }
100
101                         cancel_one();
102                     }
103                 }
104             ],
105             resolve : {
106                 cancel_reasons : function() {
107                     return service.get_cancel_reasons();
108                 }
109             }
110         }).result;
111     }
112
113     service.uncancel_holds = function(hold_ids) {
114        
115         return $uibModal.open({
116             templateUrl : './circ/share/t_uncancel_hold_dialog',
117             controller : 
118                 ['$scope', '$uibModalInstance',
119                 function($scope, $uibModalInstance) {
120                     $scope.args = {
121                         num_holds : hold_ids.length
122                     };
123                     
124                     $scope.cancel = function($event) {
125                         $uibModalInstance.dismiss();
126                         $event.preventDefault();
127                     }
128
129                     $scope.ok = function() {
130
131                         function uncancel_one() {
132                             var hold_id = hold_ids.pop();
133                             if (!hold_id) {
134                                 $uibModalInstance.close();
135                                 return;
136                             }
137                             egCore.net.request(
138                                 'open-ils.circ', 'open-ils.circ.hold.uncancel',
139                                 egCore.auth.token(), hold_id
140                             ).then(function(resp) {
141                                 if (evt = egCore.evt.parse(resp)) {
142                                     egCore.audio.play(
143                                         'warning.hold.uncancel_failed');
144                                     console.error('unable to uncancel hold: ' 
145                                         + evt.toString());
146                                 }
147                                 uncancel_one();
148                             });
149                         }
150
151                         uncancel_one();
152                     }
153                 }
154             ]
155         }).result;
156     }
157
158     service.get_cancel_reasons = function() {
159         if (egCore.env.ahrcc) return $q.when(egCore.env.ahrcc.list);
160         return egCore.pcrud.retrieveAll('ahrcc', {}, {atomic : true})
161         .then(function(list) { return egCore.env.absorbList(list, 'ahrcc').list });
162     }
163
164     // Updates a batch of holds, notifies on each response.
165     // new_values = array of hashes describing values to change,
166     // including the id of the hold to change.
167     // e.g. {id : 1, mint_condition : true}
168     service.update_holds = function(new_values) {
169         return egCore.net.request(
170             'open-ils.circ',
171             'open-ils.circ.hold.update.batch',
172             egCore.auth.token(), null, new_values).then(
173             function(resp) {
174                 if (evt = egCore.evt.parse(resp)) {
175                     egCore.audio.play(
176                         'warning.hold.batch_update');
177                     console.error('unable to batch update holds: '
178                         + evt.toString());
179                 } else {
180                     egCore.audio.play(
181                         'success.hold.batch_update');
182                 }
183             }
184         );
185     }
186
187     service.set_copy_quality = function(hold_ids) {
188         if (!hold_ids.length) return $q.when();
189         return $uibModal.open({
190             templateUrl : './circ/share/t_hold_copy_quality_dialog',
191             controller : 
192                 ['$scope', '$uibModalInstance',
193                 function($scope, $uibModalInstance) {
194
195                     function update(val) {
196                         var vals = hold_ids.map(function(hold_id) {
197                             return {id : hold_id, mint_condition : val}})
198                         service.update_holds(vals).finally(function() {
199                             $uibModalInstance.close();
200                         });
201                     }
202                     $scope.good = function() { update(true) }
203                     $scope.any = function() { update(false) }
204                     $scope.cancel = function() { $uibModalInstance.dismiss() }
205                 }
206             ]
207         }).result;
208     }
209
210     service.edit_pickup_lib = function(hold_ids) {
211         if (!hold_ids.length) return $q.when();
212         return $uibModal.open({
213             templateUrl : './circ/share/t_hold_edit_pickup_lib',
214             controller : 
215                 ['$scope', '$uibModalInstance',
216                 function($scope, $uibModalInstance) {
217                     $scope.cant_be_pickup = function (id) { return !egCore.org.CanHaveUsers(id); };
218                     $scope.args = {};
219                     $scope.ok = function() { 
220                         var vals = hold_ids.map(function(hold_id) {
221                             return {
222                                 id : hold_id, 
223                                 pickup_lib : $scope.args.org_unit.id()
224                             }
225                         });
226                         service.update_holds(vals).finally(function() {
227                             $uibModalInstance.close();
228                         });
229                     }
230                     $scope.cancel = function() { $uibModalInstance.dismiss() }
231                 }
232             ]
233         }).result;
234     }
235
236     service.get_sms_carriers = function() {
237         if (egCore.env.csc) return $q.when(egCore.env.csc.list);
238         return egCore.pcrud.retrieveAll('csc', {}, {atomic : true})
239         .then(function(list) { return egCore.env.absorbList(list, 'csc').list });
240     }
241
242     service.edit_notify_prefs = function(hold_ids) {
243         if (!hold_ids.length) return $q.when();
244         return $uibModal.open({
245             templateUrl : './circ/share/t_hold_notification_prefs',
246             controller : 
247                 ['$scope', '$uibModalInstance', 'sms_carriers',
248                 function($scope, $uibModalInstance, sms_carriers) {
249                     $scope.args = {}
250                     $scope.sms_carriers = sms_carriers;
251                     $scope.num_holds = hold_ids.length;
252                     $scope.ok = function() { 
253
254                         var vals = hold_ids.map(function(hold_id) {
255                             var val = {id : hold_id};
256                             angular.forEach(
257                                 ['email', 'phone', 'sms'],
258                                 function(type) {
259                                     var key = type + '_notify';
260                                     if ($scope.args['update_' + key]) 
261                                         val[key] = $scope.args[key];
262                                 }
263                             );
264                             if ($scope.args.update_sms_carrier)
265                                 val.sms_carrier = $scope.args.sms_carrier.id();
266                             return val;
267                         });
268
269                         service.update_holds(vals).finally(function() {
270                             $uibModalInstance.close();
271                         });
272                     }
273                     $scope.cancel = function() { $uibModalInstance.dismiss() }
274                 }
275             ],
276             resolve : {
277                 sms_carriers : service.get_sms_carriers
278             }
279         }).result;
280     }
281
282     service.edit_dates = function(hold_ids) {
283         if (!hold_ids.length) return $q.when();
284
285         // collects the fields from the dialog the user wishes to modify
286         function relay_to_update(modal_scope) {
287             var vals = hold_ids.map(function(hold_id) {
288                 var val = {id : hold_id};
289                 angular.forEach(
290                     ['thaw_date', 'request_time', 'expire_time', 'shelf_expire_time'], 
291                     function(field) {
292                         if (modal_scope.args['modify_' + field]) 
293                             val[field] = modal_scope.args[field].toISOString();
294                     }
295                 );
296
297                 return val;
298             });
299
300             console.log(JSON.stringify(vals,null,2));
301             return service.update_holds(vals);
302         }
303
304         return $uibModal.open({
305             templateUrl : './circ/share/t_hold_dates',
306             controller : 
307                 ['$scope', '$uibModalInstance',
308                 function($scope, $uibModalInstance) {
309                     var today = new Date();
310                     $scope.args = {
311                         thaw_date : today,
312                         request_time : today,
313                         expire_time : today,
314                         shelf_expire_time : today
315                     }
316                     $scope.num_holds = hold_ids.length;
317                     $scope.ok = function() { 
318                         relay_to_update($scope).then($uibModalInstance.close);
319                     }
320                     $scope.cancel = function() { $uibModalInstance.dismiss() }
321                 }
322             ],
323         }).result;
324     }
325
326     service.update_field_with_confirm = function(hold_ids, msg_key, field, value) {
327         if (!hold_ids.length) return $q.when();
328
329         return egConfirmDialog.open(
330             egCore.strings[msg_key], '', {num_holds : hold_ids.length})
331         .result.then(function() {
332
333             var vals = hold_ids.map(function(hold_id) {
334                 val = {id : hold_id};
335                 val[field] = value;
336                 return val;
337             });
338             return service.update_holds(vals);
339         });
340     }
341
342     service.suspend_holds = function(hold_ids) {
343         return service.update_field_with_confirm(
344             hold_ids, 'SUSPEND_HOLDS', 'frozen', true);
345     }
346
347     service.activate_holds = function(hold_ids) {
348         return service.update_field_with_confirm(
349             hold_ids, 'ACTIVATE_HOLDS', 'frozen', false);
350     }
351
352     service.set_top_of_queue = function(hold_ids) {
353         return service.update_field_with_confirm(
354             hold_ids, 'SET_TOP_OF_QUEUE', 'cut_in_line', true);
355     }
356
357     service.clear_top_of_queue = function(hold_ids) {
358         return service.update_field_with_confirm(
359             hold_ids, 'CLEAR_TOP_OF_QUEUE', 'cut_in_line', null);
360     }
361
362     service.transfer_to_marked_title = function(hold_ids) {
363         if (!hold_ids.length) return $q.when();
364
365         var bib_id = egCore.hatch.getLocalItem(
366             'eg.circ.hold.title_transfer_target');
367
368         if (!bib_id) {
369             // no target marked
370             return egAlertDialog.open(
371                 egCore.strings.NO_HOLD_TRANSFER_TITLE_MARKED).result;
372         }
373
374         return egConfirmDialog.open(
375             egCore.strings.TRANSFER_HOLD_TO_TITLE, '', {
376                 num_holds : hold_ids.length,
377                 bib_id : bib_id
378             }
379         ).result.then(function() {
380             return egCore.net.request(
381                 'open-ils.circ',
382                 'open-ils.circ.hold.change_title.specific_holds',
383                 egCore.auth.token(), bib_id, hold_ids);
384         });
385     }
386
387     service.transfer_all_bib_holds_to_marked_title = function(bib_ids) {
388         if (!bib_ids.length) return $q.when();
389
390         var target_bib_id = egCore.hatch.getLocalItem(
391             'eg.circ.hold.title_transfer_target');
392
393         if (!target_bib_id) {
394             // no target marked
395             return egAlertDialog.open(
396                 egCore.strings.NO_HOLD_TRANSFER_TITLE_MARKED).result;
397         }
398
399         return egConfirmDialog.open(
400             egCore.strings.TRANSFER_ALL_BIB_HOLDS_TO_TITLE, '', {
401                 num_bibs : bib_ids.length,
402                 bib_id : target_bib_id
403             }
404         ).result.then(function() {
405             return egCore.net.request(
406                 'open-ils.circ',
407                 'open-ils.circ.hold.change_title',
408                 egCore.auth.token(), target_bib_id, bib_ids);
409         });
410     }
411
412     // serially retargets each hold
413     service.retarget = function(hold_ids) {
414         if (!hold_ids.length) return $q.when();
415         var deferred = $q.defer();
416
417         egConfirmDialog.open(
418             egCore.strings.RETARGET_HOLDS, '', 
419             {hold_ids : hold_ids.join(',')}
420
421         ).result.then(function() {
422
423             function do_one() {
424                 var hold_id = hold_ids.pop();
425                 if (!hold_id) {
426                     deferred.resolve();
427                     return;
428                 }
429
430                 egCore.net.request(
431                     'open-ils.circ',
432                     'open-ils.circ.hold.reset',
433                     egCore.auth.token(), hold_id).finally(do_one);
434             }
435
436             do_one(); // kick it off
437         });
438
439         return deferred.promise;
440     }
441
442     // fleshes orgs, etc. for hold data blobs retrieved from
443     // open-ils.circ.hold.details[.batch].retrieve
444     service.local_flesh = function(hold_data) {
445
446         hold_data.status_string = 
447             egCore.strings['HOLD_STATUS_' + hold_data.status] 
448             || hold_data.status;
449
450         var hold = hold_data.hold;
451         hold.pickup_lib(egCore.org.get(hold.pickup_lib()));
452         hold.current_shelf_lib(egCore.org.get(hold.current_shelf_lib()));
453         hold_data.id = hold.id();
454
455         if (hold.requestor() && typeof hold.requestor() != 'object')
456             egCore.pcrud.retrieve('au',hold.requestor()).then(function(u) { hold.requestor(u) });
457
458         if (hold.cancel_cause() && typeof hold.cancel_cause() != 'object')
459             egCore.pcrud.retrieve('ahrcc',hold.cancel_cause()).then(function(c) { hold.cancel_cause(c) });
460
461         if (hold.usr() && typeof hold.usr() != 'object')
462             egCore.pcrud.retrieve('au',hold.usr()).then(function(u) { hold.usr(u) });
463
464         // current_copy is not always fleshed in the API
465         if (hold.current_copy() && typeof hold.current_copy() != 'object') {
466             hold.current_copy(hold_data.copy);
467             
468             // likewise, current_copy's status isn't fleshed in the API
469             if(hold.current_copy().status() !== null &&
470                typeof hold.current_copy().status() != 'object')
471                 egCore.pcrud.retrieve('ccs',hold.current_copy().status()
472                     ).then(function(c) { hold.current_copy().status(c) });
473         }
474     }
475
476     return service;
477 }])
478
479 /**  
480  * Action handlers for the common Hold grid UI.
481  * These generally scrub the data for valid input then pass the
482  * holds / copies / etc. off to the relevant action in egHolds or egCirc.
483  *
484  * Caller must apply a reset_page function, which is called after 
485  * most actionis are performed.
486  */
487 .factory('egHoldGridActions', 
488        ['$window','$location','$timeout','egCore','egHolds','egCirc',
489 function($window , $location , $timeout , egCore , egHolds , egCirc) {
490     
491     var service = {};
492
493     service.refresh = function() {
494         console.error('egHoldGridActions.refresh not defined!');
495     }
496
497     service.cancel_hold = function(items) {
498         var hold_ids = items.filter(function(item) {
499             return !item.hold.cancel_time();
500         }).map(function(item) {return item.hold.id()});
501
502         return egHolds.cancel_holds(hold_ids).then(service.refresh);
503     }
504
505     service.uncancel_hold = function(items) {
506         var hold_ids = items.filter(function(item) {
507             return item.hold.cancel_time();
508         }).map(function(item) {return item.hold.id()});
509
510         return egHolds.uncancel_holds(hold_ids).then(service.refresh);
511     }
512
513     // jump to circ list for either 1) the targeted copy or
514     // 2) the hold target copy for copy-level holds
515     service.show_recent_circs = function(items) {
516         var focus = items.length == 1;
517         angular.forEach(items, function(item) {
518             if (item.copy) {
519                 var url = egCore.env.basePath +
520                           '/cat/item/' +
521                           item.copy.id() +
522                           '/circ_list';
523                 $timeout(function() { var x = $window.open(url, '_blank'); if (focus) x.focus() });
524             }
525         });
526     }
527
528     service.show_patrons = function(items) {
529         var focus = items.length == 1;
530         angular.forEach(items, function(item) {
531             var url = egCore.env.basePath +
532                       'circ/patron/' +
533                       item.hold.usr().id() +
534                       '/holds';
535             $timeout(function() { var x = $window.open(url, '_blank'); if (focus) x.focus() });
536         });
537     }
538
539     service.show_holds_for_title = function(items) {
540         var focus = items.length == 1;
541         angular.forEach(items, function(item) {
542             var url = egCore.env.basePath +
543                       'cat/catalog/record/' +
544                       item.mvr.doc_id() +
545                       '/holds';
546             $timeout(function() { var x = $window.open(url, '_blank'); if (focus) x.focus() });
547         });
548     }
549
550
551     function generic_update(items, action) {
552         if (!items.length) return $q.when();
553         var hold_ids = items.map(function(item) {return item.hold.id()});
554         return egHolds[action](hold_ids).then(service.refresh);
555     }
556
557     service.set_copy_quality = function(items) {
558         generic_update(items, 'set_copy_quality'); }
559     service.edit_pickup_lib = function(items) {
560         generic_update(items, 'edit_pickup_lib'); }
561     service.edit_notify_prefs = function(items) {
562         generic_update(items, 'edit_notify_prefs'); }
563     service.edit_dates = function(items) {
564         generic_update(items, 'edit_dates'); }
565     service.suspend = function(items) {
566         generic_update(items, 'suspend_holds'); }
567     service.activate = function(items) {
568         generic_update(items, 'activate_holds'); }
569     service.set_top_of_queue = function(items) {
570         generic_update(items, 'set_top_of_queue'); }
571     service.clear_top_of_queue = function(items) {
572         generic_update(items, 'clear_top_of_queue'); }
573     service.transfer_to_marked_title = function(items) {
574         generic_update(items, 'transfer_to_marked_title'); }
575
576     service.mark_damaged = function(items) {
577         var copy_ids = items
578             .filter(function(item) { return Boolean(item.copy) })
579             .map(function(item) { return item.copy.id() });
580         if (copy_ids.length) 
581             egCirc.mark_damaged(copy_ids).then(service.refresh);
582     }
583
584     service.mark_missing = function(items) {
585         var copy_ids = items
586             .filter(function(item) { return Boolean(item.copy) })
587             .map(function(item) { return item.copy.id() });
588         if (copy_ids.length) 
589             egCirc.mark_missing(copy_ids).then(service.refresh);
590     }
591
592     service.retarget = function(items) {
593         var hold_ids = items.map(function(item) { return item.hold.id() });
594         egHolds.retarget(hold_ids).then(service.refresh);
595     }
596
597     return service;
598 }])
599
600 /**
601  * Hold details interface 
602  */
603 .directive('egHoldDetails', function() {
604     return {
605         restrict : 'AE',
606         templateUrl : './circ/share/t_hold_details',
607         scope : {
608             holdId : '=',
609             // if set, called whenever hold details are retrieved.  The
610             // argument is the hold blob returned from hold.details.retrieve
611             holdRetrieved : '=',
612             showPatron : '='
613         },
614         controller : [
615                     '$scope','$uibModal','egCore','egHolds','egCirc',
616             function($scope , $uibModal , egCore , egHolds , egCirc) {
617
618                 function draw() {
619                     if (!$scope.holdId) return;
620
621                     egCore.net.request(
622                         'open-ils.circ',
623                         'open-ils.circ.hold.details.retrieve.authoritative',
624                         egCore.auth.token(), $scope.holdId
625
626                     ).then(function(hold_data) { 
627                         egHolds.local_flesh(hold_data);
628     
629                         angular.forEach(hold_data, 
630                             function(val, key) { $scope[key] = val });
631
632                         // fetch + flesh the cancel_cause if needed
633                         if ($scope.hold.cancel_time()) {
634                             egHolds.get_cancel_reasons().then(function() {
635                                 // egHolds caches the causes in egEnv
636                                 $scope.hold.cancel_cause(
637                                     egCore.env.ahrcc.map[$scope.hold.cancel_cause()]);
638                             })
639                         }
640
641                         if ($scope.hold.current_copy()) {
642                             egCirc.flesh_copy_location($scope.hold.current_copy());
643                         }
644
645                         if ($scope.holdRetrieved)
646                             $scope.holdRetrieved(hold_data);
647
648                     });
649                 }
650
651                 $scope.show_notify_tab = function() {
652                     $scope.detail_tab = 'notify';
653                     egCore.pcrud.search('ahn',
654                         {hold : $scope.hold.id()}, 
655                         {flesh : 1, flesh_fields : {ahn : ['notify_staff']}}, 
656                         {atomic : true}
657                     ).then(function(nots) {
658                         $scope.hold.notifications(nots);
659                     });
660                 }
661
662                 $scope.delete_note = function(note) {
663                     egCore.pcrud.remove(note).then(function() {
664                         // remove the deleted note from the locally fleshed notes
665                         $scope.hold.notes(
666                             $scope.hold.notes().filter(function(n) {
667                                 return n.id() != note.id()
668                             })
669                         );
670                     });
671                 }
672
673                 $scope.new_note = function() {
674                     return $uibModal.open({
675                         templateUrl : './circ/share/t_hold_note_dialog',
676                         controller : 
677                             ['$scope', '$uibModalInstance',
678                             function($scope, $uibModalInstance) {
679                                 $scope.args = {};
680                                 $scope.ok = function() {
681                                     $uibModalInstance.close($scope.args)
682                                 },
683                                 $scope.cancel = function($event) {
684                                     $uibModalInstance.dismiss();
685                                     $event.preventDefault();
686                                 }
687                             }
688                         ]
689                     }).result.then(function(args) {
690                         var note = new egCore.idl.ahrn();
691                         note.hold($scope.hold.id());
692                         note.staff(true);
693                         note.slip(args.slip);
694                         note.pub(args.pub); 
695                         note.title(args.title);
696                         note.body(args.body);
697                         return egCore.pcrud.create(note).then(function(n) {
698                             $scope.hold.notes().push(n);
699                         });
700                     });
701                 }
702
703                 $scope.new_notification = function() {
704                     return $uibModal.open({
705                         templateUrl : './circ/share/t_hold_notification_dialog',
706                         controller : 
707                             ['$scope', '$uibModalInstance',
708                             function($scope, $uibModalInstance) {
709                                 $scope.args = {};
710                                 $scope.ok = function() {
711                                     $uibModalInstance.close($scope.args)
712                                 },
713                                 $scope.cancel = function($event) {
714                                     $uibModalInstance.dismiss();
715                                     $event.preventDefault();
716                                 }
717                             }
718                         ]
719                     }).result.then(function(args) {
720                         var note = new egCore.idl.ahn();
721                         note.hold($scope.hold.id());
722                         note.method(args.method);
723                         note.note(args.note);
724                         note.notify_staff(egCore.auth.user().id());
725                         note.notify_time('now');
726                         return egCore.pcrud.create(note).then(function(n) {
727                             n.notify_staff(egCore.auth.user());
728                             $scope.hold.notifications().push(n);
729                         });
730                     });
731                 }
732
733                 $scope.$watch('holdId', function(newVal, oldVal) {
734                     if (newVal != oldVal) draw();
735                 });
736
737                 draw();
738             }
739         ]
740     }
741 })
742
743