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 'egBilling','$window','egBibDisplay',
11 function($scope , $q , $routeParams , $timeout , egCore , egUser , patronSvc ,
12 $location , egGridDataProvider , $uibModal , egCirc , egConfirmDialog ,
13 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
34 // these are fetched during startup (i.e. .configure())
35 // By default, show lost/lo/cr items in the alt list
36 var display_lost = Number(
37 egCore.env.aous['ui.circ.items_out.lost']) || 2;
38 var display_lo = Number(
39 egCore.env.aous['ui.circ.items_out.longoverdue']) || 2;
40 var display_cr = Number(
41 egCore.env.aous['ui.circ.items_out.claimsreturned']) || 2;
43 var fetch_checked_in = true;
44 $scope.show_alt_circs = true;
45 if (display_lost & 4 && display_lo & 4 && display_cr & 4) {
46 // all special types are configured to be hidden once
47 // checked in, so there's no need to fetch checked-in circs.
48 fetch_checked_in = false;
50 if (display_lost & 1 && display_lo & 1 && display_cr & 1) {
51 // additionally, if all types are configured to display
52 // in the main list while checked out, nothing will
53 // ever appear in the alternate list, so we can hide
54 // the alternate list from the UI.
55 $scope.show_alt_circs = false;
59 $scope.items_out_display = 'main';
60 $scope.show_main_list = function(refresh_grid) {
61 // don't need a full reset_page() to swap tabs
62 $scope.items_out_display = 'main';
63 patronSvc.items_out = [];
64 // only refresh the grid when navigating from a tab that
65 // shares the same grid.
66 if (refresh_grid) provider.refresh();
69 $scope.show_alt_list = function(refresh_grid) {
70 // don't need a full reset_page() to swap tabs
71 $scope.items_out_display = 'alt';
72 patronSvc.items_out = [];
73 // only refresh the grid when navigating from a tab that
74 // shares the same grid.
75 if (refresh_grid) provider.refresh();
78 $scope.show_noncat_list = function() {
79 // don't need a full reset_page() to swap tabs
80 $scope.items_out_display = 'noncat';
81 patronSvc.items_out = [];
82 // Grid refresh is not necessary because switching to the
83 // noncat_list always involves instantiating a new grid.
86 // Reload the user to pick up changes in items out, fines, etc.
87 // Reload circs since the contents of the main vs. alt list may
89 function reset_page() {
90 patronSvc.refreshPrimary();
91 patronSvc.items_out = [];
92 $scope.main_list = [];
94 $timeout(provider.refresh); // allow scope changes to propagate
97 var provider = egGridDataProvider.instance({});
98 $scope.gridDataProvider = provider;
100 function fetch_circs(id_list, offset, count) {
101 if (!id_list.length || id_list.length < offset + 1) return $q.when();
103 var deferred = $q.defer();
106 // fetch the lot of circs and stream the results back via notify
107 egCore.pcrud.search('circ', {id : id_list},
110 circ : ['target_copy', 'workstation', 'checkin_workstation'],
111 acp : ['call_number', 'holds_count', 'status', 'circ_lib'],
112 acn : ['record', 'owning_lib', 'prefix', 'suffix'],
113 bre : ['wide_display_entry']
115 // avoid fetching the MARC blob by specifying which
116 // fields on the bre to select. More may be needed.
117 // note that fleshed fields are explicitly selected.
118 select : { bre : ['id'] },
119 // TODO: LP#1697954 Fetch all circs on grid render
120 // to support client-side sorting. Migrate to server-side
121 // sorting to avoid the need for fetching all items.
124 // we need an order-by to support paging
125 order_by : {circ : ['xact_start']}
127 }).then(deferred.resolve, null, function(circ) {
128 circ.circ_lib(egCore.org.get(circ.circ_lib())); // local fleshing
130 // Translate bib display field JSON blobs to JS.
131 egBibDisplay.mwdeJSONToJS(
132 circ.target_copy().call_number().record().wide_display_entry());
134 if (circ.target_copy().call_number().id() == -1) {
135 // dummy-up a record for precat items
136 circ.target_copy().call_number().record().wide_display_entry({
137 title : function() {return circ.target_copy().dummy_title()},
138 author : function() {return circ.target_copy().dummy_author()},
139 // ISBN is a multi=true field.
140 isbn : function() {return [circ.target_copy().dummy_isbn()]}
144 patronSvc.items_out.push(circ); // toss it into the cache
146 // We fetch all circs for client-side sorting, but only
147 // notify the caller for the page of requested circs.
148 if (rendered++ >= offset && rendered <= count)
149 deferred.notify(circ);
152 return deferred.promise;
155 function fetch_noncat_circs(id_list, offset, count) {
156 if (!id_list.length) return $q.when();
158 var deferred = $q.defer();
161 egCore.pcrud.search('ancc', {id : id_list},
163 flesh_fields : {ancc : ['item_type','staff']},
164 // TODO: LP#1697954 Fetch all circs on grid render
165 // to support client-side sorting. Migrate to server-side
166 // sorting to avoid the need for fetching all items.
169 // we need an order-by to support paging
170 order_by : {circ : ['circ_time']}
172 }).then(deferred.resolve, null, function(noncat_circ) {
174 // calculate the virtual due date from the item type duration
175 var seconds = egCore.date.intervalToSeconds(
176 noncat_circ.item_type().circ_duration());
177 var d = new Date(Date.parse(noncat_circ.circ_time()));
178 d.setSeconds(d.getSeconds() + seconds);
179 noncat_circ.duedate(d.toISOString());
181 // local flesh org unit
182 noncat_circ.circ_lib(egCore.org.get(noncat_circ.circ_lib()));
184 patronSvc.items_out.push(noncat_circ); // cache it
186 // We fetch all noncat circs for client-side sorting, but
187 // only notify the caller for the page of requested circs.
188 if (rendered++ >= offset && rendered <= count)
189 deferred.notify(noncat_circ);
192 return deferred.promise;
196 // decide which list each circ belongs to
197 function promote_circs(list, display_code, open) {
199 if (1 & display_code) { // bitflag 1 == top list
200 $scope.main_list = $scope.main_list.concat(list);
202 $scope.alt_list = $scope.alt_list.concat(list);
205 if (4 & display_code) return; // bitflag 4 == hide on checkin
206 $scope.alt_list = $scope.alt_list.concat(list);
210 // fetch IDs for circs we care about
211 function get_circ_ids() {
212 $scope.main_list = [];
213 $scope.alt_list = [];
215 // we can fetch these in parallel
216 var promise1 = egCore.net.request(
218 'open-ils.actor.user.checked_out.authoritative',
219 egCore.auth.token(), $scope.patron_id
220 ).then(function(outs) {
221 $scope.main_list = outs.overdue.concat(outs.out);
222 promote_circs(outs.lost, display_lost, true);
223 promote_circs(outs.long_overdue, display_lo, true);
224 promote_circs(outs.claims_returned, display_cr, true);
227 // only fetched checked-in-with-bills circs if configured to display
228 var promise2 = !fetch_checked_in ? $q.when() : egCore.net.request(
230 'open-ils.actor.user.checked_in_with_fines.authoritative',
231 egCore.auth.token(), $scope.patron_id
232 ).then(function(outs) {
233 promote_circs(outs.lost, display_lost);
234 promote_circs(outs.long_overdue, display_lo);
235 promote_circs(outs.claims_returned, display_cr);
238 return $q.all([promise1, promise2]);
241 provider.get = function(offset, count) {
243 var id_list = $scope[$scope.items_out_display + '_list'];
245 // see if we have the requested range cached
246 // Note this items_out list is reset w/ each items-out tab change
247 if (patronSvc.items_out[offset]) {
248 return provider.arrayNotifier(
249 patronSvc.items_out, offset, count);
252 if ($scope.items_out_display == 'noncat') {
253 // if there are any noncat circ IDs, we already have them
254 return fetch_noncat_circs(id_list, offset, count);
257 // See if we have the circ IDs for this range already loaded.
258 // this would happen navigating to a subsequent page.
259 if (id_list[offset]) {
260 return fetch_circs(id_list, offset, count);
263 // avoid returning the request directly to the caller so the
264 // notify()'s from egCore.net.request don't leak into the
265 // final set of notifies (i.e. the real responses);
267 var deferred = $q.defer();
268 get_circ_ids().then(function() {
270 id_list = $scope[$scope.items_out_display + '_list'];
271 $scope.gridDataProvider.grid.totalCount = id_list.length;
272 // relay the notified circs back to the grid through our promise
273 fetch_circs(id_list, offset, count).then(
274 deferred.resolve, null, deferred.notify);
277 return deferred.promise;
281 // true if circ is overdue, false otherwise
282 $scope.circIsOverdue = function(circ) {
283 // circ may not exist yet for rendered row
284 if (!circ) return false;
286 var date = new Date();
287 date.setTime(Date.parse(circ.due_date()));
288 return date < new Date();
291 $scope.edit_due_date = function(items) {
292 if (!items.length) return;
295 templateUrl : './circ/patron/t_edit_due_date_dialog',
298 '$scope','$uibModalInstance',
299 function($scope , $uibModalInstance) {
301 // if there is only one circ, default to the due date
302 // of that circ. Otherwise, default to today.
303 var due_date = items.length == 1 ?
304 Date.parse(items[0].due_date()) : new Date();
307 num_circs : items.length,
311 // Fire off the due-date updater for each circ.
312 // When all is done, close the dialog
313 $scope.ok = function(args) {
314 // toISOString gives us Zulu time, so
315 // adjust for that before truncating to date
316 var adjust_date = new Date( $scope.args.due_date );
317 adjust_date.setMinutes(
318 $scope.args.due_date.getMinutes() - adjust_date.getTimezoneOffset()
320 var due = adjust_date.toISOString().replace(/T.*/,'');
321 console.debug("applying due date of " + due);
324 angular.forEach(items, function(circ) {
328 'open-ils.circ.circulation.due_date.update',
329 egCore.auth.token(), circ.id(), due
331 ).then(function(new_circ) {
332 // update the grid circ with the canonical
333 // date from the modified circulation.
334 circ.due_date(new_circ.due_date());
339 $q.all(promises).then(function() {
340 $uibModalInstance.close();
344 $scope.cancel = function($event) {
345 $uibModalInstance.dismiss();
346 $event.preventDefault();
353 $scope.print_receipt = function(items) {
354 if (items.length == 0) return $q.when();
355 var print_data = {circulations : []};
356 var cusr = patronSvc.current;
358 angular.forEach(patronSvc.items_out, function(circ) {
359 print_data.circulations.push({
360 circ : egCore.idl.toHash(circ),
361 copy : egCore.idl.toHash(circ.target_copy()),
362 call_number : egCore.idl.toHash(circ.target_copy().call_number()),
363 title : circ.target_copy().call_number().record().wide_display_entry().title(),
364 author : circ.target_copy().call_number().record().wide_display_entry().author()
368 print_data.patron = {
369 prefix : cusr.prefix(),
370 first_given_name : cusr.first_given_name(),
371 second_given_name : cusr.second_given_name(),
372 family_name : cusr.family_name(),
373 suffix : cusr.suffix(),
374 card : { barcode : cusr.card().barcode() },
375 money_summary : patronSvc.patron_stats.fines,
376 expire_date : cusr.expire_date(),
377 alias : cusr.alias(),
378 has_email : Boolean(patronSvc.current.email() && patronSvc.current.email().match(/.*@.*/).length),
379 has_phone : Boolean(cusr.day_phone() || cusr.evening_phone() || cusr.other_phone())
382 return egCore.print.print({
384 template : 'items_out',
389 function batch_action_with_barcodes(items, action) {
390 if (!items.length) return;
391 var barcodes = items.map(function(circ)
392 { return circ.target_copy().barcode() });
393 action(barcodes).then(reset_page);
395 $scope.mark_lost = function(items) {
396 batch_action_with_barcodes(items, egCirc.mark_lost);
398 $scope.mark_claims_returned = function(items) {
399 batch_action_with_barcodes(items, egCirc.mark_claims_returned_dialog);
401 $scope.mark_claims_never_checked_out = function(items) {
402 batch_action_with_barcodes(items, egCirc.mark_claims_never_checked_out);
405 $scope.show_recent_circs = function(items) {
406 var focus = items.length == 1;
407 angular.forEach(items, function(item) {
408 var url = egCore.env.basePath +
410 item.target_copy().id() +
412 $timeout(function() { var x = $window.open(url, '_blank'); if (focus) x.focus() });
416 $scope.show_triggered_events = function(items) {
417 var focus = items.length == 1;
418 angular.forEach(items, function(item) {
419 var url = egCore.env.basePath +
421 item.target_copy().id() +
423 $timeout(function() { var x = $window.open(url, '_blank'); if (focus) x.focus() });
427 $scope.renew = function(items, msg) {
428 if (!items.length) return;
429 var barcodes = items.map(function(circ)
430 { return circ.target_copy().barcode() });
432 if (!msg) msg = egCore.strings.RENEW_ITEMS;
434 return egConfirmDialog.open(msg, barcodes.join(' '), {}).result
437 var bc = barcodes.pop();
438 if (!bc) { reset_page(); return }
439 // finally -> continue even when one fails
440 egCirc.renew({copy_barcode : bc}).finally(do_one);
446 $scope.renew_all = function() {
447 var circs = patronSvc.items_out.filter(function(circ) {
449 // all others will be rejected at the server
450 !circ.stop_fines() ||
451 circ.stop_fines() == 'MAXFINES'
454 $scope.renew(circs, egCore.strings.RENEW_ALL_ITEMS);
457 $scope.renew_with_date = function(items) {
458 if (!items.length) return;
459 var barcodes = items.map(function(circ)
460 { return circ.target_copy().barcode() });
462 return $uibModal.open({
463 templateUrl : './circ/patron/t_renew_with_date_dialog',
466 '$scope','$uibModalInstance',
467 function($scope , $uibModalInstance) {
472 $scope.cancel = function() {$uibModalInstance.dismiss()}
474 // Fire off the due-date updater for each circ.
475 // When all is done, close the dialog
476 $scope.ok = function() {
477 var due = $scope.args.date.toISOString().replace(/T.*/,'');
478 console.debug("renewing with due date: " + due);
481 if (bc = barcodes.pop()) {
482 egCirc.renew({copy_barcode : bc, due_date : due})
485 $uibModalInstance.close();
489 do_one(); // kick it off
496 $scope.checkin = function(items) {
497 if (!items.length) return;
498 var barcodes = items.map(function(circ)
499 { return circ.target_copy().barcode() });
501 return egConfirmDialog.open(
502 egCore.strings.CHECK_IN_CONFIRM, barcodes.join(' '), {
504 }).result.then(function() {
506 if (bc = barcodes.pop()) {
507 egCirc.checkin({copy_barcode : bc})
513 do_one(); // kick it off
517 $scope.add_billing = function(items) {
518 if (!items.length) return;
519 var circs = items.concat(); // don't pop from grid array
521 var circ; // don't clobber window.circ!
522 if (circ = circs.pop()) {
523 egBilling.showBillDialog({
524 // let the dialog fetch the transaction, since it's
525 // not sufficiently fleshed here.
527 patron : patronSvc.current