paymentPending?: number;
}
-const XACT_FLESH_DEPTH = 5;
-const XACT_FLESH_FIELDS = {
- mbt: ['summary', 'circulation', 'grocery'],
- circ: ['target_copy', 'workstation', 'checkin_workstation', 'circ_lib'],
- acp: [
- 'call_number',
- 'holds_count',
- 'status',
- 'circ_lib',
- 'location',
- 'floating',
- 'age_protect',
- 'parts'
- ],
- acpm: ['part'],
- acn: ['record', 'owning_lib', 'prefix', 'suffix'],
- bre: ['wide_display_entry']
-};
-
-
@Component({
templateUrl: 'bills.component.html',
selector: 'eg-patron-bills',
});
}
- load(): Promise<any> {
+ // In refresh mode, only fetch the requested xacts, with updated user
+ // summary, and slot them back into the entries array.
+ load(refreshXacts?: number[]): Promise<any> {
+ let entries = [];
this.summary = null;
- this.entries = [];
this.gridDataSource.requestingData = true;
- return this.net.request('open-ils.actor',
+ return this.net.request(
+ 'open-ils.actor',
'open-ils.actor.user.transactions.for_billing',
- this.auth.token(), this.patronId
+ this.auth.token(), this.patronId, refreshXacts
+
).pipe(tap(resp => {
+
if (!this.summary) { // 1st response is summary
this.summary = resp;
+ return;
+ }
+
+ if (refreshXacts) {
+
+ // Slot the updated xact back into place
+ entries.push(this.formatForDisplay(resp));
+ entries = entries.map(e => {
+ if (e.xact.id() === resp.id()) {
+ return this.formatForDisplay(resp);
+ }
+ return e;
+ });
+
} else {
- this.entries.push(this.formatForDisplay(resp));
+ entries.push(this.formatForDisplay(resp));
}
})).toPromise()
+
.then(_ => {
this.gridDataSource.requestingData = false;
+ this.entries = entries;
this.billGrid.reload();
});
}
this.applyingPayment = true;
this.paymentNote = '';
this.ccPaymentParams = {};
+ const payments = this.compilePayments();
this.verifyPayAmount()
.then(_ => this.annotate())
this.patronId,
this.patron().last_xact_id(),
this.paymentType,
- this.compilePayments(),
+ payments,
this.paymentNote,
this.checkNumber,
this.ccPaymentParams,
this.convertChangeToCredit
);
})
- .then(paymentIds => this.handlePayReceipt(paymentIds))
- .then(_ => this.load())
+ .then(paymentIds => this.handlePayReceipt(payments, paymentIds))
+ .then(_ => this.load(payments.map(p => p[0]))) // load xact IDs
.then(_ => this.context.refreshPatron())
.catch(msg => console.debug('Payment Canceled:', msg))
.finally(() => this.applyingPayment = false);
}
- handlePayReceipt(paymentIds: number[]): Promise<any> {
+ handlePayReceipt(payments: Array<Array<number>>, paymentIds: number[]): Promise<any> {
if (this.disableAutoPrint || !this.receiptOnPayment) {
return Promise.resolve();
return this.barcodeSelect.getBarcode('asset', this.checkoutBarcode)
.then(selection => {
if (selection) {
+ params.copy_id = selection.id;
params.copy_barcode = selection.barcode;
return params;
} else {
import {ClaimsReturnedDialogComponent} from './claims-returned-dialog.component';
import {CircComponentsComponent} from './components.component';
import {CircEventsComponent} from './events-dialog.component';
+import {OpenCircDialogComponent} from './open-circ-dialog.component';
@NgModule({
declarations: [
DueDateDialogComponent,
PrecatCheckoutDialogComponent,
ClaimsReturnedDialogComponent,
- CircEventsComponent
+ CircEventsComponent,
+ OpenCircDialogComponent
],
imports: [
StaffCommonModule,
dummy_author?: string;
dummy_isbn?: string;
circ_modifier?: string;
+ void_overdues?: boolean;
// internal tracking
_override?: boolean;
copy_id?: number;
copy_barcode?: string;
claims_never_checked_out?: boolean;
+ void_overdues?: boolean;
// internal tracking
_override?: boolean;
processCheckoutResult(
params: CheckoutParams, response: any): Promise<CheckoutResult> {
- console.debug('checkout resturned', response);
const allEvents = Array.isArray(response) ?
response.map(r => this.evt.parse(r)) :
[this.evt.parse(response)];
+ console.debug('checkout returned', allEvents.map(e => e.textcode));
+
const firstEvent = allEvents[0];
const payload = firstEvent.payload;
case 'ITEM_NOT_CATALOGED':
return this.handlePrecat(result);
+
+ case 'OPEN_CIRCULATION_EXISTS':
+ return this.handleOpenCirc(result);
}
return Promise.resolve(result);
}
+
+ // Ask the user if we should resolve the circulation and check
+ // out to the user or leave it alone.
+ // When resolving and checking out, renew if it's for the same
+ // user, otherwise check it in, then back out to the current user.
+ handleOpenCirc(result: CheckoutResult): Promise<CheckoutResult> {
+
+ let sameUser = false;
+
+ return this.net.request(
+ 'open-ils.circ',
+ 'open-ils.circ.copy_checkout_history.retrieve',
+ this.auth.token(), result.params.copy_id, 1).toPromise()
+
+ .then(circs => {
+ const circ = circs[0];
+
+ sameUser = result.params.patron_id === circ.usr();
+ this.components.openCircDialog.sameUser = sameUser;
+ this.components.openCircDialog.circDate = circ.xact_start();
+
+ return this.components.openCircDialog.open().toPromise();
+ })
+
+ .then(fromDialog => {
+
+ // Leave the open circ checked out.
+ if (!fromDialog) { return result; }
+
+ const coParams = Object.assign({}, result.params); // clone
+
+ if (sameUser) {
+ coParams.void_overdues = fromDialog.forgiveFines;
+ return this.renew(coParams);
+ }
+
+ const ciParams: CheckinParams = {
+ noop: true,
+ copy_id: coParams.copy_id,
+ void_overdues: fromDialog.forgiveFines
+ };
+
+ return this.checkin(ciParams)
+ .then(res => {
+ if (res.success) {
+ return this.checkout(coParams);
+ } else {
+ return Promise.reject('Unable to check in item');
+ }
+ });
+ });
+ }
+
handleOverridableCheckoutEvents(
result: CheckoutResult, events: EgEvent[]): Promise<CheckoutResult> {
const params = result.params;
processCheckinResult(
params: CheckinParams, response: any): Promise<CheckinResult> {
- console.debug('checkout resturned', response);
-
const allEvents = Array.isArray(response) ?
response.map(r => this.evt.parse(r)) : [this.evt.parse(response)];
+ console.debug('checkin returned', allEvents.map(e => e.textcode));
+
const firstEvent = allEvents[0];
const payload = firstEvent.payload;
<eg-precat-checkout-dialog #precatDialog></eg-precat-checkout-dialog>
+
<eg-circ-events-dialog #circEventsDialog></eg-circ-events-dialog>
+
<eg-alert-dialog #routeToCatalogingDialog
i18n-dialogTitle dialogTitle="Route To Cataloging"
i18n-dialogBody dialogBody="This item needs to be routed to CATALOGING">
</eg-alert-dialog>
+<eg-open-circ-dialog #openCircDialog></eg-open-circ-dialog>
+
+
+
import {CircEventsComponent} from './events-dialog.component';
import {StringComponent} from '@eg/share/string/string.component';
import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {OpenCircDialogComponent} from './open-circ-dialog.component';
/* Container component for sub-components used by circulation actions.
*
@ViewChild('precatDialog') precatDialog: PrecatCheckoutDialogComponent;
@ViewChild('circEventsDialog') circEventsDialog: CircEventsComponent;
@ViewChild('routeToCatalogingDialog') routeToCatalogingDialog: AlertDialogComponent;
+ @ViewChild('openCircDialog') openCircDialog: OpenCircDialogComponent;
constructor(private circ: CircService) {
this.circ.components = this;
--- /dev/null
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h4 class="modal-title">
+ <span i18n>Open Circulation</span>
+ </h4>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="close()">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <h5 class="font-weight-bold" i18n>
+ There is an open circulation on the requested item.
+ </h5>
+
+ <div class="mt-2" *ngIf="sameUser">
+ This item was already checked out to this user on {{circDate | date:'short'}}
+ </div>
+ <div class="mt-2" *ngIf="!sameUser">
+ This item was checked out by another patron on {{circDate | date:'short'}}
+ </div>
+ </div>
+ <div class="modal-footer d-flex">
+
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox" id="forgive-fines-cbox"
+ [(ngModel)]="forgiveFines"/>
+ <label class="form-check-label" for="forgive-fines-cbox" i18n>Forgive Fines?</label>
+ </div>
+
+ <div class="flex-1"></div>
+
+ <div>
+ <button type="button" class="btn btn-success" *ngIf="!sameUser"
+ (click)="close({forgiveFines: forgiveFines})" i18n>
+ Normal Checkin Then Checkout</button>
+
+ <button type="button" class="btn btn-success" *ngIf="sameUser"
+ (click)="close({forgiveFines: forgiveFines})" i18n>Renew</button>
+
+ <button type="button" class="btn btn-warning ml-2"
+ (click)="close()" i18n>Cancel</button>
+ </div>
+
+ </div>
+</ng-template>
--- /dev/null
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+/* Dialog for alerting of an existing open circulation */
+
+@Component({
+ selector: 'eg-open-circ-dialog',
+ templateUrl: 'open-circ-dialog.component.html'
+})
+
+export class OpenCircDialogComponent
+ extends DialogComponent implements OnInit {
+
+ @Input() sameUser: boolean;
+ @Input() circDate: string; // iso
+ forgiveFines = false;
+
+ constructor(
+ private modal: NgbModal, // required for passing to parent
+ private toast: ToastService,
+ private net: NetService,
+ private evt: EventService,
+ private pcrud: PcrudService,
+ private auth: AuthService) {
+ super(modal); // required for subclassing
+ }
+
+ ngOnInit() {}
+}
display in the user bills UI. API is natively "authoritative"./,
params => [
{desc => 'Authentication token', type => 'string'},
- {desc => 'User ID', type => 'number'}
+ {desc => 'User ID', type => 'number'},
+ {desc => 'Xact IDs. Optionally limit to specific transactions',
+ type => 'array'}
],
return => {
desc => q/First response is the user money summary, following
);
sub user_billing_xacts {
- my ($self, $client, $auth, $user_id) = @_;
+ my ($self, $client, $auth, $user_id, $xact_ids) = @_;
my $e = new_editor(authtoken => $auth, xact => 1);
return $e->die_event unless $e->checkauth;
# Start with the user summary.
$client->respond($e->retrieve_money_user_summary($user_id));
- my $xact_ids = $e->json_query({
+ # Even if xact_ids are specified, run this query to confirm the
+ # provided IDs are linked to the specified user and have a balance.
+ $xact_ids = $e->json_query({
select => {mbts => ['id']},
from => 'mbts',
where => {
usr => $user_id,
- balance_owed => {'<>' => 0}
+ balance_owed => {'<>' => 0},
+ $xact_ids ? (id => $xact_ids) : ()
},
order_by => {mbts => {xact_start => 'asc'}}
});