LP1904036 Patron items out grid; shared circ grid
authorBill Erickson <berickxx@gmail.com>
Fri, 19 Feb 2021 21:21:07 +0000 (16:21 -0500)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 28 Oct 2022 00:13:23 +0000 (20:13 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <js7389@princeton.edu>
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.html
Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/items.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/items.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/patron.component.html
Open-ILS/src/eg2/src/app/staff/circ/patron/patron.module.ts
Open-ILS/src/eg2/src/app/staff/share/barcodes/barcode-select.component.ts
Open-ILS/src/eg2/src/app/staff/share/circ/circ.module.ts
Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts
Open-ILS/src/eg2/src/app/staff/share/circ/grid.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/circ/grid.component.ts [new file with mode: 0644]

index 2e73da2..b1e12fe 100644 (file)
@@ -84,7 +84,7 @@
   <div class="col-lg-12">
     <eg-grid #checkoutsGrid [dataSource]="gridDataSource" [sortable]="true"
       [useLocalSort]="true" [cellTextGenerator]="cellTextGenerator"
-      persistKey="circ.patron.checkout">
+      [disablePaging]="true" persistKey="circ.patron.checkout">
 
                        <eg-grid-toolbar-action
                                i18n-group group="Add" i18n-label label="Add Item Alerts"
index f7e5b4a..bbb1998 100644 (file)
@@ -29,7 +29,7 @@ const SESSION_DUE_DATE = 'eg.circ.checkout.is_until_logout';
   templateUrl: 'checkout.component.html',
   selector: 'eg-patron-checkout'
 })
-export class CheckoutComponent implements OnInit {
+export class CheckoutComponent implements OnInit, AfterViewInit {
 
     maxNoncats = 99; // Matches AngJS version
     checkoutNoncat: IdlObject = null;
@@ -129,10 +129,10 @@ export class CheckoutComponent implements OnInit {
         let barcode;
         const promise = params ? Promise.resolve(params) : this.collectParams();
 
-        return promise.then((params: CheckoutParams) => {
-            if (!params) { return null; }
+        return promise.then((collectedParams: CheckoutParams) => {
+            if (!collectedParams) { return null; }
 
-            barcode = params.copy_barcode || '';
+            barcode = collectedParams.copy_barcode || '';
 
             if (barcode) {
 
@@ -144,7 +144,7 @@ export class CheckoutComponent implements OnInit {
                 this.copiesInFlight[barcode] = true;
             }
 
-            return this.circ.checkout(params);
+            return this.circ.checkout(collectedParams);
         })
 
         .then((result: CheckoutResult) => {
@@ -287,7 +287,7 @@ export class CheckoutComponent implements OnInit {
                 Object.keys(values).forEach(key => params[key] = values[key]);
                 this.checkout(params);
             }
-        })
+        });
     }
 
     selectedCopyIds(rows: CircGridEntry[]): number[] {
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/items.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/items.component.html
new file mode 100644 (file)
index 0000000..5ec8605
--- /dev/null
@@ -0,0 +1,38 @@
+<ng-template #progress>
+  <div class="row">
+    <div class="col-lg-6 offset-lg-3">
+      <eg-progress-inline></eg-progress-inline>
+    </div>
+  </div>
+</ng-template>
+
+<div>
+  <ul ngbNav #itemsNav="ngbNav" class="nav-tabs"
+    [activeId]="itemsTab" (navChange)="tabChange($event)">
+    <li ngbNavItem="checkouts">
+      <a ngbNavLink i18n>Items Checked Out ({{mainList.length}})</a>
+      <ng-template ngbNavContent>
+        <ng-container *ngIf="loading">
+          <ng-container *ngTemplateOutlet="progress"></ng-container>
+        </ng-container>
+        <eg-circ-grid #checkoutsGrid></eg-circ-grid>
+      </ng-template>
+    </li>
+    <li ngbNavItem="other">
+      <a ngbNavLink i18n>Other/Special Circulations ({{altList.length}})</a>
+      <ng-template ngbNavContent>
+        <ng-container>
+        </ng-container>
+      </ng-template>
+    </li>
+    <li ngbNavItem="noncat">
+      <a ngbNavLink i18n>Non-Cataloged Circulations</a>
+      <ng-template ngbNavContent>
+        <ng-container>
+        </ng-container>
+      </ng-template>
+    </li>
+  </ul>
+  <div [ngbNavOutlet]="itemsNav"></div>
+</div>
+
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
new file mode 100644 (file)
index 0000000..4886c0d
--- /dev/null
@@ -0,0 +1,131 @@
+import {Component, OnInit, AfterViewInit, Input, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Observable, empty, of, from} from 'rxjs';
+import {tap, switchMap} from 'rxjs/operators';
+import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+import {IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PatronService} from '@eg/staff/share/patron/patron.service';
+import {PatronManagerService} from './patron.service';
+import {CheckoutResult, CircService} from '@eg/staff/share/circ/circ.service';
+import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
+import {GridDataSource, GridColumn, GridCellTextGenerator} from '@eg/share/grid/grid';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {StoreService} from '@eg/core/store.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {AudioService} from '@eg/share/util/audio.service';
+import {CopyAlertsDialogComponent
+    } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
+import {CircGridComponent} from '@eg/staff/share/circ/grid.component';
+
+@Component({
+  templateUrl: 'items.component.html',
+  selector: 'eg-patron-items'
+})
+export class ItemsComponent implements OnInit, AfterViewInit {
+
+    // Note we can get the patron id from this.context.patron.id(), but
+    // on a new page load, this requires us to wait for the arrival of
+    // the patron object before we can fetch our circs.  This is just simpler.
+    @Input() patronId: number;
+
+    itemsTab = 'checkouts';
+    loading = false;
+    mainList: number[] = [];
+    altList: number[] = [];
+    noncatDataSource: GridDataSource = new GridDataSource();
+
+    @ViewChild('checkoutsGrid') private checkoutsGrid: CircGridComponent;
+
+    constructor(
+        private org: OrgService,
+        private net: NetService,
+        private auth: AuthService,
+        public circ: CircService,
+        private audio: AudioService,
+        private store: StoreService,
+        private serverStore: ServerStoreService,
+        public patronService: PatronService,
+        public context: PatronManagerService
+    ) {}
+
+    ngOnInit() {
+    }
+
+    ngAfterViewInit() {
+        setTimeout(() => this.loadTab(this.itemsTab));
+    }
+
+    tabChange(evt: NgbNavChangeEvent) {
+        setTimeout(() => this.loadTab(evt.nextId));
+    }
+
+    loadTab(name: string) {
+        this.loading = true;
+        let promise;
+        if (name === 'checkouts') {
+            promise = this.loadCheckoutsGrid();
+        }
+
+        promise.then(_ => this.loading = false);
+    }
+
+    loadCheckoutsGrid(): Promise<any> {
+        this.mainList = [];
+        this.altList = [];
+
+        const promise = this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.user.checked_out.authoritative',
+            this.auth.token(), this.patronId
+        ).toPromise().then(checkouts => {
+            this.mainList = checkouts.overdue.concat(checkouts.out);
+
+            // TODO promise_circs, etc.
+        });
+
+        // TODO: fetch checked in
+
+        return promise.then(_ => {
+            this.checkoutsGrid.load(this.mainList)
+            .subscribe(null, null, () => this.checkoutsGrid.reloadGrid());
+        });
+    }
+
+    /*
+    function get_circ_ids() {
+        $scope.main_list = [];
+        $scope.alt_list = [];
+
+        // we can fetch these in parallel
+        var promise1 = egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.user.checked_out.authoritative',
+            egCore.auth.token(), $scope.patron_id
+        ).then(function(outs) {
+            $scope.main_list = outs.overdue.concat(outs.out);
+            promote_circs(outs.lost, display_lost, true);
+            promote_circs(outs.long_overdue, display_lo, true);
+            promote_circs(outs.claims_returned, display_cr, true);
+        });
+
+        // only fetched checked-in-with-bills circs if configured to display
+        var promise2 = !fetch_checked_in ? $q.when() : egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.user.checked_in_with_fines.authoritative',
+            egCore.auth.token(), $scope.patron_id
+        ).then(function(outs) {
+            promote_circs(outs.lost, display_lost);
+            promote_circs(outs.long_overdue, display_lo);
+            promote_circs(outs.claims_returned, display_cr);
+        });
+
+        return $q.all([promise1, promise2]);
+    }
+    */
+
+}
+
+
index b520060..7362069 100644 (file)
@@ -57,6 +57,7 @@
           <a ngbNavLink i18n>Items Out</a>
           <ng-template ngbNavContent>
             <div class="">
+              <eg-patron-items [patronId]="patronId"></eg-patron-items>
             </div>
           </ng-template>
         </li>
index b4403f5..1cc7dcb 100644 (file)
@@ -17,6 +17,7 @@ import {EditToolbarComponent} from './edit-toolbar.component';
 import {BcSearchComponent} from './bcsearch.component';
 import {PrecatCheckoutDialogComponent} from './precat-dialog.component';
 import {BarcodesModule} from '@eg/staff/share/barcodes/barcodes.module';
+import {ItemsComponent} from './items.component';
 
 @NgModule({
   declarations: [
@@ -27,6 +28,7 @@ import {BarcodesModule} from '@eg/staff/share/barcodes/barcodes.module';
     EditComponent,
     EditToolbarComponent,
     BcSearchComponent,
+    ItemsComponent,
     PrecatCheckoutDialogComponent
   ],
   imports: [
index 4191c1b..6f2f847 100644 (file)
@@ -56,8 +56,8 @@ export class BarcodeSelectComponent extends DialogComponent implements OnInit {
     }
 
     selectionChanged() {
-        const id = Object.keys(this.inputs).map(id => Number(id))
-            .filter(id => this.inputs[id] === true)[0];
+        const id = Object.keys(this.inputs).map(i => Number(i))
+            .filter(i => this.inputs[i] === true)[0];
 
         if (id) {
             this.selected = this.matches.filter(match => match.id === id)[0];
@@ -93,9 +93,9 @@ export class BarcodeSelectComponent extends DialogComponent implements OnInit {
 
             if (!results) { return result; }
 
-            results.forEach(result => {
-                if (!this.evt.parse(result)) {
-                    this.matches.push(result);
+            results.forEach(res => {
+                if (!this.evt.parse(res)) {
+                    this.matches.push(res);
                 }
             });
 
index 907a85d..02d0b9a 100644 (file)
@@ -2,15 +2,18 @@ import {NgModule} from '@angular/core';
 import {StaffCommonModule} from '@eg/staff/common.module';
 import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module';
 import {CircService} from './circ.service';
+import {CircGridComponent} from './grid.component';
 
 @NgModule({
     declarations: [
+        CircGridComponent
     ],
     imports: [
         StaffCommonModule,
         HoldingsModule
     ],
     exports: [
+        CircGridComponent
     ],
     providers: [
         CircService
index 2acffcb..7bede2f 100644 (file)
@@ -31,7 +31,7 @@ export interface CheckoutParams {
 export interface CheckoutResult {
     index: number;
     evt: EgEvent;
-    params: CheckoutParams,
+    params: CheckoutParams;
     success: boolean;
     copy?: IdlObject;
     circ?: IdlObject;
diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/grid.component.html b/Open-ILS/src/eg2/src/app/staff/share/circ/grid.component.html
new file mode 100644 (file)
index 0000000..b4c1f5e
--- /dev/null
@@ -0,0 +1,40 @@
+
+<eg-progress-dialog #progressDialog></eg-progress-dialog>
+<eg-copy-alerts-dialog #copyAlertsDialog></eg-copy-alerts-dialog>
+
+<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>
+
+<eg-grid #circGrid [dataSource]="gridDataSource" [sortable]="true"
+  [useLocalSort]="true" [cellTextGenerator]="cellTextGenerator"
+  [disablePaging]="true" [persistKey]="persistKey">
+
+  <eg-grid-toolbar-action
+    i18n-group group="Add" i18n-label label="Add Item Alerts"
+    (onClick)="openItemAlerts($event, 'create')">
+  </eg-grid-toolbar-action>
+
+  <eg-grid-column [index]="true" path="circ.id" 
+    label="Circ ID" i18n-label></eg-grid-column>
+
+  <!-- TODO 
+    [datePlusTime]="true" when non-full-day circ 
+  -->
+  <eg-grid-column path="dueDate" label="Due Date" i18n-label
+    datatype="timestamp"></eg-grid-column>
+
+  <eg-grid-column path="copy.barcode" label="Barcode" i18n-label></eg-grid-column>
+
+  <eg-grid-column path="title" label="Title" i18n-label 
+    [cellTemplate]="titleTemplate"></eg-grid-column>
+
+  <eg-grid-column path="nonCatCount" label="Non-Cataloged Count"
+    i18n-label></eg-grid-column>
+
+</eg-grid>
+
+
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
new file mode 100644 (file)
index 0000000..7213294
--- /dev/null
@@ -0,0 +1,181 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Observable, empty, of, from} from 'rxjs';
+import {map, tap, switchMap} from 'rxjs/operators';
+import {IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {CheckoutParams, CheckoutResult, CircService} from './circ.service';
+import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
+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 {StoreService} from '@eg/core/store.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {AudioService} from '@eg/share/util/audio.service';
+import {CopyAlertsDialogComponent
+    } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
+import {ArrayUtil} from '@eg/share/util/array';
+
+export interface CircGridEntry {
+    title?: string;
+    author?: string;
+    isbn?: string;
+    copy?: IdlObject;
+    circ?: IdlObject;
+    dueDate?: string;
+    copyAlertCount?: number;
+    nonCatCount?: number;
+}
+
+const CIRC_FLESH_DEPTH = 4;
+const CIRC_FLESH_FIELDS = {
+  circ: ['target_copy', 'workstation', 'checkin_workstation'],
+  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: 'grid.component.html',
+  selector: 'eg-circ-grid'
+})
+export class CircGridComponent implements OnInit {
+
+    @Input() persistKey: string;
+
+    entries: CircGridEntry[] = null;
+    gridDataSource: GridDataSource = new GridDataSource();
+    cellTextGenerator: GridCellTextGenerator;
+
+    @ViewChild('circGrid') private circGrid: GridComponent;
+    @ViewChild('copyAlertsDialog')
+        private copyAlertsDialog: CopyAlertsDialogComponent;
+
+    constructor(
+        private org: OrgService,
+        private net: NetService,
+        private pcrud: PcrudService,
+        public circ: CircService,
+        private audio: AudioService,
+        private store: StoreService,
+        private serverStore: ServerStoreService
+    ) {}
+
+    ngOnInit() {
+
+        // The grid never fetches data directly.
+        // The caller is responsible initiating all data loads.
+        this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
+            if (this.entries) {
+                return from(this.entries);
+            } else {
+                return empty();
+            }
+        };
+
+        this.cellTextGenerator = {
+            title: row => row.title
+        };
+    }
+
+    // Reload the grid without any data retrieval
+    reloadGrid() {
+        this.circGrid.reload();
+    }
+
+    // Fetch circulation data and make it available to the grid.
+    load(circIds: number[]): Observable<CircGridEntry> {
+
+        // No circs to load
+        if (!circIds || circIds.length === 0) { return empty(); }
+
+        // Return the circs we have already retrieved.
+        if (this.entries) { return from(this.entries); }
+
+        this.entries = [];
+
+        return this.pcrud.search('circ', {id: circIds}, {
+            flesh: CIRC_FLESH_DEPTH,
+            flesh_fields: CIRC_FLESH_FIELDS,
+            order_by : {circ : ['xact_start']},
+
+            // Avoid fetching the MARC blob by specifying which
+            // fields on the bre to select.  More may be needed.
+            // Note that fleshed fields are explicitly selected.
+            select: {bre : ['id']}
+
+        }).pipe(map(circ => {
+
+            const entry = this.gridify(circ);
+            this.entries.push(entry);
+            return entry;
+        }));
+    }
+
+    gridify(circ: IdlObject): CircGridEntry {
+
+        const entry: CircGridEntry = {
+            circ: circ,
+            dueDate: circ.due_date(),
+            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 {
+
+            const display =
+                copy.call_number().record().wide_display_entry();
+
+            entry.title = display.title();
+            entry.author = display.author();
+            entry.isbn = display.isbn();
+        }
+
+        return entry;
+    }
+
+    selectedCopyIds(rows: CircGridEntry[]): number[] {
+        return rows
+            .filter(row => row.copy)
+            .map(row => Number(row.copy.id()));
+    }
+
+    openItemAlerts(rows: CircGridEntry[], mode: string) {
+        const copyIds = this.selectedCopyIds(rows);
+        if (copyIds.length === 0) { return; }
+
+        this.copyAlertsDialog.copyIds = copyIds;
+        this.copyAlertsDialog.mode = mode;
+        this.copyAlertsDialog.open({size: 'lg'}).subscribe(
+            modified => {
+                if (modified) {
+                    // TODO: verify the modiifed alerts are present
+                    // or go fetch them.
+                    this.circGrid.reload();
+                }
+            }
+        );
+    }
+}
+