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