]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/circ/services/holds.js
LP2061136 - Stamping 1405 DB upgrade script
[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','egWorkLog',
10 function($uibModal , $q , egCore , egConfirmDialog , egAlertDialog , egWorkLog) {
11
12     var service = {};
13
14     service.fetch_wide_holds = function(restrictions, order_by, limit, offset) {
15         return egCore.net.request(
16             'open-ils.circ',
17             'open-ils.circ.hold.wide_hash.stream',
18             egCore.auth.token(),
19             restrictions, order_by, limit, offset
20         );
21     }
22
23     service.fetch_holds = function(hold_ids) {
24         var deferred = $q.defer();
25
26         // Fetch hold details in batches for better UI responsiveness.
27         var batch_size = 5;
28         var index = 0;
29
30         function one_batch() {
31             var ids = hold_ids.slice(index, index + batch_size)
32                 .filter(function(id) {return Boolean(id)}) // avoid nulls
33
34             console.debug('egHolds.fetch_holds => ' + ids);
35             index += batch_size;
36
37             if (!ids.length) {
38                 deferred.resolve();
39                 return;
40             }
41
42             egCore.net.request(
43                 'open-ils.circ',
44                 'open-ils.circ.hold.details.batch.retrieve.authoritative',
45                 egCore.auth.token(), ids, {
46                     include_current_copy : true,
47                     include_usr          : true,
48                     include_cancel_cause : true,
49                     include_sms_carrier  : true,
50                     include_requestor    : true
51                 }
52
53             ).then(
54                 one_batch,  // kick off the next batch
55                 null, 
56                 function(hold_data) {
57                     var hold = hold_data.hold;
58                     hold_data.id = hold.id();
59                     service.local_flesh(hold_data);
60                     deferred.notify(hold_data);
61                 }
62             );
63         }
64
65         one_batch(); // kick it off
66         return deferred.promise;
67     }
68
69
70     service.cancel_holds = function(hold_ids) {
71        
72         return $uibModal.open({
73             templateUrl : './circ/share/t_cancel_hold_dialog',
74             backdrop: 'static',
75             controller : 
76                 ['$scope', '$uibModalInstance', 'cancel_reasons',
77                 function($scope, $uibModalInstance, cancel_reasons) {
78                     $scope.args = {
79                         cancel_reason : 5,
80                         cancel_reasons : cancel_reasons,
81                         num_holds : hold_ids.length
82                     };
83                     
84                     $scope.cancel = function($event) {
85                         $uibModalInstance.dismiss();
86                         $event.preventDefault();
87                     }
88
89                     $scope.ok = function() {
90
91                         function cancel_one() {
92                             var hold_id = hold_ids.pop();
93                             if (!hold_id) {
94                                 $uibModalInstance.close();
95                                 return;
96                             }
97                             egCore.net.request(
98                                 'open-ils.circ', 'open-ils.circ.hold.cancel',
99                                 egCore.auth.token(), hold_id,
100                                 $scope.args.cancel_reason,
101                                 $scope.args.note
102                             ).then(function(resp) {
103                                 if (evt = egCore.evt.parse(resp)) {
104                                     egCore.audio.play(
105                                         'warning.hold.cancel_failed');
106                                     console.error('unable to cancel hold: ' 
107                                         + evt.toString());
108                                 } else {
109                                     egCore.net.request(
110                                         'open-ils.circ', 'open-ils.circ.hold.details.retrieve',
111                                         egCore.auth.token(), hold_id, {
112                                             'suppress_notices': true,
113                                             'suppress_transits': true,
114                                             'suppress_mvr' : true,
115                                             'include_usr' : true
116                                     }).then(function(details) {
117                                         //console.log('details', details);
118                                         egWorkLog.record(
119                                             egCore.strings.EG_WORK_LOG_CANCELED_HOLD
120                                             ,{
121                                                 'action' : 'canceled_hold',
122                                                 'method' : 'open-ils.circ.hold.cancel',
123                                                 'hold_id' : hold_id,
124                                                 'patron_id' : details.hold.usr().id(),
125                                                 'user' : details.patron_last,
126                                                 'item' : details.copy ? details.copy.barcode() : null,
127                                                 'item_id' : details.copy ? details.copy.id() : null
128                                             }
129                                         );
130                                     });
131                                 }
132                                 cancel_one();
133                             });
134                         }
135
136                         cancel_one();
137                     }
138                 }
139             ],
140             resolve : {
141                 cancel_reasons : function() {
142                     return service.get_cancel_reasons().then(function(reasons) {
143                         // only display reasons for manually canceling holds
144                         return reasons.filter(function(r) {
145                             return 't' === r.manual();
146                         });
147                     });
148                 }
149             }
150         }).result;
151     }
152
153     service.uncancel_holds = function(hold_ids) {
154        
155         return $uibModal.open({
156             templateUrl : './circ/share/t_uncancel_hold_dialog',
157             backdrop: 'static',
158             controller : 
159                 ['$scope', '$uibModalInstance',
160                 function($scope, $uibModalInstance) {
161                     $scope.args = {
162                         num_holds : hold_ids.length
163                     };
164                     
165                     $scope.cancel = function($event) {
166                         $uibModalInstance.dismiss();
167                         $event.preventDefault();
168                     }
169
170                     $scope.ok = function() {
171
172                         function uncancel_one() {
173                             var hold_id = hold_ids.pop();
174                             if (!hold_id) {
175                                 $uibModalInstance.close();
176                                 return;
177                             }
178                             egCore.net.request(
179                                 'open-ils.circ', 'open-ils.circ.hold.uncancel',
180                                 egCore.auth.token(), hold_id
181                             ).then(function(resp) {
182                                 if (evt = egCore.evt.parse(resp)) {
183                                     egCore.audio.play(
184                                         'warning.hold.uncancel_failed');
185                                     console.error('unable to uncancel hold: ' 
186                                         + evt.toString());
187                                 }
188                                 uncancel_one();
189                             });
190                         }
191
192                         uncancel_one();
193                     }
194                 }
195             ]
196         }).result;
197     }
198
199     service.get_cancel_reasons = function() {
200         if (egCore.env.ahrcc) return $q.when(egCore.env.ahrcc.list);
201         return egCore.pcrud.retrieveAll('ahrcc', {}, {atomic : true})
202         .then(function(list) { return egCore.env.absorbList(list, 'ahrcc').list });
203     }
204
205     // Updates a batch of holds, notifies on each response.
206     // new_values = array of hashes describing values to change,
207     // including the id of the hold to change.
208     // e.g. {id : 1, mint_condition : true}
209     service.update_holds = function(new_values) {
210         return egCore.net.request(
211             'open-ils.circ',
212             'open-ils.circ.hold.update.batch',
213             egCore.auth.token(), null, new_values).then(
214             function(resp) {
215                 if (evt = egCore.evt.parse(resp)) {
216                     egCore.audio.play(
217                         'warning.hold.batch_update');
218                     console.error('unable to batch update holds: '
219                         + evt.toString());
220                 } else {
221                     egCore.audio.play(
222                         'success.hold.batch_update');
223                 }
224             }
225         );
226     }
227
228     service.set_copy_quality = function(hold_ids) {
229         if (!hold_ids.length) return $q.when();
230         return $uibModal.open({
231             templateUrl : './circ/share/t_hold_copy_quality_dialog',
232             backdrop: 'static',
233             controller : 
234                 ['$scope', '$uibModalInstance',
235                 function($scope, $uibModalInstance) {
236
237                     function update(val) {
238                         var vals = hold_ids.map(function(hold_id) {
239                             return {id : hold_id, mint_condition : val}})
240                         service.update_holds(vals).finally(function() {
241                             $uibModalInstance.close();
242                         });
243                     }
244                     $scope.good = function() { update(true) }
245                     $scope.any = function() { update(false) }
246                     $scope.cancel = function() { $uibModalInstance.dismiss() }
247                 }
248             ]
249         }).result;
250     }
251
252     service.edit_pickup_lib = function(hold_ids) {
253         if (!hold_ids.length) return $q.when();
254         return $uibModal.open({
255             templateUrl : './circ/share/t_hold_edit_pickup_lib',
256             backdrop: 'static',
257             controller : 
258                 ['$scope', '$uibModalInstance',
259                 function($scope, $uibModalInstance) {
260                     $scope.cant_be_pickup = function (id) { return !egCore.org.CanHaveUsers(id); };
261                     $scope.args = {};
262                     $scope.ok = function() { 
263                         var vals = hold_ids.map(function(hold_id) {
264                             return {
265                                 id : hold_id, 
266                                 pickup_lib : $scope.args.org_unit.id()
267                             }
268                         });
269                         service.update_holds(vals).finally(function() {
270                             $uibModalInstance.close();
271                         });
272                     }
273                     $scope.cancel = function() { $uibModalInstance.dismiss() }
274                 }
275             ]
276         }).result;
277     }
278
279     service.get_sms_carriers = function() {
280         if (egCore.env.csc) return $q.when(egCore.env.csc.list);
281         return egCore.pcrud.retrieveAll('csc', {}, {atomic : true})
282         .then(function(list) { return egCore.env.absorbList(list, 'csc').list });
283     }
284
285     service.edit_notify_prefs = function(hold_ids) {
286         if (!hold_ids.length) return $q.when();
287         return $uibModal.open({
288             templateUrl : './circ/share/t_hold_notification_prefs',
289             backdrop: 'static',
290             controller : 
291                 ['$scope', '$uibModalInstance', 'sms_carriers',
292                 function($scope, $uibModalInstance, sms_carriers) {
293                     $scope.args = {}
294                     $scope.sms_carriers = sms_carriers;
295                     $scope.num_holds = hold_ids.length;
296                     $scope.ok = function() { 
297
298                         var vals = hold_ids.map(function(hold_id) {
299                             var val = {id : hold_id};
300                             angular.forEach(
301                                 ['email', 'phone', 'sms'],
302                                 function(type) {
303                                     var key = type + '_notify';
304                                     if ($scope.args['update_' + key]) 
305                                         val[key] = $scope.args[key];
306                                 }
307                             );
308                             if ($scope.args.update_sms_carrier)
309                                 val.sms_carrier = $scope.args.sms_carrier.id();
310                             return val;
311                         });
312
313                         service.update_holds(vals).finally(function() {
314                             $uibModalInstance.close();
315                         });
316                     }
317                     $scope.cancel = function() { $uibModalInstance.dismiss() }
318                 }
319             ],
320             resolve : {
321                 sms_carriers : service.get_sms_carriers
322             }
323         }).result;
324     }
325
326     service.edit_dates = function(hold_ids) {
327         if (!hold_ids.length) return $q.when();
328
329         // collects the fields from the dialog the user wishes to modify
330         function relay_to_update(modal_scope) {
331             var vals = hold_ids.map(function(hold_id) {
332                 var val = {id : hold_id};
333                 angular.forEach(
334                     ['thaw_date', 'request_time', 'expire_time', 'shelf_expire_time'], 
335                     function(field) {
336                         if (modal_scope.args['modify_' + field]) { 
337                             val[field] = modal_scope.args[field].toISOString();
338                             if (field === 'thaw_date') {
339                             //If we are setting the thaw_date, freeze the hold.
340                                 val['frozen'] = true;
341                             }
342                         }
343                     }
344                 );
345
346                 return val;
347             });
348
349             console.log(JSON.stringify(vals,null,2));
350             return service.update_holds(vals);
351         }
352
353         return $uibModal.open({
354             templateUrl : './circ/share/t_hold_dates',
355             backdrop: 'static',
356             controller : 
357                 ['$scope', '$uibModalInstance',
358                 function($scope, $uibModalInstance) {
359                     var today = new Date();
360                     $scope.args = {
361                         thaw_date : today,
362                         request_time : today,
363                         expire_time : today,
364                         shelf_expire_time : today
365                     }
366                     $scope.num_holds = hold_ids.length;
367                     $scope.ok = function() { 
368                         relay_to_update($scope).then($uibModalInstance.close);
369                     }
370                     $scope.cancel = function() { $uibModalInstance.dismiss() }
371                     $scope.minDate = new Date();
372                     //watch for changes to the hold dates, and perform validations
373                     $scope.$watch('args', function(newValue,oldValue,scope) {
374                         if (newValue['thaw_date'] && newValue['thaw_date'] < today) {
375                             $scope.args['thaw_date'] = today;
376                             $scope.args.thaw_date_error = true;
377                         }
378                         if (newValue['thaw_date'] && newValue['thaw_date'] > today) {
379                             $scope.args.thaw_date_error = false;
380                         }
381                     }, true);
382                 }
383             ],
384         }).result;
385     }
386
387     service.update_field_with_confirm = function(hold_ids, msg_key, field, value) {
388         if (!hold_ids.length) return $q.when();
389
390         return egConfirmDialog.open(
391             egCore.strings[msg_key], '', {num_holds : hold_ids.length})
392         .result.then(function() {
393
394             var vals = hold_ids.map(function(hold_id) {
395                 val = {id : hold_id};
396                 val[field] = value;
397                 return val;
398             });
399             return service.update_holds(vals);
400         });
401     }
402
403     service.suspend_holds = function(hold_ids) {
404         return service.update_field_with_confirm(
405             hold_ids, 'SUSPEND_HOLDS', 'frozen', true);
406     }
407
408     service.activate_holds = function(hold_ids) {
409         return service.update_field_with_confirm(
410             hold_ids, 'ACTIVATE_HOLDS', 'frozen', false);
411     }
412
413     service.set_top_of_queue = function(hold_ids) {
414         return service.update_field_with_confirm(
415             hold_ids, 'SET_TOP_OF_QUEUE', 'cut_in_line', true);
416     }
417
418     service.clear_top_of_queue = function(hold_ids) {
419         return service.update_field_with_confirm(
420             hold_ids, 'CLEAR_TOP_OF_QUEUE', 'cut_in_line', null);
421     }
422
423     service.transfer_to_marked_title = function(hold_ids) {
424         if (!hold_ids.length) return $q.when();
425
426         var bib_id = egCore.hatch.getLocalItem(
427             'eg.circ.hold.title_transfer_target');
428
429         if (!bib_id) {
430             // no target marked
431             return egAlertDialog.open(
432                 egCore.strings.NO_HOLD_TRANSFER_TITLE_MARKED).result;
433         }
434
435         return egConfirmDialog.open(
436             egCore.strings.TRANSFER_HOLD_TO_TITLE, '', {
437                 num_holds : hold_ids.length,
438                 bib_id : bib_id
439             }
440         ).result.then(function() {
441             return egCore.net.request(
442                 'open-ils.circ',
443                 'open-ils.circ.hold.change_title.specific_holds',
444                 egCore.auth.token(), bib_id, hold_ids);
445         });
446     }
447
448     service.transfer_all_bib_holds_to_marked_title = function(bib_ids) {
449         if (!bib_ids.length) return $q.when();
450
451         var target_bib_id = egCore.hatch.getLocalItem(
452             'eg.circ.hold.title_transfer_target');
453
454         if (!target_bib_id) {
455             // no target marked
456             return egAlertDialog.open(
457                 egCore.strings.NO_HOLD_TRANSFER_TITLE_MARKED).result;
458         }
459
460         return egConfirmDialog.open(
461             egCore.strings.TRANSFER_ALL_BIB_HOLDS_TO_TITLE, '', {
462                 num_bibs : bib_ids.length,
463                 bib_id : target_bib_id
464             }
465         ).result.then(function() {
466             return egCore.net.request(
467                 'open-ils.circ',
468                 'open-ils.circ.hold.change_title',
469                 egCore.auth.token(), target_bib_id, bib_ids);
470         });
471     }
472
473     // serially retargets each hold
474     service.retarget = function(hold_ids) {
475         if (!hold_ids.length) return $q.when();
476         var deferred = $q.defer();
477
478         egConfirmDialog.open(
479             egCore.strings.RETARGET_HOLDS, '', 
480             {hold_ids : hold_ids.join(',')}
481
482         ).result.then(function() {
483
484             function do_one() {
485                 var hold_id = hold_ids.pop();
486                 if (!hold_id) {
487                     deferred.resolve();
488                     return;
489                 }
490
491                 egCore.net.request(
492                     'open-ils.circ',
493                     'open-ils.circ.hold.reset',
494                     egCore.auth.token(), hold_id).finally(do_one);
495             }
496
497             do_one(); // kick it off
498         });
499
500         return deferred.promise;
501     }
502
503     // fleshes orgs, etc. for hold data blobs retrieved from
504     // open-ils.circ.hold.details[.batch].retrieve
505     service.local_flesh = function(hold_data) {
506
507         hold_data.status_string = 
508             egCore.strings['HOLD_STATUS_' + hold_data.status] 
509             || hold_data.status;
510
511         var hold = hold_data.hold;
512         var volume = hold_data.volume;
513         hold.pickup_lib(egCore.org.get(hold.pickup_lib()));
514         hold.current_shelf_lib(egCore.org.get(hold.current_shelf_lib()));
515         hold_data.id = hold.id();
516
517         // TODO: LP#1697954 fleshing calls below are deprecated in favor
518         // of API fleshing.
519
520         if (hold.requestor() && typeof hold.requestor() != 'object') {
521             console.debug('fetching hold requestor');
522             egCore.pcrud.retrieve('au',hold.requestor()).then(function(u) { hold.requestor(u) });
523         }
524
525         if (hold.canceled_by() && typeof hold.canceled_by() != 'object') {
526             console.debug('fetching hold canceled_by');
527             egCore.pcrud.retrieve('au',hold.canceled_by()).then(function(u) { hold.canceled_by(u) });
528         }
529
530         if (hold.cancel_cause() && typeof hold.cancel_cause() != 'object') {
531             console.debug('fetching hold cancel cause');
532             egCore.pcrud.retrieve('ahrcc',hold.cancel_cause()).then(function(c) { hold.cancel_cause(c) });
533         }
534
535         if (hold.usr() && typeof hold.usr() != 'object') {
536             console.debug('fetching hold user');
537             egCore.pcrud.retrieve('au',hold.usr()).then(function(u) { hold.usr(u) });
538         }
539
540         if (hold.sms_carrier() && typeof hold.sms_carrier() != 'object') {
541             console.debug('fetching sms carrier');
542             egCore.pcrud.retrieve('csc',hold.sms_carrier()).then(function(c) { hold.sms_carrier(c) });
543         }
544
545         // current_copy is not always fleshed in the API
546         if (hold.current_copy() && typeof hold.current_copy() != 'object') {
547             hold.current_copy(hold_data.copy);
548         }
549
550         if (hold.current_copy()) {
551             // likewise, current_copy's status isn't fleshed in the API
552             if(hold.current_copy().status() !== null &&
553                typeof hold.current_copy().status() != 'object')
554                 egCore.pcrud.retrieve('ccs',hold.current_copy().status()
555                     ).then(function(c) { hold.current_copy().status(c) });
556         
557             // current_copy's shelving location position isn't always accessible
558             if (hold.current_copy().location()) {
559                 //console.debug('fetching hold copy location order');
560                 var location_id;
561                 if (typeof hold.current_copy().location() != 'object') {
562                     location_id = hold.current_copy().location();
563                 } else {
564                     location_id = hold.current_copy().location().id();
565                 }
566                 egCore.pcrud.search(
567                     'acplo',
568                     {location: location_id, org: egCore.auth.user().ws_ou()},
569                     null,
570                     {atomic:true}
571                 ).then(function(orders) {
572                     if(orders[0]){
573                         hold_data.hold._copy_location_position = orders[0].position();
574                     } else {
575                         hold_data.hold._copy_location_position = 999;
576                     }
577                 });
578             }
579
580             //Call number affixes are not always fleshed in the API
581             if (hold_data.volume.prefix) {
582                 //console.debug('fetching call number prefix');
583                 //console.log(hold_data.volume.prefix());
584                 egCore.pcrud.retrieve('acnp',hold_data.volume.prefix())
585                 .then(function(p) {hold_data.volume.prefix = p.label(); hold_data.volume.prefix_sortkey = p.label_sortkey()});
586             }
587             if (hold_data.volume.suffix) {
588                 //console.debug('fetching call number suffix');
589                 //console.log(hold_data.volume.suffix());
590                 egCore.pcrud.retrieve('acns',hold_data.volume.suffix())
591                 .then(function(s) {hold_data.volume.suffix = s.label(); hold_data.volume.suffix_sortkey = s.label_sortkey()});
592             }
593         }
594     }
595
596     return service;
597 }])
598
599 /**  
600  * Action handlers for the common Hold grid UI.
601  * These generally scrub the data for valid input then pass the
602  * holds / copies / etc. off to the relevant action in egHolds or egCirc.
603  *
604  * Caller must apply a reset_page function, which is called after 
605  * most actionis are performed.
606  */
607 .factory('egHoldGridActions', 
608        ['$window','$location','$timeout','egCore','egHolds','egCirc',
609 function($window , $location , $timeout , egCore , egHolds , egCirc) {
610     
611     var service = {};
612
613     service.refresh = function() {
614         console.error('egHoldGridActions.refresh not defined!');
615     }
616
617     service.cancel_hold = function(items) {
618         var hold_ids = items.filter(function(item) {
619             return !item.hold.cancel_time();
620         }).map(function(item) {return item.hold.id()});
621
622         return egHolds.cancel_holds(hold_ids).then(service.refresh);
623     }
624
625     service.cancel_hold_wide = function(items) {
626         var hold_ids = items.filter(function(item) {
627             return !item.hold.cancel_time;
628         }).map(function(item) {return item.hold.id});
629
630         return egHolds.cancel_holds(hold_ids).then(service.refresh);
631     }
632
633     service.uncancel_hold = function(items) {
634         var hold_ids = items.filter(function(item) {
635             return item.hold.cancel_time();
636         }).map(function(item) {return item.hold.id()});
637
638         return egHolds.uncancel_holds(hold_ids).then(service.refresh);
639     }
640
641     service.uncancel_hold_wide = function(items) {
642         var hold_ids = items.filter(function(item) {
643             return item.hold.cancel_time;
644         }).map(function(item) {return item.hold.id});
645
646         return egHolds.uncancel_holds(hold_ids).then(service.refresh);
647     }
648
649     // jump to circ list for either 1) the targeted copy or
650     // 2) the hold target copy for copy-level holds
651     service.show_recent_circs = function(items) {
652         var focus = items.length == 1;
653         angular.forEach(items, function(item) {
654             if (item.copy) {
655                 var url = egCore.env.basePath +
656                           '/cat/item/' +
657                           item.copy.id() +
658                           '/circ_list';
659                 $timeout(function() { var x = $window.open(url, '_blank'); if (focus) x.focus() });
660             }
661         });
662     }
663
664     // jump to circ list for either 1) the targeted copy or
665     // 2) the hold target copy for copy-level holds
666     service.show_recent_circs_wide = function(items) {
667         var focus = items.length == 1;
668         angular.forEach(items, function(item) {
669             if (item.hold.cp_id) {
670                 var url = egCore.env.basePath +
671                           '/cat/item/' +
672                           item.hold.cp_id +
673                           '/circ_list';
674                 $timeout(function() { var x = $window.open(url, '_blank'); if (focus) x.focus() });
675             }
676         });
677     }
678
679     service.show_patrons = function(items) {
680         var focus = items.length == 1;
681         angular.forEach(items, function(item) {
682             var url = egCore.env.basePath +
683                       'circ/patron/' +
684                       item.hold.usr().id() +
685                       '/holds';
686             $timeout(function() { var x = $window.open(url, '_blank'); if (focus) x.focus() });
687         });
688     }
689
690     service.show_patrons_wide = function(items) {
691         var focus = items.length == 1;
692         angular.forEach(items, function(item) {
693             var url = egCore.env.basePath +
694                       'circ/patron/' +
695                       item.hold.usr_id +
696                       '/holds';
697             $timeout(function() { var x = $window.open(url, '_blank'); if (focus) x.focus() });
698         });
699     }
700
701     service.show_holds_for_title = function(items) {
702         var focus = items.length == 1;
703         angular.forEach(items, function(item) {
704             var url = '/eg2/staff/catalog/record/' + item.mvr.doc_id() + '/holds';
705             $timeout(function() { var x = $window.open(url, '_blank'); if (focus) x.focus() });
706         });
707     }
708
709     service.show_holds_for_title_wide = function(items) {
710         var focus = items.length == 1;
711         angular.forEach(items, function(item) {
712             var url = '/eg2/staff/catalog/record/' + item.hold.record_id + '/holds';
713             $timeout(function() { var x = $window.open(url, '_blank'); if (focus) x.focus() });
714         });
715     }
716
717
718     function generic_update(items, action) {
719         if (!items.length) return $q.when();
720         var hold_ids = items.map(function(item) {return item.hold.id()});
721         return egHolds[action](hold_ids).then(service.refresh);
722     }
723
724     function generic_update_wide(items, action) {
725         if (!items.length) return $q.when();
726         var hold_ids = items.map(function(item) {return item.hold.id});
727         return egHolds[action](hold_ids).then(service.refresh);
728     }
729
730     service.set_copy_quality = function(items) {
731         generic_update(items, 'set_copy_quality'); }
732     service.edit_pickup_lib = function(items) {
733         generic_update(items, 'edit_pickup_lib'); }
734     service.edit_notify_prefs = function(items) {
735         generic_update(items, 'edit_notify_prefs'); }
736     service.edit_dates = function(items) {
737         generic_update(items, 'edit_dates'); }
738     service.suspend = function(items) {
739         generic_update(items, 'suspend_holds'); }
740     service.activate = function(items) {
741         generic_update(items, 'activate_holds'); }
742     service.set_top_of_queue = function(items) {
743         generic_update(items, 'set_top_of_queue'); }
744     service.clear_top_of_queue = function(items) {
745         generic_update(items, 'clear_top_of_queue'); }
746     service.transfer_to_marked_title = function(items) {
747         generic_update(items, 'transfer_to_marked_title'); }
748
749     service.set_copy_quality_wide = function(items) {
750         generic_update_wide(items, 'set_copy_quality'); }
751     service.edit_pickup_lib_wide = function(items) {
752         generic_update_wide(items, 'edit_pickup_lib'); }
753     service.edit_notify_prefs_wide = function(items) {
754         generic_update_wide(items, 'edit_notify_prefs'); }
755     service.edit_dates_wide = function(items) {
756         generic_update_wide(items, 'edit_dates'); }
757     service.suspend_wide = function(items) {
758         generic_update_wide(items, 'suspend_holds'); }
759     service.activate_wide = function(items) {
760         generic_update_wide(items, 'activate_holds'); }
761     service.set_top_of_queue_wide = function(items) {
762         generic_update_wide(items, 'set_top_of_queue'); }
763     service.clear_top_of_queue_wide = function(items) {
764         generic_update_wide(items, 'clear_top_of_queue'); }
765     service.transfer_to_marked_title_wide = function(items) {
766         generic_update_wide(items, 'transfer_to_marked_title'); }
767
768     service.mark_damaged = function(items) {
769         angular.forEach(items, function(item) {
770             if (item.copy) {
771                 egCirc.mark_damaged({
772                     id: item.copy.id(),
773                     barcode: item.copy.barcode()
774                 }).then(service.refresh);
775             }
776         });
777     }
778
779     service.mark_damaged_wide = function(items) {
780         angular.forEach(items, function(item) {
781             if (item.copy) {
782                 egCirc.mark_damaged({
783                     id: item.hold.cp_id,
784                     barcode: item.hold.cp_barcode
785                 }).then(service.refresh);
786             }
787         });
788     }
789
790     service.mark_discard = function(items) {
791         var copies = items
792             .filter(function(item) { return Boolean(item.copy) })
793             .map(function(item) {
794                 return {id: item.copy.id(), barcode: item.copy.barcode()}
795             });
796         if (copies.length)
797             egCirc.mark_discard(copies).then(service.refresh);
798     }
799
800     service.mark_missing = function(items) {
801         var copies = items
802             .filter(function(item) { return Boolean(item.copy) })
803             .map(function(item) {
804                 return {id: item.copy.id(), barcode: item.copy.barcode()}
805             });
806         if (copies.length)
807             egCirc.mark_missing(copies).then(service.refresh);
808     }
809
810     service.mark_missing_wide = function(items) {
811         var copies = items
812             .filter(function(item) { return Boolean(item.hold.cp_id) })
813             .map(function(item) { return {id: item.hold.cp_id, barcode: item.hold.cp_barcode}; });
814         if (copies.length)
815             egCirc.mark_missing(copies).then(service.refresh);
816     }
817
818     service.mark_discard_wide = function(items) {
819         var copies = items
820             .filter(function(item) { return Boolean(item.hold.cp_id) })
821             .map(function(item) { return {id: item.hold.cp_id, barcode: item.hold.cp_barcode}; });
822         if (copies.length)
823             egCirc.mark_discard(copies).then(service.refresh);
824     }
825
826     service.retarget = function(items) {
827         var hold_ids = items.map(function(item) { return item.hold.id() });
828         egHolds.retarget(hold_ids).then(service.refresh);
829     }
830
831     service.retarget_wide = function(items) {
832         var hold_ids = items.map(function(item) { return item.hold.id });
833         egHolds.retarget(hold_ids).then(service.refresh);
834     }
835
836     return service;
837 }])
838
839 /**
840  * Hold details interface 
841  */
842 .directive('egHoldDetails', function() {
843     return {
844         restrict : 'AE',
845         templateUrl : './circ/share/t_hold_details',
846         scope : {
847             holdId : '=',
848             // if set, called whenever hold details are retrieved.  The
849             // argument is the hold blob returned from hold.details.retrieve
850             holdRetrieved : '=',
851             showPatron : '='
852         },
853         controller : [
854                     '$scope','$uibModal','egCore','egHolds','egCirc',
855             function($scope , $uibModal , egCore , egHolds , egCirc) {
856
857                 function draw() {
858                     if (!$scope.holdId) return;
859
860                     egCore.net.request(
861                         'open-ils.circ',
862                         'open-ils.circ.hold.details.retrieve.authoritative',
863                         egCore.auth.token(), $scope.holdId, {
864                             include_current_copy : true,
865                             include_usr          : true,
866                             include_cancel_cause : true,
867                             include_sms_carrier  : true,
868                             include_requestor    : true
869                         }
870                     ).then(function(hold_data) { 
871                         egHolds.local_flesh(hold_data);
872     
873                         angular.forEach(hold_data, 
874                             function(val, key) { $scope[key] = val });
875
876                         // fetch + flesh the cancel_cause if needed
877                         if ($scope.hold.cancel_cause() && typeof $scope.hold.cancel_cause() != 'object') {
878                             egHolds.get_cancel_reasons().then(function() {
879                                 // egHolds caches the causes in egEnv
880                                 $scope.hold.cancel_cause(
881                                     egCore.env.ahrcc.map[$scope.hold.cancel_cause()]);
882                             })
883                         }
884
885                         if ($scope.hold.current_copy()) {
886                             egCirc.flesh_copy_location($scope.hold.current_copy());
887                         }
888
889                         if ($scope.holdRetrieved)
890                             $scope.holdRetrieved(hold_data);
891
892                     });
893                 }
894
895                 $scope.show_notify_tab = function() {
896                     $scope.detail_tab = 'notify';
897                     egCore.pcrud.search('ahn',
898                         {hold : $scope.hold.id()}, 
899                         {flesh : 1, flesh_fields : {ahn : ['notify_staff']}}, 
900                         {atomic : true}
901                     ).then(function(nots) {
902                         $scope.hold.notifications(nots);
903                     });
904                 }
905
906                 $scope.delete_note = function(note) {
907                     egCore.pcrud.remove(note).then(function() {
908                         // remove the deleted note from the locally fleshed notes
909                         $scope.hold.notes(
910                             $scope.hold.notes().filter(function(n) {
911                                 return n.id() != note.id()
912                             })
913                         );
914                     });
915                 }
916
917                 $scope.new_note = function() {
918                     return $uibModal.open({
919                         templateUrl : './circ/share/t_hold_note_dialog',
920                         backdrop: 'static',
921                         controller : 
922                             ['$scope', '$uibModalInstance',
923                             function($scope, $uibModalInstance) {
924                                 $scope.args = {};
925                                 $scope.ok = function() {
926                                     $uibModalInstance.close($scope.args)
927                                 },
928                                 $scope.cancel = function($event) {
929                                     $uibModalInstance.dismiss();
930                                     $event.preventDefault();
931                                 }
932                             }
933                         ]
934                     }).result.then(function(args) {
935                         var note = new egCore.idl.ahrn();
936                         note.hold($scope.hold.id());
937                         note.staff(true);
938                         note.slip(args.slip);
939                         note.pub(args.pub); 
940                         note.title(args.title);
941                         note.body(args.body);
942                         return egCore.pcrud.create(note).then(function(n) {
943                             $scope.hold.notes().push(n);
944                         });
945                     });
946                 }
947
948                 $scope.new_notification = function() {
949                     return $uibModal.open({
950                         templateUrl : './circ/share/t_hold_notification_dialog',
951                         backdrop: 'static',
952                         controller : 
953                             ['$scope', '$uibModalInstance',
954                             function($scope, $uibModalInstance) {
955                                 $scope.args = {};
956                                 $scope.ok = function() {
957                                     $uibModalInstance.close($scope.args)
958                                 },
959                                 $scope.cancel = function($event) {
960                                     $uibModalInstance.dismiss();
961                                     $event.preventDefault();
962                                 }
963                             }
964                         ]
965                     }).result.then(function(args) {
966                         var note = new egCore.idl.ahn();
967                         note.hold($scope.hold.id());
968                         note.method(args.method);
969                         note.note(args.note);
970                         note.notify_staff(egCore.auth.user().id());
971                         note.notify_time('now');
972                         return egCore.pcrud.create(note).then(function(n) {
973                             n.notify_staff(egCore.auth.user());
974                             $scope.hold.notifications().push(n);
975                         });
976                     });
977                 }
978
979                 $scope.$watch('holdId', function(newVal, oldVal) {
980                     if (newVal != oldVal) draw();
981                 });
982
983                 draw();
984             }
985         ]
986     }
987 })
988
989