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