From 58983e43ca2c483a4c07889615a9d761511fa756 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Fri, 5 Mar 2021 16:50:59 -0500 Subject: [PATCH] LP1904036 Bills tab Signed-off-by: Bill Erickson Signed-off-by: Jane Sandberg Signed-off-by: Galen Charlton --- .../src/app/staff/circ/patron/bills.component.css | 6 + .../src/app/staff/circ/patron/bills.component.html | 133 ++++++++++++++++----- .../src/app/staff/circ/patron/bills.component.ts | 107 ++++++++++++++++- .../src/app/staff/circ/patron/items.component.ts | 5 +- .../eg2/src/app/staff/share/circ/circ.service.ts | 42 +++++++ .../eg2/src/app/staff/share/circ/grid.component.ts | 46 ++----- 6 files changed, 274 insertions(+), 65 deletions(-) diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.css b/Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.css index 9b8b24f293..ce9c2ae648 100644 --- a/Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.css +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.css @@ -4,3 +4,9 @@ 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); +} + diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.html index 5b80429a37..a211e96006 100644 --- a/Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.html +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.html @@ -1,61 +1,65 @@ + + -
+
-
+
Total Owed:
-
{{(summary.total_paid() || 0) | currency}}
+
+ {{summary.balance_owed() | currency}}
-
+
Total Billed:
-
{{(summary.total_owed() || 0) | currency}}
+
{{summary.total_owed() | currency}}
-
+
Total Paid/Credited:
-
{{(summary.total_paid() || 0) | currency}}
+
{{summary.total_paid() | currency}}
-
+
Owed for Selected:
{{owedSelected() | currency}}
-
+
Billed for Selected:
{{billedSelected() | currency}}
-
+
Paid/Credited for Selected:
{{paidSelected() | currency}}
-
+
Refunds Available:
{{refundsAvailable() | currency}}
-
+
Credit Available:
{{patron().credit_forward_balance() | currency}}
-
+
Session Voided:
{{sessionVoided | currency}}
-
+
 
 
-
+
Pending Payment:
{{pendingPayment() | currency}}
-
+
Pending Change:
{{pendingChange() | currency}}
@@ -63,12 +67,16 @@
-
+ + +

Pay Bill

+ +
-
Pay Bill:
-
-
- @@ -79,24 +87,95 @@
-
-
+
+
-
-
+
+
-
+
-
+
+ + + + + {{r.title}} + + {{r.title}} + + + + + {{r.copy.barcode()}} + + + + + + {{r.volume.prefix().label()}} {{r.volume.label()}} {{r.volume.suffix().label()}} + + UNCATALOGED + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.ts index 6f60f61f85..ba8d39c627 100644 --- a/Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.ts @@ -1,5 +1,7 @@ -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'; @@ -8,6 +10,36 @@ import {AuthService} from '@eg/core/auth.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', @@ -22,6 +54,12 @@ export class BillsComponent implements OnInit, AfterViewInit { paymentType = 'cash_payment'; checkNumber: string; annotatePayment = false; + entries: BillGridEntry[]; + + gridDataSource: GridDataSource = new GridDataSource(); + cellTextGenerator: GridCellTextGenerator; + + @ViewChild('billGrid') private billGrid: GridComponent; constructor( private router: Router, @@ -30,11 +68,31 @@ export class BillsComponent implements OnInit, AfterViewInit { 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(); } @@ -45,8 +103,53 @@ export class BillsComponent implements OnInit, AfterViewInit { 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 { diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/items.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/items.component.ts index d645033523..791e8f27c9 100644 --- a/Open-ILS/src/eg2/src/app/staff/circ/patron/items.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/items.component.ts @@ -208,7 +208,10 @@ export class ItemsComponent implements OnInit, AfterViewInit { 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); diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts b/Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts index 8414732c23..931afac50f 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts @@ -12,6 +12,15 @@ import {AudioService} from '@eg/share/util/audio.service'; 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', @@ -172,6 +181,39 @@ export class CircService { 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 { if (this.nonCatTypes) { diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/grid.component.ts b/Open-ILS/src/eg2/src/app/staff/share/circ/grid.component.ts index 0a28134d3e..180ff4576a 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/circ/grid.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/circ/grid.component.ts @@ -8,7 +8,7 @@ import {NetService} from '@eg/core/net.service'; 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'; @@ -33,15 +33,9 @@ import {ClaimsReturnedDialogComponent} from './claims-returned-dialog.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; @@ -246,40 +240,22 @@ export class CircGridComponent implements OnInit { 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; } -- 2.11.0