2 * List of patron items checked out
5 angular.module('egPatronApp')
7 .controller('PatronItemsOutCtrl',
8 ['$scope','$q','$routeParams','egCore','egUser','patronSvc',
9 'egGridDataProvider','$modal','egCirc','egConfirmDialog','egBilling',
10 function($scope, $q, $routeParams, egCore , egUser, patronSvc ,
11 egGridDataProvider , $modal , egCirc , egConfirmDialog , egBilling) {
12 $scope.initTab('items_out', $routeParams.id);
14 // cache of circ objects for grid display
15 patronSvc.items_out = [];
17 // main list of checked out items
18 $scope.main_list = [];
20 // list of alt circs (lost, etc.) and/or check-in with fines circs
23 // these are fetched during startup (i.e. .configure())
24 // By default, show lost/lo/cr items in the alt list
25 var display_lost = Number(
26 egCore.env.aous['ui.circ.items_out.lost']) || 2;
27 var display_lo = Number(
28 egCore.env.aous['ui.circ.items_out.longoverdue']) || 2;
29 var display_cr = Number(
30 egCore.env.aous['ui.circ.items_out.claimsreturned']) || 2;
32 var fetch_checked_in = true;
33 $scope.show_alt_circs = true;
34 if (display_lost & 4 && display_lo & 4 && display_cr & 4) {
35 // all special types are configured to be hidden once
36 // checked in, so there's no need to fetch checked-in circs.
37 fetch_checked_in = false;
39 if (display_lost & 1 && display_lo & 1 && display_cr & 1) {
40 // additionally, if all types are configured to display
41 // in the main list while checked out, nothing will
42 // ever appear in the alternate list, so we can hide
43 // the alternate list from the UI.
44 $scope.show_alt_circs = false;
48 $scope.items_out_display = 'main';
49 $scope.show_main_list = function() {
50 // don't need a full reset_page() to swap tabs
51 $scope.items_out_display = 'main';
52 patronSvc.items_out = [];
56 $scope.show_alt_list = function() {
57 // don't need a full reset_page() to swap tabs
58 $scope.items_out_display = 'alt';
59 patronSvc.items_out = [];
63 // Reload the user to pick up changes in items out, fines, etc.
64 // Reload circs since the contents of the main vs. alt list may
66 function reset_page() {
67 patronSvc.refreshPrimary();
68 patronSvc.items_out = [];
69 $scope.main_list = [];
74 var provider = egGridDataProvider.instance({});
75 $scope.gridDataProvider = provider;
77 function fetch_circs(id_list, offset, count) {
78 if (!id_list.length) return $q.when();
80 // fetch the lot of circs and stream the results back via notify
81 return egCore.pcrud.search('circ', {id : id_list},
84 circ : ['target_copy', 'workstation', 'checkin_workstation'],
85 acp : ['call_number'],
87 bre : ['simple_record']
89 // avoid fetching the MARC blob by specifying which
90 // fields on the bre to select. More may be needed.
91 // note that fleshed fields are explicitly selected.
92 select : { bre : ['id'] },
95 // we need an order-by to support paging
96 order_by : {circ : ['xact_start']}
98 }).then(null, null, function(circ) {
99 circ.circ_lib(egCore.org.get(circ.circ_lib())); // local fleshing
101 if (circ.target_copy().call_number().id() == -1) {
102 // dummy-up a record for precat items
103 circ.target_copy().call_number().record().simple_record({
104 title : function() {return circ.target_copy().dummy_title()},
105 author : function() {return circ.target_copy().dummy_author()},
106 isbn : function() {return circ.target_copy().dummy_isbn()}
110 patronSvc.items_out.push(circ); // toss it into the cache
115 // decide which list each circ belongs to
116 function promote_circs(list, display_code, open) {
118 if (1 & display_code) { // bitflag 1 == top list
119 $scope.main_list = $scope.main_list.concat(list);
121 $scope.alt_list = $scope.alt_list.concat(list);
124 if (4 & display_code) return; // bitflag 4 == hide on checkin
125 $scope.alt_list = $scope.alt_list.concat(list);
129 // fetch IDs for circs we care about
130 function get_circ_ids() {
131 $scope.main_list = [];
132 $scope.alt_list = [];
134 // we can fetch these in parallel
135 var promise1 = egCore.net.request(
137 'open-ils.actor.user.checked_out.authoritative',
138 egCore.auth.token(), $scope.patron_id
139 ).then(function(outs) {
140 $scope.main_list = outs.out.concat(outs.overdue);
141 promote_circs(outs.lost, display_lost, true);
142 promote_circs(outs.long_overdue, display_lo, true);
143 promote_circs(outs.claims_returned, display_cr, true);
146 // only fetched checked-in-with-bills circs if configured to display
147 var promise2 = !fetch_checked_in ? $q.when() : egCore.net.request(
149 'open-ils.actor.user.checked_in_with_fines.authoritative',
150 egCore.auth.token(), $scope.patron_id
151 ).then(function(outs) {
152 promote_circs(outs.lost, display_lost);
153 promote_circs(outs.long_overdue, display_lo);
154 promote_circs(outs.claims_returned, display_cr);
157 return $q.all([promise1, promise2]);
160 provider.get = function(offset, count) {
162 var id_list = $scope[$scope.items_out_display + '_list'];
164 // see if we have the requested range cached
165 if (patronSvc.items_out[offset]) {
166 return provider.arrayNotifier(
167 patronSvc.items_out, offset, count);
170 // See if we have the circ IDs for this range already loaded.
171 // this would happen navigating to a subsequent page.
172 if (id_list[offset]) {
173 return fetch_circs(id_list, offset, count);
176 // avoid returning the request directly to the caller so the
177 // notify()'s from egCore.net.request don't leak into the
178 // final set of notifies (i.e. the real responses);
180 var deferred = $q.defer();
181 get_circ_ids().then(function() {
183 id_list = $scope[$scope.items_out_display + '_list'];
185 // relay the notified circs back to the grid through our promise
186 fetch_circs(id_list, offset, count).then(
187 deferred.resolve, null, deferred.notify);
190 return deferred.promise;
194 // true if circ is overdue, false otherwise
195 $scope.circIsOverdue = function(circ) {
196 // circ may not exist yet for rendered row
197 if (!circ) return false;
199 var date = new Date();
200 date.setTime(Date.parse(circ.due_date()));
201 return date < new Date();
204 $scope.edit_due_date = function(items) {
205 if (!items.length) return;
208 templateUrl : './circ/patron/t_edit_due_date_dialog',
210 '$scope','$modalInstance',
211 function($scope , $modalInstance) {
213 // if there is only one circ, default to the due date
214 // of that circ. Otherwise, default to today.
215 var due_date = items.length == 1 ?
216 Date.parse(items[0].due_date()) : new Date();
219 num_circs : items.length,
223 // Fire off the due-date updater for each circ.
224 // When all is done, close the dialog
225 $scope.ok = function(args) {
226 // toISOString gives us Zulu time, so
227 // adjust for that before truncating to date
228 var adjust_date = new Date( $scope.args.date );
229 adjust_date.setMinutes(
230 $scope.args.date.getMinutes() - adjust_date.getTimezoneOffset()
232 var due = adjust_date.toISOString().replace(/T.*/,'');
233 console.debug("applying due date of " + due);
236 angular.forEach(items, function(circ) {
240 'open-ils.circ.circulation.due_date.update',
241 egCore.auth.token(), circ.id(), due
243 ).then(function(new_circ) {
244 // update the grid circ with the canonical
245 // date from the modified circulation.
246 circ.due_date(new_circ.due_date());
251 $q.all(promises).then(function() {
252 $modalInstance.close();
256 $scope.cancel = function($event) {
257 $modalInstance.dismiss();
258 $event.preventDefault();
265 $scope.print_receipt = function(items) {
266 if (items.length == 0) return $q.when();
267 var print_data = {circulations : []}
269 angular.forEach(patronSvc.items_out, function(circ) {
270 print_data.circulations.push({
271 circ : egCore.idl.toHash(circ),
272 copy : egCore.idl.toHash(circ.target_copy()),
273 call_number : egCore.idl.toHash(circ.target_copy().call_number()),
274 title : circ.target_copy().call_number().record().simple_record().title(),
275 author : circ.target_copy().call_number().record().simple_record().author(),
279 return egCore.print.print({
281 template : 'items_out',
286 function batch_action_with_barcodes(items, action) {
287 if (!items.length) return;
288 var barcodes = items.map(function(circ)
289 { return circ.target_copy().barcode() });
290 action(barcodes).then(reset_page);
292 $scope.mark_lost = function(items) {
293 batch_action_with_barcodes(items, egCirc.mark_lost);
295 $scope.mark_claims_returned = function(items) {
296 batch_action_with_barcodes(items, egCirc.mark_claims_returned_dialog);
298 $scope.mark_claims_never_checked_out = function(items) {
299 batch_action_with_barcodes(items, egCirc.mark_claims_never_checked_out);
302 $scope.renew = function(items, msg) {
303 if (!items.length) return;
304 var barcodes = items.map(function(circ)
305 { return circ.target_copy().barcode() });
307 if (!msg) msg = egCore.strings.RENEW_ITEMS;
309 return egConfirmDialog.open(msg, barcodes.join(' '), {}).result
312 var bc = barcodes.pop();
313 if (!bc) { reset_page(); return }
314 // finally -> continue even when one fails
315 egCirc.renew({copy_barcode : bc}).finally(do_one);
321 $scope.renew_all = function() {
322 var circs = patronSvc.items_out.filter(function(circ) {
324 // all others will be rejected at the server
325 !circ.stop_fines() ||
326 circ.stop_fines() == 'MAXFINES'
329 $scope.renew(circs, egCore.strings.RENEW_ALL_ITEMS);
332 $scope.renew_with_date = function(items) {
333 if (!items.length) return;
334 var barcodes = items.map(function(circ)
335 { return circ.target_copy().barcode() });
338 templateUrl : './circ/patron/t_edit_due_date_dialog',
339 templateUrl : './circ/patron/t_renew_with_date_dialog',
341 '$scope','$modalInstance',
342 function($scope , $modalInstance) {
347 $scope.cancel = function() {$modalInstance.dismiss()}
349 // Fire off the due-date updater for each circ.
350 // When all is done, close the dialog
351 $scope.ok = function() {
352 var due = $scope.args.date.toISOString().replace(/T.*/,'');
353 console.debug("renewing with due date: " + due);
356 if (bc = barcodes.pop()) {
357 egCirc.renew({copy_barcode : bc, due_date : due})
360 $modalInstance.close();
364 do_one(); // kick it off
371 $scope.checkin = function(items) {
372 if (!items.length) return;
373 var barcodes = items.map(function(circ)
374 { return circ.target_copy().barcode() });
376 return egConfirmDialog.open(
377 egCore.strings.CHECK_IN_CONFIRM, barcodes.join(' '), {
379 }).result.then(function() {
381 if (bc = barcodes.pop()) {
382 egCirc.checkin({copy_barcode : bc})
388 do_one(); // kick it off
392 $scope.add_billing = function(items) {
393 if (!items.length) return;
394 var circs = items.concat(); // don't pop from grid array
396 var circ; // don't clobber window.circ!
397 if (circ = circs.pop()) {
398 egBilling.showBillDialog({
399 // let the dialog fetch the transaction, since it's
400 // not sufficiently fleshed here.
402 patron : patronSvc.current