2 * List of patron items checked out
5 angular.module('egPatronApp')
7 .controller('PatronItemsOutCtrl',
8 ['$scope','$q','$routeParams','$timeout','egCore','egUser','patronSvc','$location',
9 'egGridDataProvider','$uibModal','egCirc','egConfirmDialog','egBilling','$window',
10 function($scope, $q, $routeParams, $timeout, egCore , egUser, patronSvc , $location,
11 egGridDataProvider , $uibModal , egCirc , egConfirmDialog , egBilling , $window) {
13 // list of noncatatloged circulations. Define before initTab to
14 // avoid any possibility of race condition, since they are loaded
15 // during init, but may be referenced before init completes.
16 $scope.noncat_list = [];
18 $scope.initTab('items_out', $routeParams.id).then(function() {
19 // sort inline to support paging
20 $scope.noncat_list = patronSvc.noncat_ids.sort();
23 // cache of circ objects for grid display
24 patronSvc.items_out = [];
26 // main list of checked out items
27 $scope.main_list = [];
29 // list of alt circs (lost, etc.) and/or check-in with fines circs
32 // these are fetched during startup (i.e. .configure())
33 // By default, show lost/lo/cr items in the alt list
34 var display_lost = Number(
35 egCore.env.aous['ui.circ.items_out.lost']) || 2;
36 var display_lo = Number(
37 egCore.env.aous['ui.circ.items_out.longoverdue']) || 2;
38 var display_cr = Number(
39 egCore.env.aous['ui.circ.items_out.claimsreturned']) || 2;
41 var fetch_checked_in = true;
42 $scope.show_alt_circs = true;
43 if (display_lost & 4 && display_lo & 4 && display_cr & 4) {
44 // all special types are configured to be hidden once
45 // checked in, so there's no need to fetch checked-in circs.
46 fetch_checked_in = false;
48 if (display_lost & 1 && display_lo & 1 && display_cr & 1) {
49 // additionally, if all types are configured to display
50 // in the main list while checked out, nothing will
51 // ever appear in the alternate list, so we can hide
52 // the alternate list from the UI.
53 $scope.show_alt_circs = false;
57 $scope.items_out_display = 'main';
58 $scope.show_main_list = function(refresh_grid) {
59 // don't need a full reset_page() to swap tabs
60 $scope.items_out_display = 'main';
61 patronSvc.items_out = [];
62 // only refresh the grid when navigating from a tab that
63 // shares the same grid.
64 if (refresh_grid) provider.refresh();
67 $scope.show_alt_list = function(refresh_grid) {
68 // don't need a full reset_page() to swap tabs
69 $scope.items_out_display = 'alt';
70 patronSvc.items_out = [];
71 // only refresh the grid when navigating from a tab that
72 // shares the same grid.
73 if (refresh_grid) provider.refresh();
76 $scope.show_noncat_list = function() {
77 // don't need a full reset_page() to swap tabs
78 $scope.items_out_display = 'noncat';
79 patronSvc.items_out = [];
80 // Grid refresh is not necessary because switching to the
81 // noncat_list always involves instantiating a new grid.
84 // Reload the user to pick up changes in items out, fines, etc.
85 // Reload circs since the contents of the main vs. alt list may
87 function reset_page() {
88 patronSvc.refreshPrimary();
89 patronSvc.items_out = [];
90 $scope.main_list = [];
92 $timeout(provider.refresh); // allow scope changes to propagate
95 var provider = egGridDataProvider.instance({});
96 $scope.gridDataProvider = provider;
98 function fetch_circs(id_list, offset, count) {
99 if (!id_list.length) return $q.when();
101 var deferred = $q.defer();
104 // fetch the lot of circs and stream the results back via notify
105 egCore.pcrud.search('circ', {id : id_list},
108 circ : ['target_copy', 'workstation', 'checkin_workstation'],
109 acp : ['call_number', 'holds_count', 'status', 'circ_lib'],
110 acn : ['record', 'owning_lib'],
111 bre : ['simple_record']
113 // avoid fetching the MARC blob by specifying which
114 // fields on the bre to select. More may be needed.
115 // note that fleshed fields are explicitly selected.
116 select : { bre : ['id'] },
117 // TODO: LP#1697954 Fetch all circs on grid render
118 // to support client-side sorting. Migrate to server-side
119 // sorting to avoid the need for fetching all items.
122 // we need an order-by to support paging
123 order_by : {circ : ['xact_start']}
125 }).then(deferred.resolve, null, function(circ) {
126 circ.circ_lib(egCore.org.get(circ.circ_lib())); // local fleshing
128 if (circ.target_copy().call_number().id() == -1) {
129 // dummy-up a record for precat items
130 circ.target_copy().call_number().record().simple_record({
131 title : function() {return circ.target_copy().dummy_title()},
132 author : function() {return circ.target_copy().dummy_author()},
133 isbn : function() {return circ.target_copy().dummy_isbn()}
137 patronSvc.items_out.push(circ); // toss it into the cache
139 // We fetch all circs for client-side sorting, but only
140 // notify the caller for the page of requested circs.
141 if (rendered++ >= offset && rendered <= count)
142 deferred.notify(circ);
145 return deferred.promise;
148 function fetch_noncat_circs(id_list, offset, count) {
149 if (!id_list.length) return $q.when();
151 var deferred = $q.defer();
154 egCore.pcrud.search('ancc', {id : id_list},
156 flesh_fields : {ancc : ['item_type','staff']},
157 // TODO: LP#1697954 Fetch all circs on grid render
158 // to support client-side sorting. Migrate to server-side
159 // sorting to avoid the need for fetching all items.
162 // we need an order-by to support paging
163 order_by : {circ : ['circ_time']}
165 }).then(deferred.resolve, null, function(noncat_circ) {
167 // calculate the virtual due date from the item type duration
168 var seconds = egCore.date.intervalToSeconds(
169 noncat_circ.item_type().circ_duration());
170 var d = new Date(Date.parse(noncat_circ.circ_time()));
171 d.setSeconds(d.getSeconds() + seconds);
172 noncat_circ.duedate(d.toISOString());
174 // local flesh org unit
175 noncat_circ.circ_lib(egCore.org.get(noncat_circ.circ_lib()));
177 patronSvc.items_out.push(noncat_circ); // cache it
179 // We fetch all noncat circs for client-side sorting, but
180 // only notify the caller for the page of requested circs.
181 if (rendered++ >= offset && rendered <= count)
182 deferred.notify(noncat_circ);
185 return deferred.promise;
189 // decide which list each circ belongs to
190 function promote_circs(list, display_code, open) {
192 if (1 & display_code) { // bitflag 1 == top list
193 $scope.main_list = $scope.main_list.concat(list);
195 $scope.alt_list = $scope.alt_list.concat(list);
198 if (4 & display_code) return; // bitflag 4 == hide on checkin
199 $scope.alt_list = $scope.alt_list.concat(list);
203 // fetch IDs for circs we care about
204 function get_circ_ids() {
205 $scope.main_list = [];
206 $scope.alt_list = [];
208 // we can fetch these in parallel
209 var promise1 = egCore.net.request(
211 'open-ils.actor.user.checked_out.authoritative',
212 egCore.auth.token(), $scope.patron_id
213 ).then(function(outs) {
214 $scope.main_list = outs.overdue.concat(outs.out);
215 promote_circs(outs.lost, display_lost, true);
216 promote_circs(outs.long_overdue, display_lo, true);
217 promote_circs(outs.claims_returned, display_cr, true);
220 // only fetched checked-in-with-bills circs if configured to display
221 var promise2 = !fetch_checked_in ? $q.when() : egCore.net.request(
223 'open-ils.actor.user.checked_in_with_fines.authoritative',
224 egCore.auth.token(), $scope.patron_id
225 ).then(function(outs) {
226 promote_circs(outs.lost, display_lost);
227 promote_circs(outs.long_overdue, display_lo);
228 promote_circs(outs.claims_returned, display_cr);
231 return $q.all([promise1, promise2]);
234 provider.get = function(offset, count) {
236 var id_list = $scope[$scope.items_out_display + '_list'];
238 // see if we have the requested range cached
239 // Note this items_out list is reset w/ each items-out tab change
240 if (patronSvc.items_out[offset]) {
241 return provider.arrayNotifier(
242 patronSvc.items_out, offset, count);
245 if ($scope.items_out_display == 'noncat') {
246 // if there are any noncat circ IDs, we already have them
247 return fetch_noncat_circs(id_list, offset, count);
250 // See if we have the circ IDs for this range already loaded.
251 // this would happen navigating to a subsequent page.
252 if (id_list[offset]) {
253 return fetch_circs(id_list, offset, count);
256 // avoid returning the request directly to the caller so the
257 // notify()'s from egCore.net.request don't leak into the
258 // final set of notifies (i.e. the real responses);
260 var deferred = $q.defer();
261 get_circ_ids().then(function() {
263 id_list = $scope[$scope.items_out_display + '_list'];
265 // relay the notified circs back to the grid through our promise
266 fetch_circs(id_list, offset, count).then(
267 deferred.resolve, null, deferred.notify);
270 return deferred.promise;
274 // true if circ is overdue, false otherwise
275 $scope.circIsOverdue = function(circ) {
276 // circ may not exist yet for rendered row
277 if (!circ) return false;
279 var date = new Date();
280 date.setTime(Date.parse(circ.due_date()));
281 return date < new Date();
284 $scope.edit_due_date = function(items) {
285 if (!items.length) return;
288 templateUrl : './circ/patron/t_edit_due_date_dialog',
291 '$scope','$uibModalInstance',
292 function($scope , $uibModalInstance) {
294 // if there is only one circ, default to the due date
295 // of that circ. Otherwise, default to today.
296 var due_date = items.length == 1 ?
297 Date.parse(items[0].due_date()) : new Date();
300 num_circs : items.length,
304 // Fire off the due-date updater for each circ.
305 // When all is done, close the dialog
306 $scope.ok = function(args) {
307 // toISOString gives us Zulu time, so
308 // adjust for that before truncating to date
309 var adjust_date = new Date( $scope.args.due_date );
310 adjust_date.setMinutes(
311 $scope.args.due_date.getMinutes() - adjust_date.getTimezoneOffset()
313 var due = adjust_date.toISOString().replace(/T.*/,'');
314 console.debug("applying due date of " + due);
317 angular.forEach(items, function(circ) {
321 'open-ils.circ.circulation.due_date.update',
322 egCore.auth.token(), circ.id(), due
324 ).then(function(new_circ) {
325 // update the grid circ with the canonical
326 // date from the modified circulation.
327 circ.due_date(new_circ.due_date());
332 $q.all(promises).then(function() {
333 $uibModalInstance.close();
337 $scope.cancel = function($event) {
338 $uibModalInstance.dismiss();
339 $event.preventDefault();
346 $scope.print_receipt = function(items) {
347 if (items.length == 0) return $q.when();
348 var print_data = {circulations : []}
350 angular.forEach(patronSvc.items_out, function(circ) {
351 print_data.circulations.push({
352 circ : egCore.idl.toHash(circ),
353 copy : egCore.idl.toHash(circ.target_copy()),
354 call_number : egCore.idl.toHash(circ.target_copy().call_number()),
355 title : circ.target_copy().call_number().record().simple_record().title(),
356 author : circ.target_copy().call_number().record().simple_record().author(),
360 return egCore.print.print({
362 template : 'items_out',
367 function batch_action_with_barcodes(items, action) {
368 if (!items.length) return;
369 var barcodes = items.map(function(circ)
370 { return circ.target_copy().barcode() });
371 action(barcodes).then(reset_page);
373 $scope.mark_lost = function(items) {
374 batch_action_with_barcodes(items, egCirc.mark_lost);
376 $scope.mark_claims_returned = function(items) {
377 batch_action_with_barcodes(items, egCirc.mark_claims_returned_dialog);
379 $scope.mark_claims_never_checked_out = function(items) {
380 batch_action_with_barcodes(items, egCirc.mark_claims_never_checked_out);
383 $scope.show_recent_circs = function(items) {
384 var focus = items.length == 1;
385 angular.forEach(items, function(item) {
386 var url = egCore.env.basePath +
388 item.target_copy().id() +
390 $timeout(function() { var x = $window.open(url, '_blank'); if (focus) x.focus() });
394 $scope.show_triggered_events = function(items) {
395 var focus = items.length == 1;
396 angular.forEach(items, function(item) {
397 var url = egCore.env.basePath +
399 item.target_copy().id() +
401 $timeout(function() { var x = $window.open(url, '_blank'); if (focus) x.focus() });
405 $scope.renew = function(items, msg) {
406 if (!items.length) return;
407 var barcodes = items.map(function(circ)
408 { return circ.target_copy().barcode() });
410 if (!msg) msg = egCore.strings.RENEW_ITEMS;
412 return egConfirmDialog.open(msg, barcodes.join(' '), {}).result
415 var bc = barcodes.pop();
416 if (!bc) { reset_page(); return }
417 // finally -> continue even when one fails
418 egCirc.renew({copy_barcode : bc}).finally(do_one);
424 $scope.renew_all = function() {
425 var circs = patronSvc.items_out.filter(function(circ) {
427 // all others will be rejected at the server
428 !circ.stop_fines() ||
429 circ.stop_fines() == 'MAXFINES'
432 $scope.renew(circs, egCore.strings.RENEW_ALL_ITEMS);
435 $scope.renew_with_date = function(items) {
436 if (!items.length) return;
437 var barcodes = items.map(function(circ)
438 { return circ.target_copy().barcode() });
440 return $uibModal.open({
441 templateUrl : './circ/patron/t_edit_due_date_dialog',
442 templateUrl : './circ/patron/t_renew_with_date_dialog',
445 '$scope','$uibModalInstance',
446 function($scope , $uibModalInstance) {
451 $scope.cancel = function() {$uibModalInstance.dismiss()}
453 // Fire off the due-date updater for each circ.
454 // When all is done, close the dialog
455 $scope.ok = function() {
456 var due = $scope.args.date.toISOString().replace(/T.*/,'');
457 console.debug("renewing with due date: " + due);
460 if (bc = barcodes.pop()) {
461 egCirc.renew({copy_barcode : bc, due_date : due})
464 $uibModalInstance.close();
468 do_one(); // kick it off
475 $scope.checkin = function(items) {
476 if (!items.length) return;
477 var barcodes = items.map(function(circ)
478 { return circ.target_copy().barcode() });
480 return egConfirmDialog.open(
481 egCore.strings.CHECK_IN_CONFIRM, barcodes.join(' '), {
483 }).result.then(function() {
485 if (bc = barcodes.pop()) {
486 egCirc.checkin({copy_barcode : bc})
492 do_one(); // kick it off
496 $scope.add_billing = function(items) {
497 if (!items.length) return;
498 var circs = items.concat(); // don't pop from grid array
500 var circ; // don't clobber window.circ!
501 if (circ = circs.pop()) {
502 egBilling.showBillDialog({
503 // let the dialog fetch the transaction, since it's
504 // not sufficiently fleshed here.
506 patron : patronSvc.current