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