2 * List of patron items checked out
5 angular.module('egPatronApp')
7 .controller('PatronItemsOutCtrl',
8 ['$scope','$q','$routeParams','$timeout','egCore','egUser','patronSvc',
9 '$location','egGridDataProvider','$uibModal','egCirc','egConfirmDialog',
10 'egProgressDialog','egBilling','$window','egBibDisplay',
11 function($scope , $q , $routeParams , $timeout , egCore , egUser , patronSvc ,
12 $location , egGridDataProvider , $uibModal , egCirc , egConfirmDialog ,
13 egProgressDialog , egBilling , $window , egBibDisplay) {
15 // list of noncatatloged circulations. Define before initTab to
16 // avoid any possibility of race condition, since they are loaded
17 // during init, but may be referenced before init completes.
18 $scope.noncat_list = [];
20 $scope.initTab('items_out', $routeParams.id).then(function() {
21 // sort inline to support paging
22 $scope.noncat_list = patronSvc.noncat_ids.sort();
25 // cache of circ objects for grid display
26 patronSvc.items_out = [];
28 // main list of checked out items
29 $scope.main_list = [];
31 // list of alt circs (lost, etc.) and/or check-in with fines circs
35 'ui.circ.suppress_checkin_popups' // add other settings as needed
36 ]).then(function(set) {
37 $scope.suppress_popups = set['ui.circ.suppress_checkin_popups'];
40 // these are fetched during startup (i.e. .configure())
41 // By default, show lost/lo/cr items in the alt list
42 var display_lost = Number(
43 egCore.env.aous['ui.circ.items_out.lost']) || 2;
44 var display_lo = Number(
45 egCore.env.aous['ui.circ.items_out.longoverdue']) || 2;
46 var display_cr = Number(
47 egCore.env.aous['ui.circ.items_out.claimsreturned']) || 2;
49 var fetch_checked_in = true;
50 $scope.show_alt_circs = true;
51 if (display_lost & 4 && display_lo & 4 && display_cr & 4) {
52 // all special types are configured to be hidden once
53 // checked in, so there's no need to fetch checked-in circs.
54 fetch_checked_in = false;
56 if (display_lost & 1 && display_lo & 1 && display_cr & 1) {
57 // additionally, if all types are configured to display
58 // in the main list while checked out, nothing will
59 // ever appear in the alternate list, so we can hide
60 // the alternate list from the UI.
61 $scope.show_alt_circs = false;
65 $scope.items_out_display = 'main';
66 $scope.show_main_list = function(refresh_grid) {
67 // don't need a full reset_page() to swap tabs
68 $scope.items_out_display = 'main';
69 patronSvc.items_out = [];
70 // only refresh the grid when navigating from a tab that
71 // shares the same grid.
72 if (refresh_grid) provider.refresh();
75 $scope.show_alt_list = function(refresh_grid) {
76 // don't need a full reset_page() to swap tabs
77 $scope.items_out_display = 'alt';
78 patronSvc.items_out = [];
79 // only refresh the grid when navigating from a tab that
80 // shares the same grid.
81 if (refresh_grid) provider.refresh();
84 $scope.show_noncat_list = function() {
85 // don't need a full reset_page() to swap tabs
86 $scope.items_out_display = 'noncat';
87 patronSvc.items_out = [];
88 // Grid refresh is not necessary because switching to the
89 // noncat_list always involves instantiating a new grid.
92 $scope.colorizeItemsOutList = {
93 apply: function(item) {
94 var duedate = item.due_date();
95 if (duedate && duedate < new Date().toISOString()) {
101 // Reload the user to pick up changes in items out, fines, etc.
102 // Reload circs since the contents of the main vs. alt list may
104 function reset_page() {
105 patronSvc.refreshPrimary();
106 patronSvc.items_out = [];
107 $scope.main_list = [];
108 $scope.alt_list = [];
109 $timeout(provider.refresh); // allow scope changes to propagate
112 var provider = egGridDataProvider.instance({});
113 $scope.gridDataProvider = provider;
115 function fetch_circs(id_list, offset, count) {
116 if (!id_list.length || id_list.length < offset + 1) return $q.when();
118 var deferred = $q.defer();
121 egProgressDialog.open();
123 // fetch the lot of circs and stream the results back via notify
124 egCore.pcrud.search('circ', {id : id_list},
127 circ : ['target_copy', 'workstation', 'checkin_workstation'],
128 acp : ['call_number', 'holds_count', 'status', 'circ_lib', 'location', 'floating', 'age_protect', 'parts'],
130 acn : ['record', 'owning_lib', 'prefix', 'suffix'],
131 bre : ['wide_display_entry']
133 // avoid fetching the MARC blob by specifying which
134 // fields on the bre to select. More may be needed.
135 // note that fleshed fields are explicitly selected.
136 select : { bre : ['id'] },
137 // TODO: LP#1697954 Fetch all circs on grid render
138 // to support client-side sorting. Migrate to server-side
139 // sorting to avoid the need for fetching all items.
142 // we need an order-by to support paging
143 order_by : {circ : ['xact_start']}
145 }).then(null, null, function(circ) {
146 circ.circ_lib(egCore.org.get(circ.circ_lib())); // local fleshing
148 // Translate bib display field JSON blobs to JS.
149 // Collapse multi/array fields down to comma-separated strings.
150 egBibDisplay.mwdeJSONToJS(
151 circ.target_copy().call_number().record().wide_display_entry(), true);
153 if (circ.target_copy().call_number().id() == -1) {
154 // dummy-up a record for precat items
155 circ.target_copy().call_number().record().wide_display_entry({
156 title : function() {return circ.target_copy().dummy_title()},
157 author : function() {return circ.target_copy().dummy_author()},
158 isbn : function() {return circ.target_copy().dummy_isbn()}
161 circ._parts = circ.target_copy().parts().map(function(part) {
165 patronSvc.items_out.push(circ);
169 var circIds = patronSvc.items_out.map(function(circ) { return circ.id() });
173 'open-ils.actor.user.itemsout.notices',
174 egCore.auth.token(), circIds
176 ).then(deferred.resolve, null, function(notice) {
178 var circ = patronSvc.items_out.filter(
179 function(circ) {return circ.id() == notice.circ_id})[0];
181 if (notice.numNotices) {
182 circ.action_trigger_event_count = notice.numNotices;
183 circ.action_trigger_latest_event_date = notice.lastDt;
186 if (rendered++ >= offset && rendered <= count) {
187 egProgressDialog.close();
188 deferred.notify(circ);
193 return deferred.promise;
196 function fetch_noncat_circs(id_list, offset, count) {
197 if (!id_list.length) return $q.when();
199 var deferred = $q.defer();
202 egCore.pcrud.search('ancc', {id : id_list},
204 flesh_fields : {ancc : ['item_type','staff']},
205 // TODO: LP#1697954 Fetch all circs on grid render
206 // to support client-side sorting. Migrate to server-side
207 // sorting to avoid the need for fetching all items.
210 // we need an order-by to support paging
211 order_by : {circ : ['circ_time']}
213 }).then(deferred.resolve, null, function(noncat_circ) {
215 // calculate the virtual due date from the item type duration
216 var seconds = egCore.date.intervalToSeconds(
217 noncat_circ.item_type().circ_duration());
218 var d = new Date(Date.parse(noncat_circ.circ_time()));
219 d.setSeconds(d.getSeconds() + seconds);
220 noncat_circ.duedate(d.toISOString());
222 // local flesh org unit
223 noncat_circ.circ_lib(egCore.org.get(noncat_circ.circ_lib()));
225 patronSvc.items_out.push(noncat_circ); // cache it
227 // We fetch all noncat circs for client-side sorting, but
228 // only notify the caller for the page of requested circs.
229 if (rendered++ >= offset && rendered <= count)
230 deferred.notify(noncat_circ);
233 return deferred.promise;
237 // decide which list each circ belongs to
238 function promote_circs(list, display_code, open) {
240 if (1 & display_code) { // bitflag 1 == top list
241 $scope.main_list = $scope.main_list.concat(list);
243 $scope.alt_list = $scope.alt_list.concat(list);
246 if (4 & display_code) return; // bitflag 4 == hide on checkin
247 $scope.alt_list = $scope.alt_list.concat(list);
251 // fetch IDs for circs we care about
252 function get_circ_ids() {
253 $scope.main_list = [];
254 $scope.alt_list = [];
256 // we can fetch these in parallel
257 var promise1 = egCore.net.request(
259 'open-ils.actor.user.checked_out.authoritative',
260 egCore.auth.token(), $scope.patron_id
261 ).then(function(outs) {
262 $scope.main_list = outs.overdue.concat(outs.out);
263 promote_circs(outs.lost, display_lost, true);
264 promote_circs(outs.long_overdue, display_lo, true);
265 promote_circs(outs.claims_returned, display_cr, true);
268 // only fetched checked-in-with-bills circs if configured to display
269 var promise2 = !fetch_checked_in ? $q.when() : egCore.net.request(
271 'open-ils.actor.user.checked_in_with_fines.authoritative',
272 egCore.auth.token(), $scope.patron_id
273 ).then(function(outs) {
274 promote_circs(outs.lost, display_lost);
275 promote_circs(outs.long_overdue, display_lo);
276 promote_circs(outs.claims_returned, display_cr);
279 return $q.all([promise1, promise2]);
282 provider.get = function(offset, count) {
284 var id_list = $scope[$scope.items_out_display + '_list'];
286 // see if we have the requested range cached
287 // Note this items_out list is reset w/ each items-out tab change
288 if (patronSvc.items_out[offset]) {
289 return provider.arrayNotifier(
290 patronSvc.items_out, offset, count);
293 if ($scope.items_out_display == 'noncat') {
294 // if there are any noncat circ IDs, we already have them
295 return fetch_noncat_circs(id_list, offset, count);
298 // See if we have the circ IDs for this range already loaded.
299 // this would happen navigating to a subsequent page.
300 if (id_list[offset]) {
301 return fetch_circs(id_list, offset, count);
304 // avoid returning the request directly to the caller so the
305 // notify()'s from egCore.net.request don't leak into the
306 // final set of notifies (i.e. the real responses);
308 var deferred = $q.defer();
309 get_circ_ids().then(function() {
311 id_list = $scope[$scope.items_out_display + '_list'];
312 $scope.gridDataProvider.grid.totalCount = id_list.length;
313 // relay the notified circs back to the grid through our promise
314 fetch_circs(id_list, offset, count).then(
315 deferred.resolve, null, deferred.notify);
318 return deferred.promise;
322 // true if circ is overdue, false otherwise
323 $scope.circIsOverdue = function(circ) {
324 // circ may not exist yet for rendered row
325 if (!circ) return false;
327 var date = new Date();
328 date.setTime(Date.parse(circ.due_date()));
329 return date < new Date();
332 $scope.edit_due_date = function(items) {
333 if (!items.length) return;
336 templateUrl : './circ/patron/t_edit_due_date_dialog',
339 '$scope','$uibModalInstance',
340 function($scope , $uibModalInstance) {
342 // if there is only one circ, default to the due date
343 // of that circ. Otherwise, default to today.
344 var due_date = items.length == 1 ?
345 Date.parse(items[0].due_date()) : new Date();
348 num_circs : items.length,
352 // Fire off the due-date updater for each circ.
353 // When all is done, close the dialog
354 $scope.ok = function(args) {
355 var due = $scope.args.due_date.toISOString();
356 console.debug("applying due date of " + due);
359 angular.forEach(items, function(circ) {
363 'open-ils.circ.circulation.due_date.update',
364 egCore.auth.token(), circ.id(), due
366 ).then(function(new_circ) {
367 // update the grid circ with the canonical
368 // date from the modified circulation.
369 circ.due_date(new_circ.due_date());
374 $q.all(promises).then(function() {
375 $uibModalInstance.close();
379 $scope.cancel = function($event) {
380 $uibModalInstance.dismiss();
381 $event.preventDefault();
388 $scope.print_receipt = function(items) {
389 if (items.length == 0) return $q.when();
390 var print_data = {circulations : []};
391 var cusr = patronSvc.current;
393 angular.forEach(items, function(circ) {
394 print_data.circulations.push({
395 circ : egCore.idl.toHash(circ),
396 copy : egCore.idl.toHash(circ.target_copy()),
397 call_number : egCore.idl.toHash(circ.target_copy().call_number()),
398 title : circ.target_copy().call_number().record().wide_display_entry().title(),
399 author : circ.target_copy().call_number().record().wide_display_entry().author()
403 print_data.patron = {
404 prefix : cusr.prefix(),
405 first_given_name : cusr.first_given_name(),
406 second_given_name : cusr.second_given_name(),
407 family_name : cusr.family_name(),
408 suffix : cusr.suffix(),
409 pref_prefix : cusr.pref_prefix(),
410 pref_first_given_name : cusr.pref_first_given_name(),
411 pref_second_given_name : cusr.pref_second_given_name(),
412 pref_family_name : cusr.pref_family_name(),
413 pref_suffix : cusr.pref_suffix(),
414 card : { barcode : cusr.card().barcode() },
415 money_summary : patronSvc.patron_stats.fines,
416 expire_date : cusr.expire_date(),
417 alias : cusr.alias(),
418 has_email : Boolean(patronSvc.current.email() && patronSvc.current.email().match(/.*@.*/)),
419 has_phone : Boolean(cusr.day_phone() || cusr.evening_phone() || cusr.other_phone())
422 return egCore.print.print({
424 template : 'items_out',
429 function batch_action_with_flat_copies(items, action) {
430 if (!items.length) return;
431 var copies = items.map(function(circ)
432 { return egCore.idl.toHash(circ.target_copy()) });
433 action(copies).then(reset_page);
435 function batch_action_with_barcodes(items, action) {
436 if (!items.length) return;
437 var barcodes = items.map(function(circ)
438 { return circ.target_copy().barcode() });
439 action(barcodes).then(reset_page);
441 $scope.mark_damaged = function(items) {
442 if (items.length == 0) return;
444 angular.forEach(items, function(circ) {
445 egCirc.mark_damaged({
446 id: circ.target_copy().id(),
447 barcode: circ.target_copy().barcode(),
448 circ_lib: circ.target_copy().circ_lib().id()
449 }).then(() => $timeout(reset_page,1000)) // reset after each, because rejecting one stops the $q.all() chain
452 $scope.mark_missing = function(items) {
453 batch_action_with_flat_copies(items, egCirc.mark_missing);
455 $scope.mark_lost = function(items) {
456 batch_action_with_barcodes(items, egCirc.mark_lost);
458 $scope.mark_claims_returned = function(items) {
459 batch_action_with_barcodes(items, egCirc.mark_claims_returned_dialog);
461 $scope.mark_claims_never_checked_out = function(items) {
462 batch_action_with_barcodes(items, egCirc.mark_claims_never_checked_out);
465 $scope.show_recent_circs = function(items) {
466 var focus = items.length == 1;
467 angular.forEach(items, function(item) {
468 var url = egCore.env.basePath +
470 item.target_copy().id() +
472 $timeout(function() { var x = $window.open(url, '_blank'); if (focus) x.focus() });
476 $scope.show_triggered_events = function(items) {
477 var focus = items.length == 1;
478 angular.forEach(items, function(item) {
479 var url = '/eg2/staff/circ/item/event-log/' +
480 item.target_copy().id();
481 $timeout(function() { var x = $window.open(url, '_blank'); if (focus) x.focus() });
486 $scope.renew = function(items, msg) {
487 if (!items.length) return;
488 var barcodes = items.map(function(circ)
489 { return circ.target_copy().barcode() });
491 if (!msg) msg = egCore.strings.RENEW_ITEMS;
493 return egConfirmDialog.open(msg, barcodes.join(' '), {}).result
495 window.oils_cancel_batch = false;
496 window.oils_inside_batch = true;
497 function batch_cleanup() {
498 if (window.oils_inside_batch && window.oils_op_change_within_batch) {
499 window.oils_op_change_undo_func();
501 window.oils_inside_batch = false;
502 window.oils_op_change_within_batch = false;
506 var bc = barcodes.pop();
511 if (window.oils_op_change_within_batch) {
512 window.oils_op_change_toast_func();
514 // finally -> continue even when one fails
515 egCirc.renew({copy_barcode : bc}).finally(function() {
516 if (!window.oils_cancel_batch) {
519 console.log('batch cancelled');
529 $scope.renew_all = function() {
530 var circs = patronSvc.items_out.filter(function(circ) {
532 // all others will be rejected at the server
533 !circ.stop_fines() ||
534 circ.stop_fines() == 'MAXFINES'
537 $scope.renew(circs, egCore.strings.RENEW_ALL_ITEMS);
540 $scope.renew_with_date = function(items) {
541 if (!items.length) return;
542 var barcodes = items.map(function(circ)
543 { return circ.target_copy().barcode() });
545 return $uibModal.open({
546 templateUrl : './circ/patron/t_renew_with_date_dialog',
549 '$scope','$uibModalInstance',
550 function($scope , $uibModalInstance) {
551 var now = new Date();
552 $scope.outOfRange = false;
553 $scope.minDate = new Date(now);
558 $scope.cancel = function() {$uibModalInstance.dismiss()}
560 // Fire off the due-date updater for each circ.
561 // When all is done, close the dialog
562 $scope.ok = function() {
563 var due = $scope.args.date.toISOString().replace(/T.*/,'');
564 console.debug("renewing with due date: " + due);
567 if (bc = barcodes.pop()) {
568 egCirc.renew({copy_barcode : bc, due_date : due})
571 $uibModalInstance.close();
575 do_one(); // kick it off
582 $scope.checkin = function(items) {
583 if (!items.length) return;
584 var copies = items.map(function(circ) { return circ.target_copy() });
585 var barcodes = copies.map(function(copy) { return copy.barcode() });
589 if (copy = copies.pop()) {
590 // Checkin expects a barcode, but will pass other
591 // parameters too. Passing the copy ID allows
592 // for the checkin of deleted copies on the server.
594 {copy_barcode: copy.barcode(), copy_id: copy.id()},
595 {suppress_popups: $scope.suppress_popups})
601 if ($scope.suppress_popups) {
604 return egConfirmDialog.open(
605 egCore.strings.CHECK_IN_CONFIRM, barcodes.join(' '), {
607 }).result.then(function() {
608 do_one(); // kick it off
613 $scope.add_billing = function(items) {
614 if (!items.length) return;
615 var circs = items.concat(); // don't pop from grid array
617 var circ; // don't clobber window.circ!
618 if (circ = circs.pop()) {
619 egBilling.showBillDialog({
620 // let the dialog fetch the transaction, since it's
621 // not sufficiently fleshed here.
623 patron : patronSvc.current