<ng-container *ngIf="loading">
<ng-container *ngTemplateOutlet="progress"></ng-container>
</ng-container>
- <eg-circ-grid #checkoutsGrid (reloadRequested)="load()">
+ <eg-circ-grid #checkoutsGrid [persistKey]="persistKey" (reloadRequested)="load()">
</eg-circ-grid>
</ng-template>
</li>
<ng-container *ngIf="loading">
<ng-container *ngTemplateOutlet="progress"></ng-container>
</ng-container>
- <eg-circ-grid #otherGrid (reloadRequested)="load()">
+ <eg-circ-grid #otherGrid [persistKey]="persistKey" (reloadRequested)="load()">
</eg-circ-grid>
</ng-container>
</ng-template>
<ng-container *ngIf="loading">
<ng-container *ngTemplateOutlet="progress"></ng-container>
</ng-container>
- <eg-circ-grid #nonCatGrid (reloadRequested)="load()">
+ <eg-circ-grid #nonCatGrid [persistKey]="persistKey" (reloadRequested)="load()">
</eg-circ-grid>
</ng-container>
</ng-container>
displayClaimsReturned: number = null;
fetchCheckedIn = true;
displayAltList = true;
+ persistKey: string;
@ViewChild('checkoutsGrid') private checkoutsGrid: CircGridComponent;
@ViewChild('otherGrid') private otherGrid: CircGridComponent;
) {}
ngOnInit() {
- this.load();
+ this.load(true);
}
ngAfterViewInit() {
}
- load(): Promise<any> {
+ load(firstLoad?: boolean): Promise<any> {
this.loading = true;
- return this.applyDisplaySettings()
- .then(_ => this.loadTab(this.itemsTab));
+
+ if (firstLoad) {
+ return this.applyDisplaySettings()
+ .then(_ => this.loadTab(this.itemsTab));
+ } else {
+ return this.loadTab(this.itemsTab)
+ .then(_ => this.context.refreshPatron());
+ }
}
tabChange(evt: NgbNavChangeEvent) {
setTimeout(() => this.loadTab(evt.nextId));
}
- loadTab(name: string) {
+ loadTab(name: string): Promise<any> {
this.loading = true;
let promise;
if (name === 'checkouts') {
promise = this.loadNonCatGrid();
}
- promise.then(_ => this.loading = false);
+ this.persistKey = `circ.patron.items.${name}`;
+
+ return promise.then(_ => this.loading = false);
}
applyDisplaySettings(): Promise<any> {
this.loaded = false;
this.patron = null;
this.checkouts = [];
+ return this.refreshPatron(id).then(_ => this.loaded = true);
+ }
+
+ // Update the patron data without resetting all of the context data.
+ refreshPatron(id?: number): Promise<any> {
+ if (!id) { id = this.patron.id(); }
+
this.alerts = new PatronAlerts();
return this.net.request(
this.auth.token(), id, PATRON_FLESH_FIELDS).toPromise()
.then(p => this.patron = p)
.then(_ => this.getPatronStats(id))
- .then(_ => this.compileAlerts())
- .then(_ => this.loaded = true);
+ .then(_ => this.compileAlerts());
}
getPatronStats(id: number): Promise<any> {
--- /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>
+
+<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">
+ <eg-combobox #bTypeCbox [entries]="billingTypes"
+ [required]="true" (onChange)="btChanged($event)"></eg-combobox>
+ </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"
+ 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} 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 */
+
+@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 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 = 101; // Stock "Misc"
+ 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() {
+ this.close();
+ }
+}
+
import {ClaimsReturnedDialogComponent} from './claims-returned-dialog.component';
import {CircComponentsComponent} from './components.component';
import {CircEventsComponent} from './events-dialog.component';
+import {AddBillingDialogComponent} from './billing-dialog.component';
@NgModule({
declarations: [
DueDateDialogComponent,
PrecatCheckoutDialogComponent,
ClaimsReturnedDialogComponent,
- CircEventsComponent
+ CircEventsComponent,
+ AddBillingDialogComponent
],
imports: [
StaffCommonModule,
],
exports: [
CircGridComponent,
- CircComponentsComponent
+ CircComponentsComponent,
+ AddBillingDialogComponent
],
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(
case 'SUCCESS':
case 'NO_CHANGE':
this.audio.play('success.checkin');
- // TODO do copy status stuff
- break;
+ return this.handleCheckinSuccess(result);
case 'ITEM_NOT_CATALOGED':
this.audio.play('error.checkout.no_cataloged');
return this.components.routeToCatalogingDialog.open()
.toPromise().then(_ => result);
}
-
- // alert, etc.
}
return Promise.resolve(result);
}
+ handleCheckinSuccess(result: CheckinResult): Promise<CheckinResult> {
+ return Promise.resolve(result);
+ }
+
handleOverridableCheckinEvents(
result: CheckinResult, events: EgEvent[]): Promise<CheckinResult> {
const params = result.params;
dialogTitle="Claims Never Checked Out"
dialogBody="Mark {{claimsNeverCount}} items as Never Checked Out?">
</eg-confirm-dialog>
+<eg-add-billing-dialog #addBillingDialog></eg-add-billing-dialog>
<ng-template #titleTemplate let-r="row">
<eg-grid #circGrid [dataSource]="gridDataSource" [sortable]="true"
[rowFlairIsEnabled]="true" [rowFlairCallback]="rowFlair"
- [rowClassCallback]="rowClass"
- [useLocalSort]="true" [cellTextGenerator]="cellTextGenerator"
- [disablePaging]="true" [persistKey]="persistKey">
+ [rowClassCallback]="rowClass" [persistKey]="persistKey"
+ [useLocalSort]="true" [cellTextGenerator]="cellTextGenerator">
<eg-grid-toolbar-action
i18n-group group="View" i18n-label label="Print Item Receipt(s)"
</eg-grid-toolbar-action>
<eg-grid-toolbar-action
- i18n-group group="Edit" i18n-label label="Edit Due Date"
+ i18n-group group="Add" i18n-label label="Add Billing"
+ (onClick)="openBillingDialog($event)">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-group group="Circulation" i18n-label label="Edit Due Date"
(onClick)="editDueDate($event)">
</eg-grid-toolbar-action>
} 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';
export interface CircGridEntry {
index: string; // class + id -- row index
private progressDialog: ProgressDialogComponent;
@ViewChild('claimsReturnedDialog')
private claimsReturnedDialog: ClaimsReturnedDialogComponent;
+ @ViewChild('addBillingDialog')
+ private addBillingDialog: AddBillingDialogComponent;
constructor(
private org: OrgService,
// The grid never fetches data directly.
// The caller is responsible initiating all data loads.
this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
- return this.entries ? from(this.entries) : empty();
+ if (!this.entries) { return empty(); }
+
+ const page = this.entries.slice(pager.offset, pager.offset + pager.limit)
+ .filter(entry => entry !== undefined);
+
+ return from(page);
};
this.cellTextGenerator = {
if (!circ) { return false; } // noncat
if (row.overdue === undefined) {
- row.overdue = (Date.parse(circ.due_date()) < this.nowDate);
+
+ if (circ.stop_fines() &&
+ // Items that aren't really checked out can't be overdue.
+ circ.stop_fines().match(/LOST|CLAIMSRETURNED|CLAIMSNEVERCHECKEDOUT/)) {
+ row.overdue = false;
+ } else {
+ row.overdue = (Date.parse(circ.due_date()) < this.nowDate);
+ }
}
return row.overdue;
}
);
});
}
+
+ openBillingDialog(rows: CircGridEntry[]) {
+
+ let changesApplied = false;
+
+ from(this.getCircIds(rows))
+ .pipe(concatMap(id => {
+ this.addBillingDialog.xactId = id;
+ return this.addBillingDialog.open();
+ }))
+ .subscribe(
+ changes => {
+ if (changes) { changesApplied = true; }
+ },
+ err => this.reportError(err),
+ () => {
+ if (changesApplied) {
+ this.emitReloadRequest();
+ }
+ }
+ );
+ }
}