border-bottom: 1px solid rgba(0,0,0,.125);
}
+.num-receipts {
+ width: 4em;
+}
+
<div class="ml-2"><label for="pay-amount" i18n>Payment Received:</label></div>
<div class="ml-1">
<input type="number" class="form-control" [(ngModel)]="paymentAmount"
- id="pay-amount" [min]="0"/>
+ (ngModelChange)="updatePendingColumn()" id="pay-amount" [min]="0"/>
</div>
<div class="ml-2 form-check form-check-inline">
<input class="form-check-input" type="checkbox"
</eg-grid>
+<div class="row mt-2">
+ <div class="col-lg-12 d-flex">
+ <div class="flex-1"></div>
+ <div class="d-flex flex-colum justify-content-end">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox" id="patron-credit-cbox"
+ [(ngModel)]="convertChangeToCredit"/>
+ <label class="form-check-label" for="patron-credit-cbox" i18n>
+ Convert Change To Patron Credit
+ </label>
+ </div>
+ </div>
+ <div class="d-flex flex-colum justify-content-end">
+ <div class="form-check form-check-inline ml-2">
+ <input class="form-check-input" type="checkbox" id="receipt-on-payment-cbox"
+ [(ngModel)]="receiptOnPayment"/>
+ <label class="form-check-label" for="receipt-on-payment-cbox" i18n>
+ Receipt On Payment
+ </label>
+ </div>
+ </div>
+ <div class="form-inline ml-2">
+ <div class="input-group">
+ <div class="input-group-prepend">
+ <span class="input-group-text" i18n># Receipts</span>
+ </div>
+ <input type="number" class="form-control num-receipts" [(ngModel)]="numReceipts"/>
+ </div>
+ </div>
+ </div>
+</div>
+
+
+
import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
-import {CreditCardDialogComponent, CreditCardPaymentParams
- } from '@eg/staff/share/circ/credit-card-dialog.component';
+import {CreditCardDialogComponent
+ } from '@eg/staff/share/billing/credit-card-dialog.component';
+import {BillingService, CreditCardPaymentParams} from '@eg/staff/share/billing/billing.service';
interface BillGridEntry extends CircDisplayInfo {
xact: IdlObject // mbt
checkNumber: string;
paymentAmount: number;
annotatePayment = false;
- annotation: string;
+ paymentNote: string;
entries: BillGridEntry[];
convertChangeToCredit = false;
receiptOnPayment = false;
+ applyingPayment = false;
+ numReceipts = 1;
ccPaymentParams: CreditCardPaymentParams;
+ disableAutoPrint = false;
maxPayAmount = 100000;
warnPayAmount = 1000;
private auth: AuthService,
private serverStore: ServerStoreService,
private circ: CircService,
+ private billing: BillingService,
public patronService: PatronService,
public context: PatronContextService
) {}
applySettings(): Promise<any> {
return this.serverStore.getItemBatch([
'ui.circ.billing.amount_warn',
- 'ui.circ.billing.amount_limit'
+ 'ui.circ.billing.amount_limit',
+ 'circ.staff_client.do_not_auto_attempt_print'
]).then(sets => {
this.maxPayAmount = sets['ui.circ.billing.amount_limit'] || 100000;
this.warnPayAmount = sets['ui.circ.billing.amount_warn'] || 1000;
+
+ const noPrint = sets['circ.staff_client.do_not_auto_attempt_print'];
+ if (noPrint && noPrint.includes('Bill Pay')) {
+ this.disableAutoPrint = true;
+ }
});
}
ngAfterViewInit() {
+ // Recaclulate the amount owed per selected transaction as the
+ // grid rows selections change.
+ this.billGrid.context.rowSelector.selectionChange
+ .subscribe(_ => this.updatePendingColumn());
+
this.focusPayAmount();
}
});
}
- load() {
+ load(): Promise<any> {
this.summary = null;
this.entries = [];
this.gridDataSource.requestingData = true;
- 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
- ).subscribe(
- resp => {
- if (!this.summary) { // 1st response is summary
- this.summary = resp;
- } else {
- this.entries.push(this.formatForDisplay(resp));
- }
- },
- null,
- () => {
- this.gridDataSource.requestingData = false;
- this.billGrid.reload();
+ ).pipe(tap(resp => {
+ if (!this.summary) { // 1st response is summary
+ this.summary = resp;
+ } else {
+ this.entries.push(this.formatForDisplay(resp));
}
- );
+ })).toPromise()
+ .then(_ => {
+ this.gridDataSource.requestingData = false;
+ this.billGrid.reload();
+ });
}
formatForDisplay(xact: IdlObject): BillGridEntry {
pendingPaymentInfo(): {payment: number, change: number} {
const amt = this.paymentAmount || 0;
+ const owedSelected = this.owedSelected();
- if (amt >= this.paidSelected()) {
- const owedSelected = this.owedSelected();
+ if (amt >= owedSelected) {
return {
- payment : this.owedSelected(),
+ payment : owedSelected,
change : amt - owedSelected
}
}
if (!this.billGrid) { return true; } // still loading
return (
+ this.applyingPayment ||
+ !this.pendingPayment() ||
this.paymentAmount === 0 ||
(this.paymentAmount < 0 && this.paymentType !== 'refund') ||
this.billGrid.context.rowSelector.selected().length === 0
if (this.amountExceedsMax()) { return; }
- this.annotation = '';
+ this.applyingPayment = true;
+ this.paymentNote = '';
+ this.ccPaymentParams = {};
this.verifyPayAmount()
.then(_ => this.annotate())
- .then(_ => this.addCcArgs())
- .catch(err => console.debug('Payment was canceled:', err));
+ .then(_ => this.getCcParams())
+ .then(_ => {
+ return this.billing.applyPayment(
+ this.patronId,
+ this.patron().last_xact_id(),
+ this.paymentType,
+ this.compilePayments(),
+ this.paymentNote,
+ this.checkNumber,
+ this.ccPaymentParams,
+ this.convertChangeToCredit
+ );
+ })
+ .then(paymentIds => this.handlePayReceipt(paymentIds))
+ .then(_ => this.load())
+ .then(_ => this.context.refreshPatron())
+ .catch(msg => console.debug('Payment Canceled:', msg))
+ .finally(() => this.applyingPayment = false);
+ }
+
+ handlePayReceipt(paymentIds: number[]): Promise<any> {
+
+ if (this.disableAutoPrint || !this.receiptOnPayment) {
+ return Promise.resolve();
+ }
+
+ // TODO
+ // return this.printer.pr
+ }
+
+ compilePayments(): Array<Array<number>> {
+ const payments = [];
+ this.entries.forEach(row => {
+ if (row.paymentPending) {
+ payments.push([row.xact.id(), row.paymentPending]);
+ }
+ });
+ return payments;
}
amountExceedsMax(): boolean {
return true;
}
- addCcArgs(): Promise<any> {
- this.ccPaymentParams = {};
-
+ // Credit card info
+ getCcParams(): Promise<any> {
if (this.paymentType !== 'credit_card_payment') {
return Promise.resolve();
}
return this.annotateDialog.open().toPromise()
.then(value => {
if (!value) {
+ // TODO: there is no way in PromptDialog to
+ // differentiate between canceling the dialog and
+ // submitting the dialog with no value. In this case,
+ // if the dialog is submitted with no value, we may want
+ // to leave the dialog open so a value can be applied.
return Promise.reject('No annotation supplied');
}
- this.annotation = value;
+ this.paymentNote = value;
+ });
+ }
+
+ updatePendingColumn() {
+
+ // Reset...
+ this.entries.forEach(row => row.paymentPending = 0);
+
+ var amount = this.pendingPayment();
+ let done = false;
+
+ this.billGrid.context.rowSelector.selected().forEach(index => {
+ if (done) { return; }
+
+ const row = this.billGrid.context.getRowByIndex(index);
+ const owed = Number(row.xact.summary().balance_owed());
+
+ if (amount > owed) {
+ // Pending payment amount exceeds balance of this
+ // row. Pay the entire amount
+ row.paymentPending = owed;
+ amount -= owed;
+
+ } else {
+ // balance owed on the current item matches or exceeds
+ // the pending payment. Apply the full remainder of
+ // the payment to this item... and we're done.
+ //
+ // Limit to two decimal places to avoid floating point
+ // issues and cast back to number to match data type.
+ row.paymentPending = Number(amount.toFixed(2));
+ done = true;
+ }
});
}
}
import {FmRecordEditorModule} from '@eg/share/fm-editor/fm-editor.module';
import {StaffCommonModule} from '@eg/staff/common.module';
import {HoldsModule} from '@eg/staff/share/holds/holds.module';
+import {BillingModule} from '@eg/staff/share/billing/billing.module';
import {CircModule} from '@eg/staff/share/circ/circ.module';
import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module';
import {BookingModule} from '@eg/staff/share/booking/booking.module';
imports: [
StaffCommonModule,
FmRecordEditorModule,
+ BillingModule,
CircModule,
HoldsModule,
HoldingsModule,
<div class="row mb-1"
[ngClass]="{'alert alert-danger p-0': context.patronStats.fines.total_owed > 0}">
<div class="col-lg-5" i18n>Fines Owed</div>
- <div class="col-lg-7">{{context.patronStats.fines.total_owed | currency}}</div>
+ <div class="col-lg-7">{{context.patronStats.fines.balance_owed | currency}}</div>
</div>
<!-- TODO GROUP FINES -->
--- /dev/null
+<eg-string #successMsg text="Successfully Added Billing" i18n-text></eg-string>
+<eg-string #errorMsg text="Failed To Add Billing" i18n-text></eg-string>
+
+<!-- putting this here guarantees it's available to ViewChild before open -->
+<ng-template #bTypes>
+ <eg-combobox #bTypeCbox [entries]="billingTypes"
+ [required]="true" (onChange)="btChanged($event)"></eg-combobox>
+</ng-template>
+
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h4 class="modal-title" i18n>
+ Bill Patron:
+ {{xact.usr().family_name()}},
+ {{xact.usr().first_given_name()}} :
+ {{xact.usr().card().barcode()}}
+ </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">
+
+ <div class="row">
+ <div class="col-lg-2" i18n>Bill #</div>
+ <div class="col-lg-4">{{xact.id()}}</div>
+ <div class="col-lg-4" i18n>Total Billed</div>
+ <div class="col-lg-2">{{xact.summary().total_owed() | currency}}</div>
+ </div>
+ <div class="row">
+ <div class="col-lg-2" i18n>Type</div>
+ <div class="col-lg-4">{{xact.summary().xact_type()}}</div>
+ <div class="col-lg-4" i18n>Total Paid</div>
+ <div class="col-lg-2">{{xact.summary().total_paid() | currency}}</div>
+ </div>
+ <div class="row">
+ <div class="col-lg-2" i18n>Start</div>
+ <div class="col-lg-4">{{xact.xact_start() | date:'short'}}</div>
+ <div class="col-lg-4" i18n>Balance Owed</div>
+ <div class="col-lg-2">{{xact.summary().balance_owed() | currency}}</div>
+ </div>
+ <div class="row">
+ <div class="col-lg-2" i18n>Finish</div>
+ <div class="col-lg-4">{{xact.xact_finish() | date:'short'}}</div>
+ <div class="col-lg-4" i18n>Renewal?</div>
+ <div class="col-lg-2"><eg-bool [value]="isRenewal()"></eg-bool></div>
+ </div>
+
+ <hr/>
+
+ <div class="form-validated">
+ <div class="row mt-2">
+ <div class="col-lg-4" i18n>Location</div>
+ <div class="col-lg-8" i18n>{{hereOrg}}</div>
+ </div>
+ <div class="row mt-2">
+ <div class="col-lg-4" i18n>Billing Type</div>
+ <div class="col-lg-8">
+ <ng-container *ngTemplateOutlet="bTypes"></ng-container>
+ </div>
+ </div>
+ <div class="row mt-2">
+ <div class="col-lg-4" i18n>Amount</div>
+ <div class="col-lg-8" i18n>
+ <input type="number" class="form-control" id="amount-input"
+ required [(ngModel)]="amount" [min]="0"/>
+ </div>
+ </div>
+ <div class="row mt-2">
+ <div class="col-lg-4" i18n>Note</div>
+ <div class="col-lg-8" i18n>
+ <textarea class="form-control" [rows]="3" [(ngModel)]="note"></textarea>
+ </div>
+ </div>
+ </div>
+
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-success" [disabled]="!saveable()"
+ (click)="submit()" i18n>Submit Bill</button>
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Cancel</button>
+ </div>
+</ng-template>
--- /dev/null
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs';
+import {switchMap} from 'rxjs/operators';
+import {IdlObject, IdlService} 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 {OrgService} from '@eg/core/org.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, ComboboxComponent} from '@eg/share/combobox/combobox.component';
+import {BillingService} from './billing.service';
+
+/* Add a billing to a transaction */
+
+const DEFAULT_BILLING_TYPE = 101; // Stock "Misc"
+
+@Component({
+ selector: 'eg-add-billing-dialog',
+ templateUrl: 'billing-dialog.component.html'
+})
+
+export class AddBillingDialogComponent
+ extends DialogComponent implements OnInit {
+
+ @Input() xactId: number;
+
+ xact: IdlObject;
+ billingType: ComboboxEntry;
+ billingTypes: ComboboxEntry[] = [];
+ hereOrg: string;
+ amount: number;
+ note: string;
+
+ @ViewChild('successMsg') private successMsg: StringComponent;
+ @ViewChild('errorMsg') private errorMsg: StringComponent;
+ @ViewChild('bTypeCbox') private bTypeCbox: ComboboxComponent;
+
+ constructor(
+ private modal: NgbModal, // required for passing to parent
+ private toast: ToastService,
+ private net: NetService,
+ private idl: IdlService,
+ private evt: EventService,
+ private pcrud: PcrudService,
+ private billing: BillingService,
+ private org: OrgService,
+ private auth: AuthService) {
+ super(modal);
+ }
+
+ ngOnInit() {
+ this.billing.getUserBillingTypes().then(types => {
+ this.billingTypes = types.map(bt => {
+ return {id: bt.id(), label: bt.name(), fm: bt};
+ });
+ });
+
+ this.hereOrg = this.org.get(this.auth.user().ws_ou()).shortname();
+
+ this.onOpen$.subscribe(_ => {
+ this.amount = null;
+ this.note = '';
+ this.bTypeCbox.selectedId = DEFAULT_BILLING_TYPE;
+ const node = document.getElementById('amount-input');
+ if (node) { node.focus(); }
+ });
+ }
+
+ open(options: NgbModalOptions = {}): Observable<any> {
+
+ // Fetch the xact data before opening the dialog.
+ return this.pcrud.retrieve('mbt', this.xactId, {
+ flesh: 2,
+ flesh_fields: {
+ mbt: ['usr', 'summary', 'circulation'],
+ au: ['card']
+ }
+ }).pipe(switchMap(xact => {
+ this.xact = xact;
+ return super.open(options);
+ }));
+ }
+
+ isRenewal(): boolean {
+ return (
+ this.xact &&
+ this.xact.circulation() &&
+ this.xact.circulation().parent_circ() !== null
+ );
+ }
+
+ btChanged(entry: ComboboxEntry) {
+ this.billingType = entry;
+ if (entry && entry.fm.default_price()) {
+ this.amount = entry.fm.default_price();
+ }
+ }
+
+ saveable(): boolean {
+ return this.billingType && this.amount > 0;
+ }
+
+ submit() {
+ const bill = this.idl.create('mb');
+ bill.xact(this.xactId);
+ bill.amount(this.amount);
+ bill.btype(this.billingType.id);
+ bill.billing_type(this.billingType.label);
+ bill.note(this.note);
+
+ this.net.request(
+ 'open-ils.circ',
+ 'open-ils.circ.money.billing.create',
+ this.auth.token(), bill
+ ).subscribe(billId => {
+
+ const evt = this.evt.parse(billId);
+ if (evt) {
+ console.error(evt);
+ alert(evt);
+ this.close(null);
+ } else {
+ this.close(billId);
+ }
+ });
+ }
+}
+
--- /dev/null
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {BillingService} from './billing.service';
+import {AddBillingDialogComponent} from './billing-dialog.component';
+import {CreditCardDialogComponent} from './credit-card-dialog.component';
+
+@NgModule({
+ declarations: [
+ CreditCardDialogComponent,
+ AddBillingDialogComponent
+ ],
+ imports: [
+ StaffCommonModule
+ ],
+ exports: [
+ AddBillingDialogComponent,
+ CreditCardDialogComponent
+ ],
+ providers: [
+ BillingService
+ ]
+})
+
+export class BillingModule {}
--- /dev/null
+import {Injectable} from '@angular/core';
+import {Observable, empty, from} from 'rxjs';
+import {map, concatMap, mergeMap} from 'rxjs/operators';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {EventService, EgEvent} from '@eg/core/event.service';
+import {AuthService} from '@eg/core/auth.service';
+import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
+import {AudioService} from '@eg/share/util/audio.service';
+
+export interface CreditCardPaymentParams {
+ where_process?: 0 | 1,
+ approval_code?: string,
+ expire_month?: number,
+ expire_year?: number,
+ billing_first?: string,
+ billing_last?: string,
+ billing_address?: string,
+ billing_city?: string,
+ billing_state?: string,
+ billing_zip?: string,
+ note?: string
+}
+
+@Injectable()
+export class BillingService {
+ billingTypes: IdlObject[];
+ userBillingTypes: IdlObject[];
+
+ constructor(
+ private evt: EventService,
+ private org: OrgService,
+ private net: NetService,
+ private pcrud: PcrudService,
+ private auth: AuthService
+ ) {}
+
+ // Returns billing types owned "here", excluding system types
+ getUserBillingTypes(): Promise<IdlObject[]> {
+ if (this.userBillingTypes) {
+ return Promise.resolve(this.userBillingTypes);
+ }
+
+ return this.pcrud.search('cbt',
+ { id: {'>': 100},
+ owner: this.org.fullPath(this.auth.user().ws_ou(), true)
+ },
+ {order_by: {cbt: 'name'}},
+ {atomic: true}
+ ).toPromise().then(types => this.userBillingTypes = types);
+ }
+
+ // Returns billing types owned "here", including system types
+ getBillingTypes(): Promise<IdlObject[]> {
+ if (this.billingTypes) {
+ return Promise.resolve(this.billingTypes);
+ }
+
+ return this.pcrud.search('cbt',
+ {owner: this.org.fullPath(this.auth.user().ws_ou(), true)},
+ {order_by: {cbt: 'name'}},
+ {atomic: true}
+ ).toPromise().then(types => this.billingTypes = types);
+ }
+
+ applyPayment(
+ patronId: number,
+ patronLastXactId: string,
+ paymentType: string,
+ payments: Array<Array<number>>,
+ paymentNote?: string,
+ checkNumber?: string,
+ creditCardParams?: CreditCardPaymentParams,
+ convertChangeToCredit?: boolean): Promise<number[]> {
+
+ return this.net.request(
+ 'open-ils.circ',
+ 'open-ils.circ.money.payment',
+ this.auth.token(), {
+ userid: patronId,
+ note: paymentNote || '',
+ payment_type: paymentType,
+ check_number: checkNumber,
+ payments: payments,
+ patron_credit: convertChangeToCredit,
+ cc_args: creditCardParams
+ }, patronLastXactId).toPromise()
+
+ .then(response => {
+
+ const evt = this.evt.parse(response);
+ if (evt) {
+ console.error(evt);
+ return Promise.reject(evt);
+ }
+
+ // TODO work log
+
+ return response.payments;
+ });
+ }
+}
+
--- /dev/null
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h4 class="modal-title" i18n>Credit Card Information</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" *ngIf="args">
+
+ <div class="card">
+ <div class="card-header" i18n>Credit Card Info</div>
+ <div class="card-body form-validated">
+ <div class="row">
+ <div class="col-lg-4"><label i18n>Process Where</label></div>
+ <div class="col-lg-8">
+ <select class="form-control" [(ngModel)]="args.where_process">
+ <option [value]='1' [disabled]="!supportsExternal" i18n>
+ Process payment through Evergreen
+ </option>
+ <option [value]='0' i18n>
+ Record externally processed payment
+ </option>
+ </select>
+ </div>
+ </div>
+ <ng-container *ngIf="args.where_process == 1">
+ <div class="row mt-2">
+ <div class="col-lg-4"><label i18n>Approval Code</label></div>
+ <div class="col-lg-8">
+ <input type="text" class="form-control"
+ required [(ngModel)]="args.approval_code"/>
+ </div>
+ </div>
+ </ng-container>
+ <ng-container *ngIf="args.where_process == 0">
+ <div class="row mt-2">
+ <div class="col-lg-4"><label i18n>Expire Month</label></div>
+ <div class="col-lg-8">
+ <input type="number" class="form-control" [min]="1"
+ required [(ngModel)]="args.expire_month"/>
+ </div>
+ </div>
+ <div class="row mt-2">
+ <div class="col-lg-4"><label i18n>Expire Year</label></div>
+ <div class="col-lg-8">
+ <input type="number" class="form-control" [min]="thisYear"
+ required [(ngModel)]="args.expire_year"/>
+ </div>
+ </div>
+ </ng-container>
+ </div>
+ </div>
+
+ <div class="card mt-2">
+ <div class="card-header" i18n>Optional Fields</div>
+ <div class="card-body form-validated">
+ <div class="row">
+ <div class="col-lg-4"><label i18n>Billing Name (first)</label></div>
+ <div class="col-lg-8">
+ <input type='text' class="form-control" [(ngModel)]="args.billing_first"/>
+ </div>
+ </div>
+ <div class="row mt-2">
+ <div class="col-lg-4"><label i18n>Billing Name (last)</label></div>
+ <div class="col-lg-8">
+ <input type='text' class="form-control" [(ngModel)]="args.billing_last"/>
+ </div>
+ </div>
+
+ <ng-container *ngIf="args.where_process == 0">
+ <div class="row mt-2">
+ <div class="col-lg-4"><label i18n>Address</label></div>
+ <div class="col-lg-8">
+ <input type='text' class="form-control" [(ngModel)]="args.billing_address"/>
+ </div>
+ </div>
+ <div class="row mt-2">
+ <div class="col-lg-4"><label i18n>City, town or village</label></div>
+ <div class="col-lg-8">
+ <input type='text' class="form-control" [(ngModel)]="args.billing_city"/>
+ </div>
+ </div>
+ <div class="row mt-2">
+ <div class="col-lg-4"><label i18n>State or province</label></div>
+ <div class="col-lg-8">
+ <input type='text' class="form-control" [(ngModel)]="args.billing_state"/>
+ </div>
+ </div>
+ <div class="row mt-2">
+ <div class="col-lg-4"><label i18n>ZIP or postal code</label></div>
+ <div class="col-lg-8">
+ <input type='text' class="form-control" [(ngModel)]="args.billing_zip"/>
+ </div>
+ </div>
+ </ng-container>
+
+ <div class="row mt-2">
+ <div class="col-lg-4"><label i18n>Note</label></div>
+ <div class="col-lg-8">
+ <input type='text' class="form-control" [(ngModel)]="args.note"/>
+ </div>
+ </div>
+
+ </div>
+ </div>
+
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-success" [disabled]="!saveable()"
+ (click)="submit(args)" i18n>Submit</button>
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Cancel</button>
+ </div>
+</ng-template>
--- /dev/null
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs';
+import {switchMap} from 'rxjs/operators';
+import {IdlObject, IdlService} 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 {OrgService} from '@eg/core/org.service';
+import {ServerStoreService} from '@eg/core/server-store.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, ComboboxComponent} from '@eg/share/combobox/combobox.component';
+
+export interface CreditCardPaymentParams {
+ where_process?: 0 | 1,
+ approval_code?: string,
+ expire_month?: number,
+ expire_year?: number,
+ billing_first?: string,
+ billing_last?: string,
+ billing_address?: string,
+ billing_city?: string,
+ billing_state?: string,
+ billing_zip?: string,
+ note?: string
+}
+
+/* Dialog for collecting credit card payment information */
+
+@Component({
+ selector: 'eg-credit-card-dialog',
+ templateUrl: 'credit-card-dialog.component.html'
+})
+
+export class CreditCardDialogComponent
+ extends DialogComponent implements OnInit {
+
+ @Input() patron: IdlObject; // au, fleshed with billing address
+ args: CreditCardPaymentParams;
+ supportsExternal: boolean;
+ thisYear = new Date().getFullYear();
+
+ constructor(
+ private modal: NgbModal,
+ private toast: ToastService,
+ private net: NetService,
+ private idl: IdlService,
+ private evt: EventService,
+ private pcrud: PcrudService,
+ private org: OrgService,
+ private serverStore: ServerStoreService,
+ private auth: AuthService) {
+ super(modal);
+ }
+
+ ngOnInit() {
+
+ this.onOpen$.subscribe(_ => {
+
+ this.args = {
+ billing_first: this.patron.first_given_name(),
+ billing_last: this.patron.family_name(),
+ };
+
+ const addr =
+ this.patron.billing_address() || this.patron.mailing_address();
+
+ if (addr) {
+ this.args.billing_address = addr.street1() +
+ (addr.street2() ? ' ' + addr.street2() : '');
+ this.args.billing_city = addr.city();
+ this.args.billing_state = addr.state();
+ this.args.billing_zip = addr.post_code();
+ }
+
+ this.supportsExternal = false;
+
+ this.serverStore.getItem('credit.processor.default')
+ .then(processor => {
+ if (processor && processor !== 'Stripe') {
+ this.supportsExternal = true;
+ this.args.where_process = 1;
+ }
+ })
+ });
+ }
+
+ saveable(): boolean {
+ if (!this.args) { return false; }
+
+ if (this.args.where_process === 0) {
+ return Boolean(this.args.approval_code);
+ }
+
+ return Boolean(this.args.expire_month) && Boolean(this.args.expire_year);
+ }
+
+
+ submit() {
+ }
+}
+
+++ /dev/null
-<eg-string #successMsg text="Successfully Added Billing" i18n-text></eg-string>
-<eg-string #errorMsg text="Failed To Add Billing" i18n-text></eg-string>
-
-<!-- putting this here guarantees it's available to ViewChild before open -->
-<ng-template #bTypes>
- <eg-combobox #bTypeCbox [entries]="billingTypes"
- [required]="true" (onChange)="btChanged($event)"></eg-combobox>
-</ng-template>
-
-<ng-template #dialogContent>
- <div class="modal-header bg-info">
- <h4 class="modal-title" i18n>
- Bill Patron:
- {{xact.usr().family_name()}},
- {{xact.usr().first_given_name()}} :
- {{xact.usr().card().barcode()}}
- </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">
-
- <div class="row">
- <div class="col-lg-2" i18n>Bill #</div>
- <div class="col-lg-4">{{xact.id()}}</div>
- <div class="col-lg-4" i18n>Total Billed</div>
- <div class="col-lg-2">{{xact.summary().total_owed() | currency}}</div>
- </div>
- <div class="row">
- <div class="col-lg-2" i18n>Type</div>
- <div class="col-lg-4">{{xact.summary().xact_type()}}</div>
- <div class="col-lg-4" i18n>Total Paid</div>
- <div class="col-lg-2">{{xact.summary().total_paid() | currency}}</div>
- </div>
- <div class="row">
- <div class="col-lg-2" i18n>Start</div>
- <div class="col-lg-4">{{xact.xact_start() | date:'short'}}</div>
- <div class="col-lg-4" i18n>Balance Owed</div>
- <div class="col-lg-2">{{xact.summary().balance_owed() | currency}}</div>
- </div>
- <div class="row">
- <div class="col-lg-2" i18n>Finish</div>
- <div class="col-lg-4">{{xact.xact_finish() | date:'short'}}</div>
- <div class="col-lg-4" i18n>Renewal?</div>
- <div class="col-lg-2"><eg-bool [value]="isRenewal()"></eg-bool></div>
- </div>
-
- <hr/>
-
- <div class="form-validated">
- <div class="row mt-2">
- <div class="col-lg-4" i18n>Location</div>
- <div class="col-lg-8" i18n>{{hereOrg}}</div>
- </div>
- <div class="row mt-2">
- <div class="col-lg-4" i18n>Billing Type</div>
- <div class="col-lg-8">
- <ng-container *ngTemplateOutlet="bTypes"></ng-container>
- </div>
- </div>
- <div class="row mt-2">
- <div class="col-lg-4" i18n>Amount</div>
- <div class="col-lg-8" i18n>
- <input type="number" class="form-control" id="amount-input"
- required [(ngModel)]="amount" [min]="0"/>
- </div>
- </div>
- <div class="row mt-2">
- <div class="col-lg-4" i18n>Note</div>
- <div class="col-lg-8" i18n>
- <textarea class="form-control" [rows]="3" [(ngModel)]="note"></textarea>
- </div>
- </div>
- </div>
-
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-success" [disabled]="!saveable()"
- (click)="submit()" i18n>Submit Bill</button>
- <button type="button" class="btn btn-warning"
- (click)="close()" i18n>Cancel</button>
- </div>
-</ng-template>
+++ /dev/null
-import {Component, OnInit, Input, ViewChild} from '@angular/core';
-import {Observable} from 'rxjs';
-import {switchMap} from 'rxjs/operators';
-import {IdlObject, IdlService} 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 {OrgService} from '@eg/core/org.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, ComboboxComponent} from '@eg/share/combobox/combobox.component';
-import {CircService} from './circ.service';
-
-/* Add a billing to a transaction */
-
-const DEFAULT_BILLING_TYPE = 101; // Stock "Misc"
-
-@Component({
- selector: 'eg-add-billing-dialog',
- templateUrl: 'billing-dialog.component.html'
-})
-
-export class AddBillingDialogComponent
- extends DialogComponent implements OnInit {
-
- @Input() xactId: number;
-
- xact: IdlObject;
- billingType: ComboboxEntry;
- billingTypes: ComboboxEntry[] = [];
- hereOrg: string;
- amount: number;
- note: string;
-
- @ViewChild('successMsg') private successMsg: StringComponent;
- @ViewChild('errorMsg') private errorMsg: StringComponent;
- @ViewChild('bTypeCbox') private bTypeCbox: ComboboxComponent;
-
- constructor(
- private modal: NgbModal, // required for passing to parent
- private toast: ToastService,
- private net: NetService,
- private idl: IdlService,
- private evt: EventService,
- private pcrud: PcrudService,
- private circ: CircService,
- private org: OrgService,
- private auth: AuthService) {
- super(modal);
- }
-
- ngOnInit() {
- this.circ.getBillingTypes().then(types => {
- this.billingTypes = types.map(bt => {
- return {id: bt.id(), label: bt.name(), fm: bt};
- });
- });
-
- this.hereOrg = this.org.get(this.auth.user().ws_ou()).shortname();
-
- this.onOpen$.subscribe(_ => {
- this.amount = null;
- this.note = '';
- this.bTypeCbox.selectedId = DEFAULT_BILLING_TYPE;
- const node = document.getElementById('amount-input');
- if (node) { node.focus(); }
- });
- }
-
- open(options: NgbModalOptions = {}): Observable<any> {
-
- // Fetch the xact data before opening the dialog.
- return this.pcrud.retrieve('mbt', this.xactId, {
- flesh: 2,
- flesh_fields: {
- mbt: ['usr', 'summary', 'circulation'],
- au: ['card']
- }
- }).pipe(switchMap(xact => {
- this.xact = xact;
- return super.open(options);
- }));
- }
-
- isRenewal(): boolean {
- return (
- this.xact &&
- this.xact.circulation() &&
- this.xact.circulation().parent_circ() !== null
- );
- }
-
- btChanged(entry: ComboboxEntry) {
- this.billingType = entry;
- if (entry && entry.fm.default_price()) {
- this.amount = entry.fm.default_price();
- }
- }
-
- saveable(): boolean {
- return this.billingType && this.amount > 0;
- }
-
- submit() {
- const bill = this.idl.create('mb');
- bill.xact(this.xactId);
- bill.amount(this.amount);
- bill.btype(this.billingType.id);
- bill.billing_type(this.billingType.label);
- bill.note(this.note);
-
- this.net.request(
- 'open-ils.circ',
- 'open-ils.circ.money.billing.create',
- this.auth.token(), bill
- ).subscribe(billId => {
-
- const evt = this.evt.parse(billId);
- if (evt) {
- console.error(evt);
- alert(evt);
- this.close(null);
- } else {
- this.close(billId);
- }
- });
- }
-}
-
import {NgModule} from '@angular/core';
import {StaffCommonModule} from '@eg/staff/common.module';
import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module';
+import {BillingModule} from '@eg/staff/share/billing/billing.module';
import {CircService} from './circ.service';
import {CircGridComponent} from './grid.component';
import {DueDateDialogComponent} from './due-date-dialog.component';
import {ClaimsReturnedDialogComponent} from './claims-returned-dialog.component';
import {CircComponentsComponent} from './components.component';
import {CircEventsComponent} from './events-dialog.component';
-import {AddBillingDialogComponent} from './billing-dialog.component';
-import {CreditCardDialogComponent} from './credit-card-dialog.component';
@NgModule({
declarations: [
DueDateDialogComponent,
PrecatCheckoutDialogComponent,
ClaimsReturnedDialogComponent,
- CircEventsComponent,
- CreditCardDialogComponent,
- AddBillingDialogComponent
+ CircEventsComponent
],
imports: [
StaffCommonModule,
- HoldingsModule
+ HoldingsModule,
+ BillingModule
],
exports: [
CircGridComponent,
- CircComponentsComponent,
- AddBillingDialogComponent,
- CreditCardDialogComponent
+ CircComponentsComponent
],
providers: [
CircService
components: CircComponentsComponent;
nonCatTypes: IdlObject[] = null;
- billingTypes: IdlObject[] = null;
autoOverrideCheckoutEvents: {[textcode: string]: boolean} = {};
suppressCheckinPopups = false;
ignoreCheckinPrecats = false;
).toPromise().then(types => this.nonCatTypes = types);
}
- getBillingTypes(): Promise<IdlObject[]> {
- if (this.billingTypes) {
- return Promise.resolve(this.billingTypes);
- }
-
- return this.pcrud.search('cbt',
- {
- id: {'>': 100}, // first 100 are reserved
- owner: this.org.fullPath(this.auth.user().ws_ou(), true)
- },
- {order_by: {cbt: 'name'}},
- {atomic: true}
- ).toPromise().then(types => this.billingTypes = types);
- }
-
// Remove internal tracking variables on Param objects so they are
// not sent to the server, which can result in autoload errors.
apiParams(
+++ /dev/null
-<ng-template #dialogContent>
- <div class="modal-header bg-info">
- <h4 class="modal-title" i18n>Credit Card Information</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" *ngIf="args">
-
- <div class="card">
- <div class="card-header" i18n>Credit Card Info</div>
- <div class="card-body form-validated">
- <div class="row">
- <div class="col-lg-4"><label i18n>Process Where</label></div>
- <div class="col-lg-8">
- <select class="form-control" [(ngModel)]="args.where_process">
- <option [value]='1' [disabled]="!supportsExternal" i18n>
- Process payment through Evergreen
- </option>
- <option [value]='0' i18n>
- Record externally processed payment
- </option>
- </select>
- </div>
- </div>
- <ng-container *ngIf="args.where_process == 1">
- <div class="row mt-2">
- <div class="col-lg-4"><label i18n>Approval Code</label></div>
- <div class="col-lg-8">
- <input type="text" class="form-control"
- required [(ngModel)]="args.approval_code"/>
- </div>
- </div>
- </ng-container>
- <ng-container *ngIf="args.where_process == 0">
- <div class="row mt-2">
- <div class="col-lg-4"><label i18n>Expire Month</label></div>
- <div class="col-lg-8">
- <input type="number" class="form-control" [min]="1"
- required [(ngModel)]="args.expire_month"/>
- </div>
- </div>
- <div class="row mt-2">
- <div class="col-lg-4"><label i18n>Expire Year</label></div>
- <div class="col-lg-8">
- <input type="number" class="form-control" [min]="thisYear"
- required [(ngModel)]="args.expire_year"/>
- </div>
- </div>
- </ng-container>
- </div>
- </div>
-
- <div class="card mt-2">
- <div class="card-header" i18n>Optional Fields</div>
- <div class="card-body form-validated">
- <div class="row">
- <div class="col-lg-4"><label i18n>Billing Name (first)</label></div>
- <div class="col-lg-8">
- <input type='text' class="form-control" [(ngModel)]="args.billing_first"/>
- </div>
- </div>
- <div class="row mt-2">
- <div class="col-lg-4"><label i18n>Billing Name (last)</label></div>
- <div class="col-lg-8">
- <input type='text' class="form-control" [(ngModel)]="args.billing_last"/>
- </div>
- </div>
-
- <ng-container *ngIf="args.where_process == 0">
- <div class="row mt-2">
- <div class="col-lg-4"><label i18n>Address</label></div>
- <div class="col-lg-8">
- <input type='text' class="form-control" [(ngModel)]="args.billing_address"/>
- </div>
- </div>
- <div class="row mt-2">
- <div class="col-lg-4"><label i18n>City, town or village</label></div>
- <div class="col-lg-8">
- <input type='text' class="form-control" [(ngModel)]="args.billing_city"/>
- </div>
- </div>
- <div class="row mt-2">
- <div class="col-lg-4"><label i18n>State or province</label></div>
- <div class="col-lg-8">
- <input type='text' class="form-control" [(ngModel)]="args.billing_state"/>
- </div>
- </div>
- <div class="row mt-2">
- <div class="col-lg-4"><label i18n>ZIP or postal code</label></div>
- <div class="col-lg-8">
- <input type='text' class="form-control" [(ngModel)]="args.billing_zip"/>
- </div>
- </div>
- </ng-container>
-
- <div class="row mt-2">
- <div class="col-lg-4"><label i18n>Note</label></div>
- <div class="col-lg-8">
- <input type='text' class="form-control" [(ngModel)]="args.note"/>
- </div>
- </div>
-
- </div>
- </div>
-
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-success" [disabled]="!saveable()"
- (click)="submit(args)" i18n>Submit</button>
- <button type="button" class="btn btn-warning"
- (click)="close()" i18n>Cancel</button>
- </div>
-</ng-template>
+++ /dev/null
-import {Component, OnInit, Input, ViewChild} from '@angular/core';
-import {Observable} from 'rxjs';
-import {switchMap} from 'rxjs/operators';
-import {IdlObject, IdlService} 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 {OrgService} from '@eg/core/org.service';
-import {ServerStoreService} from '@eg/core/server-store.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, ComboboxComponent} from '@eg/share/combobox/combobox.component';
-import {CircService} from './circ.service';
-
-export interface CreditCardPaymentParams {
- where_process?: 0 | 1,
- approval_code?: string,
- expire_month?: number,
- expire_year?: number,
- billing_first?: string,
- billing_last?: string,
- billing_address?: string,
- billing_city?: string,
- billing_state?: string,
- billing_zip?: string,
- note?: string
-}
-
-/* Dialog for collecting credit card payment information */
-
-@Component({
- selector: 'eg-credit-card-dialog',
- templateUrl: 'credit-card-dialog.component.html'
-})
-
-export class CreditCardDialogComponent
- extends DialogComponent implements OnInit {
-
- @Input() patron: IdlObject; // au, fleshed with billing address
- args: CreditCardPaymentParams;
- supportsExternal: boolean;
- thisYear = new Date().getFullYear();
-
- constructor(
- private modal: NgbModal,
- private toast: ToastService,
- private net: NetService,
- private idl: IdlService,
- private evt: EventService,
- private pcrud: PcrudService,
- private circ: CircService,
- private org: OrgService,
- private serverStore: ServerStoreService,
- private auth: AuthService) {
- super(modal);
- }
-
- ngOnInit() {
-
- this.onOpen$.subscribe(_ => {
-
- this.args = {
- billing_first: this.patron.first_given_name(),
- billing_last: this.patron.family_name(),
- };
-
- const addr =
- this.patron.billing_address() || this.patron.mailing_address();
-
- if (addr) {
- this.args.billing_address = addr.street1() +
- (addr.street2() ? ' ' + addr.street2() : '');
- this.args.billing_city = addr.city();
- this.args.billing_state = addr.state();
- this.args.billing_zip = addr.post_code();
- }
-
- this.supportsExternal = false;
-
- this.serverStore.getItem('credit.processor.default')
- .then(processor => {
- if (processor && processor !== 'Stripe') {
- this.supportsExternal = true;
- this.args.where_process = 1;
- }
- })
- });
- }
-
- saveable(): boolean {
- if (!this.args) { return false; }
-
- if (this.args.where_process === 0) {
- return Boolean(this.args.approval_code);
- }
-
- return Boolean(this.args.expire_month) && Boolean(this.args.expire_year);
- }
-
-
- submit() {
- }
-}
-
} from '@eg/staff/share/holdings/mark-missing-dialog.component';
import {ClaimsReturnedDialogComponent} from './claims-returned-dialog.component';
import {ToastService} from '@eg/share/toast/toast.service';
-import {AddBillingDialogComponent} from './billing-dialog.component';
+import {AddBillingDialogComponent} from '@eg/staff/share/billing/billing-dialog.component';
export interface CircGridEntry extends CircDisplayInfo {
index: string; // class + id -- row index
import {NgModule} from '@angular/core';
import {StaffCommonModule} from '@eg/staff/common.module';
+import {BillingModule} from '@eg/staff/share/billing/billing.module';
import {HoldingsService} from './holdings.service';
import {MarkDamagedDialogComponent} from './mark-damaged-dialog.component';
import {MarkMissingDialogComponent} from './mark-missing-dialog.component';
BatchItemAttrComponent
],
imports: [
- StaffCommonModule
+ StaffCommonModule,
+ BillingModule
],
exports: [
MarkDamagedDialogComponent,
import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {BillingService} from '@eg/staff/share/billing/billing.service';
/**
* Dialog for marking items damaged and asessing related bills.
private evt: EventService,
private pcrud: PcrudService,
private org: OrgService,
+ private billing: BillingService,
private bib: BibRecordService,
private auth: AuthService) {
super(modal); // required for subclassing
- this.billingTypes = [];
}
/**
// Fetch-cache billing types
getBillingTypes(): Promise<any> {
- if (this.billingTypes.length > 1) {
- return Promise.resolve();
- }
- return this.pcrud.search('cbt',
- {owner: this.org.fullPath(this.auth.user().ws_ou(), true)},
- {}, {atomic: true}
- ).toPromise().then(bts => {
- this.billingTypes = bts
- .sort((a, b) => a.name() < b.name() ? -1 : 1)
- .map(bt => ({id: bt.id(), label: bt.name()}));
+ return this.billing.getUserBillingTypes().then(types => {
+ this.billingTypes =
+ types.map(bt => ({id: bt.id(), label: bt.name()}));
});
}