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