width: 8em;
}
+.striped {
+ background-color: rgba(0,0,0,.03);
+ border-top: 1px solid rgba(0,0,0,.125);
+ border-bottom: 1px solid rgba(0,0,0,.125);
+}
+
+<!-- SUMMARY -->
+
<ng-container *ngIf="summary">
- <div class="row border-bottom pb-2 pt-2 mb-4 border border-secondary rounded">
+ <div class="row border-bottom border-secondary pb-2 pt-2 mb-4">
<div class="col-lg-3 pr-0 mr-0 border-right">
- <div class="d-flex">
+ <div class="d-flex pt-1 pb-1 striped">
<div class="flex-4" i18n>Total Owed:</div>
- <div class="flex-1">{{(summary.total_paid() || 0) | currency}}</div>
+ <div class="flex-1"
+ [ngClass]="{'font-weight-bold' : summary.balance_owed() > 0}">
+ {{summary.balance_owed() | currency}}</div>
</div>
- <div class="d-flex">
+ <div class="d-flex pt-1 pb-1">
<div class="flex-4" i18n>Total Billed:</div>
- <div class="flex-1">{{(summary.total_owed() || 0) | currency}}</div>
+ <div class="flex-1">{{summary.total_owed() | currency}}</div>
</div>
- <div class="d-flex">
+ <div class="d-flex pt-1 pb-1 striped">
<div class="flex-4" i18n>Total Paid/Credited:</div>
- <div class="flex-1">{{(summary.total_paid() || 0) | currency}}</div>
+ <div class="flex-1">{{summary.total_paid() | currency}}</div>
</div>
</div>
<div class="col-lg-3 pr-0 mr-0 border-right">
- <div class="d-flex">
+ <div class="d-flex pt-1 pb-1 striped">
<div class="flex-4" i18n>Owed for Selected:</div>
<div class="flex-1">{{owedSelected() | currency}}</div>
</div>
- <div class="d-flex">
+ <div class="d-flex pt-1 pb-1">
<div class="flex-4" i18n>Billed for Selected:</div>
<div class="flex-1">{{billedSelected() | currency}}</div>
</div>
- <div class="d-flex">
+ <div class="d-flex pt-1 pb-1 striped">
<div class="flex-4" i18n>Paid/Credited for Selected:</div>
<div class="flex-1">{{paidSelected() | currency}}</div>
</div>
</div>
<div class="col-lg-3 pr-0 mr-0 border-right">
- <div class="d-flex">
+ <div class="d-flex pt-1 pb-1 striped">
<div class="flex-4" i18n>Refunds Available:</div>
<div class="flex-1">{{refundsAvailable() | currency}}</div>
</div>
- <div class="d-flex">
+ <div class="d-flex pt-1 pb-1">
<div class="flex-4" i18n>Credit Available:</div>
<div class="flex-1">{{patron().credit_forward_balance() | currency}}</div>
</div>
- <div class="d-flex">
+ <div class="d-flex pt-1 pb-1 striped">
<div class="flex-4" i18n>Session Voided:</div>
<div class="flex-1">{{sessionVoided | currency}}</div>
</div>
</div>
<div class="col-lg-3 pr-0 mr-0">
- <div class="d-flex">
+ <div class="d-flex pt-1 pb-1 striped">
<div class="flex-4"> </div>
<div class="flex-1"> </div>
</div>
- <div class="d-flex">
+ <div class="d-flex pt-1 pb-1">
<div class="flex-4" i18n>Pending Payment:</div>
<div class="flex-1 font-weight-bold">{{pendingPayment() | currency}}</div>
</div>
- <div class="d-flex">
+ <div class="d-flex pt-1 pb-1 striped">
<div class="flex-4" i18n>Pending Change:</div>
<div class="flex-1 font-weight-bold">{{pendingChange() | currency}}</div>
</div>
</div>
</ng-container>
-<div class="row bg-light border border-dark rounded pt-2 pb-2 mt-2 mb-2 payment-form">
+<!-- BILL PAY FORM -->
+
+<h3 i18n>Pay Bill</h3>
+
+<div class="row bg-light border border-dark rounded pt-2 pb-2 mt-2 mb-4 payment-form">
<div class="col-lg-12 d-flex form-inline form-validated">
- <div class="font-weight-bold" i18n>Pay Bill:</div>
- <div class="ml-4"><label for="payment-type" i18n>Payment Type:</label></div>
- <div class="ml-2">
- <select [(ngModel)]="paymentType" class="form-control ml-2" id="payment-type">
+ <div class="flex-1"></div>
+ <div class="ml-2"><label for="payment-type" i18n>Payment Type:</label></div>
+ <div class="ml-1">
+ <select [(ngModel)]="paymentType" class="form-control" id="payment-type">
<option value="cash_payment" i18n>Cash</option>
<option value="check_payment" i18n>Check</option>
<option value="credit_card_payment" i18n>Credit Card</option>
<option value="goods_payment" i18n>Goods</option>
</select>
</div>
- <div class="ml-4"><label for="check-number" i18n>Check Number:</label></div>
- <div class="ml-2">
+ <div class="ml-2"><label for="check-number" i18n>Check Number:</label></div>
+ <div class="ml-1">
<input type="text" class="form-control" [(ngModel)]="checkNumber"
id="check-number" [disabled]="paymentType !== 'check_payment'"/>
</div>
- <div class="ml-4"><label for="pay-amount" i18n>Payment Received:</label></div>
- <div class="ml-2">
+ <div class="ml-2"><label for="pay-amount" i18n>Payment Received:</label></div>
+ <div class="ml-1">
<input type="number" class="form-control" [(ngModel)]="payAmount"
id="pay-amount" [min]="0"/>
</div>
- <div class="ml-4 form-check form-check-inline">
+ <div class="ml-2 form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="annotate" [(ngModel)]="annotatePayment"/>
<label class="form-check-label" for="annotate" i18n>Annotate</label>
</div>
- <div class="ml-4">
+ <div class="ml-2">
<button class="btn btn-outline-dark" (click)="applyPayment()" i18n>Apply Payment</button>
</div>
</div>
</div>
+<!-- BILLS GRID -->
+
+<ng-template #titleTemplate let-r="row">
+ <ng-container *ngIf="r.record">
+ <a routerLink="/staff/catalog/record/{{r.record.id()}}">{{r.title}}</a>
+ </ng-container>
+ <ng-container *ngIf="!r.record">{{r.title}}</ng-container>
+</ng-template>
+
+<ng-template #barcodeTemplate let-r="row">
+ <ng-container *ngIf="r.copy">
+ <a href="/eg/staff/cat/item/{{r.copy.id()}}">{{r.copy.barcode()}}</a>
+ </ng-container>
+</ng-template>
+
+<ng-template #callNumberTemplate let-r="row">
+ <ng-container *ngIf="r.volume">
+ {{r.volume.prefix().label()}} {{r.volume.label()}} {{r.volume.suffix().label()}}
+ </ng-container>
+ <ng-container *ngIf="!r.volume" i18n>UNCATALOGED</ng-container>
+</ng-template>
+
+<eg-grid #billGrid [dataSource]="gridDataSource"
+ [sortable]="true" [useLocalSort]="true"
+ [cellTextGenerator]="cellTextGenerator">
+
+ <eg-grid-column path="xact.id" [index]="true" label="Bill #" i18n-label>
+ </eg-grid-column>
+
+ <eg-grid-column path="xact.xact_start" datatype="timestamp" [datePlusTime]="true"
+ label="Start" i18n-label></eg-grid-column>
+
+ <eg-grid-column path="xact.summary.xact_type"
+ label="Type" i18n-label></eg-grid-column>
+
+ <eg-grid-column path="xact.summary.last_billing_type"
+ label="Last Billing Type" i18n-label></eg-grid-column>
+
+ <eg-grid-column path="billingLocation"
+ label="Billing Location" i18n-label></eg-grid-column>
+
+ <eg-grid-column path="call_number" label="Call Number" i18n-label
+ [cellTemplate]="callNumberTemplate"></eg-grid-column>
+
+ <eg-grid-column name="copy_barcode" label="Item Barcode" i18n-label
+ [cellTemplate]="barcodeTemplate"></eg-grid-column>
+
+ <eg-grid-column path="copy.location.name" label="Shelving Location"
+ i18n-label></eg-grid-column>
+
+ <eg-grid-column name="title" label="Title" i18n-label
+ [cellTemplate]="titleTemplate"></eg-grid-column>
+
+ <eg-grid-column path="xact.summary.balance_owed" datatype="money"
+ label="Balance Owed" i18n-label></eg-grid-column>
+
+ <eg-grid-column path="xact.summary.total_owed" datatype="money"
+ label="Total Billed" i18n-label></eg-grid-column>
+
+ <eg-grid-column path="xact.summary.total_paid" datatype="money"
+ label="Total Paid" i18n-label></eg-grid-column>
+
+ <eg-grid-column name="paymentPending" datatype="money"
+ label="Payment Pending" i18n-label></eg-grid-column>
+
+ <eg-grid-column name="author" [hidden]="true"
+ label="Author" i18n-label></eg-grid-column>
+ <eg-grid-column path="usr" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path="unrecovered" [hidden]="true"></eg-grid-column>
+
+</eg-grid>
-import {Component, Input, OnInit, AfterViewInit} from '@angular/core';
+import {Component, Input, OnInit, AfterViewInit, ViewChild} from '@angular/core';
import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {from, empty} from 'rxjs';
+import {concatMap, tap} from 'rxjs/operators';
import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
import {IdlObject} from '@eg/core/idl.service';
import {NetService} from '@eg/core/net.service';
import {ServerStoreService} from '@eg/core/server-store.service';
import {PatronService} from '@eg/staff/share/patron/patron.service';
import {PatronContextService} from './patron.service';
+import {GridDataSource, GridColumn, GridCellTextGenerator} from '@eg/share/grid/grid';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {Pager} from '@eg/share/util/pager';
+import {CircService, CircDisplayInfo} from '@eg/staff/share/circ/circ.service';
+
+interface BillGridEntry extends CircDisplayInfo {
+ xact: IdlObject // mbt
+ billingLocation?: string;
+ 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',
paymentType = 'cash_payment';
checkNumber: string;
annotatePayment = false;
+ entries: BillGridEntry[];
+
+ gridDataSource: GridDataSource = new GridDataSource();
+ cellTextGenerator: GridCellTextGenerator;
+
+ @ViewChild('billGrid') private billGrid: GridComponent;
constructor(
private router: Router,
private pcrud: PcrudService,
private auth: AuthService,
private store: ServerStoreService,
+ private circ: CircService,
public patronService: PatronService,
public context: PatronContextService
) {}
ngOnInit() {
+
+ this.cellTextGenerator = {
+ title: row => row.title,
+ copy_barcode: row => row.copy ? row.copy.barcode() : '',
+ call_number: row => row.volume ? row.volume.label() : ''
+ };
+
+ // The grid never fetches data directly, it only serves what
+ // we have manually retrieved.
+ this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
+ if (!this.entries) { return empty(); }
+
+ const page =
+ this.entries.slice(pager.offset, pager.offset + pager.limit)
+ .filter(entry => entry !== undefined);
+
+ return from(page);
+ };
+
this.load();
}
load() {
+ const xactIds = [];
+
+
+ // TODO: run this in a single pcrud transaction
+
this.pcrud.retrieve('mous', this.patronId, {}, {authoritative : true})
- .subscribe(sum => this.summary = sum);
+ .pipe(tap(sum => this.summary = sum))
+ .pipe(concatMap(_ => {
+ return this.pcrud.search('mbts',
+ {usr: this.patronId, balance_owed: {'<>' : 0}},
+ {select: {mbts: ['id']}}, {authoritative : true}
+ ).pipe(tap(summary => xactIds.push(summary.id())));
+ }))
+ .pipe(concatMap(_ => {
+ this.entries = [];
+ return this.pcrud.search('mbt', {id: xactIds}, {
+ flesh: XACT_FLESH_DEPTH,
+ flesh_fields: XACT_FLESH_FIELDS,
+ order_by: {mbts : ['xact_start']},
+ select: {bre : ['id']}
+ }, {authoritative : true}
+ ).pipe(tap(xact => this.entries.push(this.formatForDisplay(xact))));
+ }))
+ .subscribe(null, null, () => this.billGrid.reload());
+ }
+
+ formatForDisplay(xact: IdlObject): BillGridEntry {
+
+ const entry: BillGridEntry = {
+ xact: xact,
+ paymentPending: 0
+ };
+
+ if (xact.summary().xact_type() !== 'circulation') {
+ entry.title = xact.summary().last_billing_type();
+ entry.billingLocation =
+ xact.grocery().billing_location().shortname();
+ return entry;
+ }
+
+ const circDisplay: CircDisplayInfo =
+ this.circ.getDisplayInfo(xact.circulation());
+
+ entry.billingLocation =
+ xact.circulation().circ_lib().shortname();
+
+ return Object.assign(entry, circDisplay);
}
patron(): IdlObject {
const entry: CircGridEntry = {
index: `ancc-${circ.id()}`,
title: circ.item_type().name(),
- dueDate: circ.duedate()
+ dueDate: circ.duedate(),
+ copy: null,
+ author: '',
+ isbn: ''
};
this.nonCatGrid.appendGridEntry(entry);
import {CircEventsComponent} from './events-dialog.component';
import {CircComponentsComponent} from './components.component';
+export interface CircDisplayInfo {
+ title?: string;
+ author?: string;
+ isbn?: string;
+ copy?: IdlObject; // acp
+ volume?: IdlObject; // acn
+ record?: IdlObject; // bre
+ display?: IdlObject; // mwde
+}
const CAN_OVERRIDE_CHECKOUT_EVENTS = [
'PATRON_EXCEEDS_OVERDUE_COUNT',
private bib: BibRecordService,
) {}
+ // 'circ' is fleshed with copy, vol, bib, wide_display_entry
+ // Extracts some display info from a fleshed circ.
+ getDisplayInfo(circ: IdlObject): CircDisplayInfo {
+
+ const copy = circ.target_copy();
+
+ if (copy.call_number().id() === -1) { // precat
+ return {
+ title: copy.dummy_title(),
+ author: copy.dummy_author(),
+ isbn: copy.dummy_isbn(),
+ copy: copy
+ };
+ }
+
+ const volume = copy.call_number();
+ const record = volume.record();
+ const display = record.wide_display_entry();
+
+ let isbn = JSON.parse(display.isbn());
+ if (Array.isArray(isbn)) { isbn = isbn.join(','); }
+
+ return {
+ title: JSON.parse(display.title()),
+ author: JSON.parse(display.author()),
+ isbn: isbn,
+ copy: copy,
+ volume: volume,
+ record: record,
+ display: display
+ };
+ }
+
getNonCatTypes(): Promise<IdlObject[]> {
if (this.nonCatTypes) {
import {AuthService} from '@eg/core/auth.service';
import {PcrudService} from '@eg/core/pcrud.service';
import {CheckoutParams, CheckoutResult, CheckinParams, CheckinResult,
- CircService} from './circ.service';
+ CircDisplayInfo, CircService} from './circ.service';
import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
import {ToastService} from '@eg/share/toast/toast.service';
import {AddBillingDialogComponent} from './billing-dialog.component';
-export interface CircGridEntry {
+export interface CircGridEntry extends CircDisplayInfo {
index: string; // class + id -- row index
- title?: string;
- author?: string;
- isbn?: string;
- copy?: IdlObject;
circ?: IdlObject;
- volume?: IdlObject;
- record?: IdlObject;
dueDate?: string;
copyAlertCount?: number;
nonCatCount?: number;
gridify(circ: IdlObject): CircGridEntry {
+ const circDisplay = this.circ.getDisplayInfo(circ);
+
const entry: CircGridEntry = {
index: `circ-${circ.id()}`,
circ: circ,
dueDate: circ.due_date(),
+ title: circDisplay.title,
+ author: circDisplay.author,
+ isbn: circDisplay.isbn,
+ copy: circDisplay.copy,
+ volume: circDisplay.volume,
+ record: circDisplay.copy,
+ display: circDisplay.display,
copyAlertCount: 0 // TODO
};
- const copy = circ.target_copy();
- entry.copy = copy;
-
- // Some values have to be manually extracted / normalized
- if (copy.call_number().id() === -1) {
-
- entry.title = copy.dummy_title();
- entry.author = copy.dummy_author();
- entry.isbn = copy.dummy_isbn();
-
- } else {
-
- entry.volume = copy.call_number();
- entry.record = entry.volume.record();
-
- // display entries are JSON-encoded and some are lists
- const display = entry.record.wide_display_entry();
-
- entry.title = JSON.parse(display.title());
- entry.author = JSON.parse(display.author());
- entry.isbn = JSON.parse(display.isbn());
-
- if (Array.isArray(entry.isbn)) {
- entry.isbn = entry.isbn.join(',');
- }
- }
-
return entry;
}