LP1936233 Item Status UI Angular Port WIP
authorBill Erickson <berickxx@gmail.com>
Wed, 16 Jun 2021 21:55:08 +0000 (17:55 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 14 Jul 2021 15:24:11 +0000 (11:24 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/staff/cat/item/item.module.ts
Open-ILS/src/eg2/src/app/staff/cat/item/recent-history.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/cat/item/recent-history.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/cat/item/routing.module.ts
Open-ILS/src/eg2/src/app/staff/cat/item/status.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/cat/item/status.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/cat/item/summary.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/cat/item/summary.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts

index 9c501fb..f104689 100644 (file)
@@ -5,16 +5,26 @@ import {ItemRoutingModule} from './routing.module';
 import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module';
 import {PatronModule} from '@eg/staff/share/patron/patron.module';
 import {MarkItemMissingPiecesComponent} from './missing-pieces.component';
+import {ItemStatusComponent} from './status.component';
+import {BarcodesModule} from '@eg/staff/share/barcodes/barcodes.module';
+import {CircModule} from '@eg/staff/share/circ/circ.module';
+import {ItemSummaryComponent} from './summary.component';
+import {ItemRecentHistoryComponent} from './recent-history.component';
 
 @NgModule({
   declarations: [
-    MarkItemMissingPiecesComponent
+    MarkItemMissingPiecesComponent,
+    ItemSummaryComponent,
+    ItemStatusComponent,
+    ItemRecentHistoryComponent
   ],
   imports: [
     StaffCommonModule,
     CommonWidgetsModule,
     ItemRoutingModule,
     HoldingsModule,
+    BarcodesModule,
+    CircModule,
     PatronModule
   ],
   providers: [
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/item/recent-history.component.html b/Open-ILS/src/eg2/src/app/staff/cat/item/recent-history.component.html
new file mode 100644 (file)
index 0000000..b089e57
--- /dev/null
@@ -0,0 +1,71 @@
+
+<div class="row pt-3" *ngIf="circInfo">
+  <div class="col-lg-6">
+    <div *ngIf="!circInfo.prevCircSummary" class="alert alert-info" i18n>
+      No Previous Circ Group
+    </div>
+    <div class="well-table" *ngIf="item && !loading && circInfo.prevCircSummary">
+    </div>
+  </div>
+  <div class="col-lg-6">
+    <div *ngIf="!circInfo.circSummary" class="alert alert-info" i18n>
+      No Recent Circ Group
+    </div>
+    <div class="well-table" *ngIf="item && !loading && circInfo.currentCirc">
+      <div class="well-row">
+        <div class="well-label" i18n>Patron</div>
+        <div class="well-value" *ngIf="circInfo.currentCirc">
+          <a i18n
+            routerLink="/staff/circInfo.currentCirc/patron/{{circInfo.currentCirc.usr().id()}}">
+            {{circInfo.currentCirc.usr().family_name()}}, 
+            {{circInfo.currentCirc.usr().first_given_name()}}, 
+            {{circInfo.currentCirc.usr().second_given_name()}}
+          </a>
+        </div>
+      </div>
+
+      <div class="well-row">
+        <div class="well-label" i18n>Total Circs</div>
+        <div class="well-value">{{circInfo.totalCircs}}</div>
+      </div>
+
+      <div class="well-row">
+        <div class="well-label" i18n>Checkout Date</div>
+        <div class="well-value">
+          <ng-container *ngIf="circInfo.currentCirc">
+            {{circInfo.circSummary.start_time() | date:format.dateTimeFormat}}
+          </ng-container>
+        </div>
+      </div>
+
+      <div class="well-row">
+        <div class="well-label" i18n>Checkout Workstation</div>
+        <div class="well-value">
+          <ng-container *ngIf="circInfo.currentCirc">
+            {{circInfo.circSummary.checkout_workstation()}}
+          </ng-container>
+        </div>
+      </div>
+
+      <div class="well-row">
+        <div class="well-label" i18n>Last Renewed On</div>
+        <div class="well-value">
+          <ng-container *ngIf="circInfo.circSummary">
+            {{circInfo.circSummary.last_renewal_time() | date:format.dateTimeFormat}}
+          </ng-container>
+        </div>
+      </div>
+
+      <div class="well-row">
+        <div class="well-label" i18n>Renewal Workstation</div>
+        <div class="well-value">
+          <ng-container *ngIf="circInfo.currentCirc">
+            {{circInfo.circSummary.last_renewal_workstation()}}
+          </ng-container>
+        </div>
+      </div>
+
+    </div>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/item/recent-history.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/item/recent-history.component.ts
new file mode 100644 (file)
index 0000000..210f000
--- /dev/null
@@ -0,0 +1,63 @@
+import {Component, Input, OnInit, AfterViewInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {PrintService} from '@eg/share/print/print.service';
+import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
+import {EventService} from '@eg/core/event.service';
+import {PermService} from '@eg/core/perm.service';
+import {PatronPenaltyDialogComponent} from '@eg/staff/share/patron/penalty-dialog.component';
+import {BarcodeSelectComponent} from '@eg/staff/share/barcodes/barcode-select.component';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {CircService, ItemCircInfo} from '@eg/staff/share/circ/circ.service';
+import {CopyAlertsDialogComponent
+    } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
+import {FormatService} from '@eg/core/format.service';
+
+@Component({
+  selector: 'eg-item-recent-history',
+  templateUrl: 'recent-history.component.html'
+})
+
+export class ItemRecentHistoryComponent implements OnInit {
+
+    @Input() item: IdlObject;
+
+    loading = false;
+    circInfo: ItemCircInfo;
+
+    @ViewChild('copyAlertsDialog') private copyAlertsDialog: CopyAlertsDialogComponent;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private net: NetService,
+        private org: OrgService,
+        private printer: PrintService,
+        private pcrud: PcrudService,
+        private auth: AuthService,
+        private perms: PermService,
+        private idl: IdlService,
+        private evt: EventService,
+        private cat: CatalogService,
+        private holdings: HoldingsService,
+        private circs: CircService,
+        public  format: FormatService
+    ) { }
+
+    ngOnInit() {
+        this.loading = true;
+        this.loadCircInfo()
+        .then(_ => this.loading = false);
+    }
+
+    loadCircInfo(): Promise<any> {
+        return this.circs.getItemCircInfo(this.item)
+        .then(info => this.circInfo = info);
+    }
+}
+
+
index b3e7759..f7f4bb4 100644 (file)
@@ -1,6 +1,7 @@
 import {NgModule} from '@angular/core';
 import {RouterModule, Routes} from '@angular/router';
 import {MarkItemMissingPiecesComponent} from './missing-pieces.component';
+import {ItemStatusComponent} from './status.component';
 
 const routes: Routes = [{
     path: 'missing_pieces',
@@ -8,6 +9,15 @@ const routes: Routes = [{
   }, {
     path: 'missing_pieces/:id',
     component: MarkItemMissingPiecesComponent
+  }, {
+    path: 'list',
+    component: ItemStatusComponent
+  }, {
+    path: ':id/:tab',
+    component: ItemStatusComponent
+  }, {
+    path: ':id',
+    component: ItemStatusComponent
 }];
 
 @NgModule({
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/item/status.component.html b/Open-ILS/src/eg2/src/app/staff/cat/item/status.component.html
new file mode 100644 (file)
index 0000000..ea75b82
--- /dev/null
@@ -0,0 +1,51 @@
+<eg-staff-banner i18n-bannerText bannerText="Item Status">
+</eg-staff-banner>
+
+<eg-barcode-select #barcodeSelect></eg-barcode-select>
+<eg-circ-components></eg-circ-components>
+<eg-bucket-dialog #bucketDialog></eg-bucket-dialog>
+
+<div class="row">
+  <div class="col-lg-12 form-inline">
+    <div class="input-group">
+      <div class="input-group-prepend">
+        <span class="input-group-text" id='barcode-label' i18n>Barcode</span>
+      </div>
+      <input type="text" class="form-control" id="item-barcode-input" 
+        (keydown)="noSuchItem=false; true;"
+        (keyup.enter)="getItemByBarcode()" [(ngModel)]="itemBarcode" 
+        aria-describedby="barcode-label"/>
+    </div>
+    <button class="btn btn-outline-dark" 
+      (click)="getItemByBarcode()" i18n>Submit</button>
+  </div>
+</div>
+
+
+
+<div *ngIf="tab != 'list' && item">
+
+  <div class="mt-2 mb-4">
+    <eg-bib-summary [recordId]="item.call_number().record().id()">
+    </eg-bib-summary>
+  </div>
+
+  <ul ngbNav #itemNav="ngbNav" class="nav-tabs"
+    [activeId]="tab" (navChange)="tabChange($event)">
+    <li ngbNavItem="summary">
+      <a ngbNavLink i18n>Quick Summary</a>
+      <ng-template ngbNavContent>
+        <eg-item-summary [item]="item"></eg-item-summary>
+      </ng-template>
+    </li>
+    <li ngbNavItem="recent-history">
+      <a ngbNavLink i18n>Recent Circ History</a>
+      <ng-template ngbNavContent>
+        <eg-item-recent-history [item]="item"></eg-item-recent-history>
+      </ng-template>
+    </li>
+  </ul>
+  <div [ngbNavOutlet]="itemNav"></div>
+
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/item/status.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/item/status.component.ts
new file mode 100644 (file)
index 0000000..7764ba1
--- /dev/null
@@ -0,0 +1,160 @@
+import {Component, Input, OnInit, AfterViewInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {PrintService} from '@eg/share/print/print.service';
+import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
+import {EventService} from '@eg/core/event.service';
+import {PatronPenaltyDialogComponent} from '@eg/staff/share/patron/penalty-dialog.component';
+import {BarcodeSelectComponent} from '@eg/staff/share/barcodes/barcode-select.component';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+import {CopyAlertsDialogComponent
+    } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
+import {BucketDialogComponent
+    } from '@eg/staff/share/buckets/bucket-dialog.component';
+
+@Component({
+  templateUrl: 'status.component.html'
+})
+
+export class ItemStatusComponent implements OnInit, AfterViewInit {
+
+    itemId: number;
+    itemBarcode: string;
+    noSuchItem = false;
+    item: IdlObject;
+    tab: string;
+
+    @ViewChild('barcodeSelect') private barcodeSelect: BarcodeSelectComponent;
+    @ViewChild('bucketDialog') private bucketDialog: BucketDialogComponent;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private net: NetService,
+        private printer: PrintService,
+        private pcrud: PcrudService,
+        private auth: AuthService,
+        private evt: EventService,
+        private cat: CatalogService,
+        private holdings: HoldingsService
+    ) {}
+
+    ngOnInit() {
+
+        this.itemId = +this.route.snapshot.paramMap.get('id');
+        this.tab = this.route.snapshot.paramMap.get('tab');
+
+        if (!this.tab) {
+            if (this.itemId) {
+                this.router.navigate([`/staff/cat/item/${this.itemId}/summary`])
+                .then(ok => {if (ok) { this.load(); }});
+                return;
+            } else {
+                this.tab = 'list';
+            }
+        }
+
+        this.load();
+    }
+
+    load() {
+
+        this.cat.fetchCcvms()
+        .then(_ => this.cat.fetchCmfs())
+        .then(_ => {
+            if (this.itemId) {
+                return this.getItemById(this.itemId);
+            }
+        })
+        .then(_ => {
+            // Avoid watching for changes until after ngOnInit is complete
+            // so we don't grab the same copy twice.
+
+            this.route.paramMap.subscribe((params: ParamMap) => {
+                this.tab = params.get('tab');
+                const id = +params.get('id');
+
+                if (id !== this.itemId) {
+                    this.itemId = id;
+                    if (id) {
+                        this.getItemById(id);
+                    }
+                }
+            });
+        });
+    }
+
+    ngAfterViewInit() {
+        this.selectInput();
+    }
+
+    tabChange(evt: NgbNavChangeEvent) {
+        this.router.navigate([`/staff/cat/item/${this.itemId}/${evt.nextId}`]);
+    }
+
+    getItemByBarcode(): Promise<any> {
+        this.itemId = null;
+        this.item = null;
+
+        if (!this.itemBarcode) { return Promise.resolve(); }
+
+        return this.barcodeSelect.getBarcode('asset', this.itemBarcode)
+        .then(res => {
+            if (!res.id) {
+                this.noSuchItem = true;
+            } else {
+                this.itemBarcode = null;
+
+                if (this.tab === 'list') {
+                    this.selectInput();
+                    return this.getItemById(res.id);
+                } else {
+                    this.router.navigate([`/staff/cat/item/${res.id}/${this.tab}`]);
+                }
+            }
+        });
+    }
+
+    selectInput() {
+        setTimeout(() => {
+            const node: HTMLInputElement =
+                document.getElementById('item-barcode-input') as HTMLInputElement;
+            if (node) { node.select(); }
+        });
+    }
+
+    getItemById(id: number): Promise<any> {
+
+        const flesh = {
+            flesh : 4,
+            flesh_fields : {
+                acp : [
+                    'call_number', 'location', 'status', 'floating', 'circ_modifier',
+                    'age_protect', 'circ_lib', 'copy_alerts', 'creator',
+                    'editor', 'circ_as_type', 'latest_inventory', 'floating'
+                ],
+                acn : ['record', 'prefix', 'suffix', 'label_class', 'owning_lib'],
+                bre : ['simple_record', 'creator', 'editor'],
+                alci : ['inventory_workstation']
+            },
+            select : {
+                // avoid fleshing MARC on the bre
+                // note: don't add simple_record.. not sure why
+                bre : ['id', 'tcn_value', 'creator', 'editor', 'create_date', 'edit_date'],
+            }
+        };
+
+        return this.pcrud.retrieve('acp', id, flesh)
+        .toPromise().then(item => {
+            this.item = item;
+            this.itemId = item.id();
+            this.selectInput();
+        });
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/item/summary.component.html b/Open-ILS/src/eg2/src/app/staff/cat/item/summary.component.html
new file mode 100644 (file)
index 0000000..4d9065f
--- /dev/null
@@ -0,0 +1,288 @@
+
+<eg-copy-alerts-dialog #copyAlertsDialog></eg-copy-alerts-dialog>
+
+<div class="alert alert-danger" *ngIf="item.deleted() == 't'">
+  This item has been marked as Deleted.
+</div>
+
+<div class="well-table" *ngIf="item && !loading">
+
+  <div class="well-row" *ngIf="item.dummy_title() || item.dummy_author()">
+    <div class="well-label" *ngIf="item.dummy_title()" i18n>Precat Title</div>
+    <div class="well-value" *ngIf="item.dummy_title()">{{item.dummy_title()}}</div>
+    
+    <div class="well-label" *ngIf="item.dummy_author()" i18n>Precat Author</div>
+    <div class="well-value" *ngIf="item.dummy_author()">{{item.dummy_author()}}</div>
+  </div>
+
+  <div class="well-row">
+    <div class="well-label" i18n>Barcode</div>
+    <div class="well-value">{{item.barcode()}}</div>
+
+    <div class="well-label" i18n>Circ Library</div>
+    <div class="well-value">{{item.circ_lib().shortname()}}</div>
+
+    <div class="well-label" i18n>Call # Prefix</div>
+    <div class="well-value">
+      {{item.call_number().prefix().label()}}
+    </div>
+
+    <div class="well-label" i18n>Status</div>
+    <div
+      class="well-value"
+      title="Holdable: {{item.status().holdable()}} / 
+        OPAC Visible: {{item.status().opac_visible()}}"
+    >{{item.status().name()}}</div>
+  </div>
+
+  <div class="well-row">
+    <div class="well-label" i18n>Price</div>
+    <div class="well-value">{{item.price()}}</div>
+
+    <div class="well-label" i18n>Owning Library</div>
+    <div class="well-value">{{item.call_number().owning_lib().shortname()}}</div>
+
+    <div class="well-label" i18n>Call #</div>
+    <div class="well-value">{{item.call_number().label()}}</div>
+
+    <div class="well-label" i18n>Due Date</div>
+    <div class="well-value">
+      <ng-container *ngIf="circInfo.currentCirc">
+        {{circInfo.currentCirc | egDueDatePipe}}
+      </ng-container>
+    </div>
+  </div>
+
+  <div class="well-row">
+       <div class="well-label" i18n>Acquisition Cost</div>
+    <div class="well-value">{{item.cost()}}</div>
+
+    <div class="well-label" i18n>Shelving Location</div>
+    <div
+      class="well-value"
+      title="Holdable: {{item.location().holdable()}} / 
+        OPAC Visible: {{item.location().opac_visible()}}"
+    >{{item.location().name()}}</div>
+
+    <div class="well-label" i18n>Call # Suffix</div>
+    <div class="well-value">
+      {{item.call_number().suffix().label()}}
+    </div>
+
+    <div class="well-label" i18n>Checkout Date</div>
+    <div class="well-value">
+      <ng-container *ngIf="circInfo.currentCirc">
+        {{circInfo.circSummary.start_time() | date:'shortDate'}}
+      </ng-container>
+    </div>
+  </div>
+
+  <div class="well-row">
+    <div class="well-label" i18n>ISBN</div>
+    <div class="well-value">
+      {{item.call_number().record().simple_record().isbn() || item.dummy_isbn()}}
+    </div>
+
+    <div class="well-label" i18n>Loan Duration</div>
+    <div class="well-value">
+      <div *ngIf="item.loan_duration() == 1" i18n>Short</div>
+      <div *ngIf="item.loan_duration() == 2" i18n>Normal</div>
+      <div *ngIf="item.loan_duration() == 3" i18n>Long</div>
+    </div>
+
+    <div class="well-label" i18n>Renewal Type</div>
+    <div class="well-value">
+      <ng-container *ngIf="circInfo.currentCirc">
+        <div *ngIf="circInfo.currentCirc.opac_renewal() == 't'" i18n>OPAC</div>
+        <div *ngIf="circInfo.currentCirc.desk_renewal() == 't'" i18n>Desk</div>
+        <div *ngIf="circInfo.currentCirc.phone_renewal() == 't'" i18n>Phone</div>
+        <div *ngIf="circInfo.currentCirc.auto_renewal() == 't'" i18n>Automatic</div>
+      </ng-container>
+    </div>
+
+    <div class="well-label" i18n>Checkout Workstation</div>
+    <div class="well-value">
+      <ng-container *ngIf="circInfo.currentCirc">
+        {{circInfo.circSummary.checkout_workstation()}}
+      </ng-container>
+    </div>
+  </div>
+
+  <div class="well-row">
+    <div class="well-label" i18n>Date Created</div>
+    <div class="well-value">{{item.create_date() | date:'shortDate'}}</div>
+    <div class="well-label" i18n>Fine Level</div>
+    <div class="well-value">
+      <div *ngIf="item.fine_level() == 1" i18n>Low</div>
+      <div *ngIf="item.fine_level() == 2" i18n>Normal</div>
+      <div *ngIf="item.fine_level() == 3" i18n>High</div>
+    </div>
+
+    <div class="well-label" i18n>Total Circs</div>
+    <div class="well-value">{{circInfo.totalCircs}}</div>
+
+    <div class="well-label" i18n>Duration Rule</div>
+    <div class="well-value">
+      <ng-container *ngIf="circInfo.currentCirc">
+        {{circInfo.currentCirc.duration_rule().name()}}
+      </ng-container>
+    </div>
+  </div>
+
+  <div class="well-row">
+    <div class="well-label" i18n>Date Active</div>
+    <div class="well-value">{{item.active_date() | date:'shortDate'}}</div>
+
+    <div class="well-label" i18n>Reference</div>
+    <div class="well-value"><eg-bool [value]="item.ref()"></eg-bool></div>
+
+    <div class="well-label" i18n>Total Circs - Current Year</div>
+    <div class="well-value">{{circInfo.circsThisYear}}</div>
+
+    <div class="well-label" i18n>Recurring Fine Rule</div>
+    <div class="well-value">
+      <ng-container *ngIf="circInfo.currentCirc">
+        {{circInfo.currentCirc.recurring_fine_rule().name()}}
+      </ng-container>
+    </div>
+  </div>
+
+  <div class="well-row">
+    <div class="well-label" i18n>Status Changed</div>
+    <div class="well-value">{{item.status_changed_time() | date:'shortDate'}}</div>
+
+    <div class="well-label" i18n>OPAC Visible</div>
+    <div class="well-value"><eg-bool [value]="item.opac_visible()"></eg-bool></div>
+
+    <div class="well-label" i18n>Total Circs - Prev Year</div>
+    <div class="well-value">{{circInfo.circsPrevYear}}</div>
+
+    <div class="well-label" i18n>Max Fine Rule</div>
+    <div class="well-value">
+      <ng-container *ngIf="circInfo.currentCirc">
+        {{circInfo.currentCirc.max_fine_rule().name()}}
+      </ng-container>
+    </div>
+  </div>
+
+  <div class="well-row">
+    <div class="well-label" i18n>Item ID</div>
+    <div class="well-value">{{item.id()}}</div>
+
+    <div class="well-label" i18n>Holdable</div>
+    <div class="well-value"><eg-bool [value]="item.holdable()"></eg-bool></div>
+
+    <div class="well-label" i18n>In-House Uses</div>
+    <div class="well-value">{{item._inHouseUseCount}}</div>
+
+    <div class="well-label" i18n>Checkin Time</div>
+    <div class="well-value">
+      <ng-container *ngIf="circInfo.currentCirc">
+        {{circInfo.currentCirc.checkin_time() || 
+          circInfo.circSummary.last_checkin_time() | date:'shortDate'}}
+      </ng-container>
+    </div>
+  </div>
+
+  <div class="well-row">
+    <div class="well-label" i18n>Circulate</div>
+    <div class="well-value"><eg-bool [value]="item.circulate()"></eg-bool></div>
+
+    <div class="well-label" i18n>Renewal Workstation</div>
+    <div class="well-value">
+      <ng-container *ngIf="circInfo.currentCirc">
+        {{circInfo.circSummary.last_renewal_workstation()}}
+      </ng-container>
+    </div>
+
+    <div class="well-label" i18n>Remaining Renewals</div>
+    <div class="well-value">
+      <ng-container *ngIf="circInfo.currentCirc">
+        {{circInfo.currentCirc.renewal_remaining()}}
+      </ng-container>
+    </div>
+
+    <div class="well-label" i18n>Checkin Scan Time</div>
+    <div class="well-value">
+      <ng-container *ngIf="circInfo.currentCirc">
+        {{circInfo.currentCirc.checkin_scan_time() || 
+          circInfo.circSummary.last_checkin_scan_time() | date:'shortDate'}}
+      </ng-container>
+    </div>
+  </div>
+
+  <div class="well-row">
+    <div class="well-label" i18n>Floating</div>
+    <div class="well-value">
+      <ng-container *ngIf="item.floating()">
+        {{item.floating().name()}}
+      </ng-container>
+    </div>
+
+    <div class="well-label" i18n>Circ Modifier</div>
+    <div class="well-value">
+      <ng-container *ngIf="item.circ_modifier()">
+        {{item.circ_modifier().name()}}
+      </ng-container>
+    </div>
+
+    <div class="well-label" i18n>Age-based Hold Protection</div>
+    <div class="well-value">
+      <ng-container *ngIf="item.age_protect()">
+        {{item.age_protect().name()}}
+      </ng-container>
+    </div>
+
+    <div class="well-label" i18n>Checkin Workstation</div>
+    <div class="well-value">
+      <ng-container *ngIf="circInfo.currentCirc">
+        <ng-container *ngIf="circInfo.currentCirc.checkin_workstation()">
+          {{circInfo.currentCirc.checkin_workstation().name()}}
+        </ng-container>
+        <ng-container 
+          *ngIf="!circInfo.currentCirc.checkin_workstation() && circInfo.circSummary.last_checkin_workstation()">
+          {{circInfo.circSummary.last_checkin_workstation().name()}}
+        </ng-container>
+      </ng-container>
+    </div>
+  </div>
+
+  <div class="well-row">
+    <div class="well-label" i18n>Inventory Date</div>
+    <div class="well-value">
+      <ng-container *ngIf="item.latest_inventory()">
+        {{item.latest_inventory().inventory_date() | date:'shortDate'}}
+      </ng-container>
+    </div>
+
+    <div class="well-label" i18n>Inventory Workstation</div>
+    <div class="well-value">
+      <ng-container *ngIf="item.latest_inventory()">
+        {{item.latest_inventory().inventory_workstation().name()}}
+      </ng-container>
+    </div>
+
+    <div class="well-label"></div>
+    <div class="well-value"></div>
+    <div class="well-label"></div>
+    <div class="well-value"></div>
+  </div>
+
+  <div class="well-row">
+    <div class="well-label" i18n>Item Alerts</div>
+    <div class="well-value" id="item-status-alert-msg">
+      <button class="btn btn-outline-dark" (click)="addItemAlerts()" i18n>Add</button>
+      <button class="btn btn-outline-dark ml-2" [disabled]="item.copy_alerts().length == 0"
+        (click)="manageItemAlerts()"  i18n>Manage</button>
+    </div>
+
+    <!-- maintain positioning -->
+    <div class="well-label"></div>
+    <div class="well-value"></div>
+    <div class="well-label"></div>
+    <div class="well-value"></div>
+    <div class="well-label"></div>
+    <div class="well-value"></div>
+  </div>
+
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/item/summary.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/item/summary.component.ts
new file mode 100644 (file)
index 0000000..3b02cbf
--- /dev/null
@@ -0,0 +1,72 @@
+import {Component, Input, OnInit, AfterViewInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {PrintService} from '@eg/share/print/print.service';
+import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
+import {EventService} from '@eg/core/event.service';
+import {PermService} from '@eg/core/perm.service';
+import {PatronPenaltyDialogComponent} from '@eg/staff/share/patron/penalty-dialog.component';
+import {BarcodeSelectComponent} from '@eg/staff/share/barcodes/barcode-select.component';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {CircService, ItemCircInfo} from '@eg/staff/share/circ/circ.service';
+import {CopyAlertsDialogComponent
+    } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
+
+@Component({
+  selector: 'eg-item-summary',
+  templateUrl: 'summary.component.html'
+})
+
+export class ItemSummaryComponent implements OnInit {
+
+    @Input() item: IdlObject;
+
+    loading = false;
+    circInfo: ItemCircInfo;
+
+    @ViewChild('copyAlertsDialog') private copyAlertsDialog: CopyAlertsDialogComponent;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private net: NetService,
+        private org: OrgService,
+        private printer: PrintService,
+        private pcrud: PcrudService,
+        private auth: AuthService,
+        private perms: PermService,
+        private idl: IdlService,
+        private evt: EventService,
+        private cat: CatalogService,
+        private holdings: HoldingsService,
+        private circs: CircService
+    ) { }
+
+    ngOnInit() {
+        this.loading = true;
+        this.loadCircInfo()
+        .then(_ => this.loading = false);
+    }
+
+    loadCircInfo(): Promise<any> {
+        return this.circs.getItemCircInfo(this.item)
+        .then(info => this.circInfo = info);
+    }
+
+    addItemAlerts() {
+        this.copyAlertsDialog.copyIds = [this.item.id()];
+        this.copyAlertsDialog.mode = 'create';
+        this.copyAlertsDialog.open({size: 'lg'}).subscribe();
+    }
+
+    manageItemAlerts() {
+        this.copyAlertsDialog.copyIds = [this.item.id()];
+        this.copyAlertsDialog.mode = 'manage';
+        this.copyAlertsDialog.open({size: 'lg'}).subscribe();
+    }
+}
+
index ffe0d38..81bee26 100644 (file)
@@ -4,7 +4,7 @@ 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 {PcrudService, PcrudQueryOps} 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';
@@ -15,6 +15,7 @@ import {StringService} from '@eg/share/string/string.service';
 import {ServerStoreService} from '@eg/core/server-store.service';
 import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
 import {WorkLogService, WorkLogEntry} from '@eg/staff/share/worklog/worklog.service';
+import {PermService} from '@eg/core/perm.service';
 
 export interface CircDisplayInfo {
     title?: string;
@@ -196,6 +197,17 @@ export interface CheckinResult extends CircResultCommon {
     destCourierCode?: string;
 }
 
+export interface ItemCircInfo {
+    maxHistoryCount: number;
+    circSummary?: IdlObject;
+    prevCircSummary?: IdlObject;
+    currentCirc?: IdlObject;
+    prevCircUser?: IdlObject;
+    totalCircs: number;
+    circsThisYear: number;
+    circsPrevYear: number;
+}
+
 @Injectable()
 export class CircService {
     static resultIndex = 0;
@@ -220,6 +232,7 @@ export class CircService {
         private auth: AuthService,
         private holdings: HoldingsService,
         private worklog: WorkLogService,
+        private perms: PermService,
         private bib: BibRecordService
     ) {}
 
@@ -1228,5 +1241,126 @@ export class CircService {
 
         return checkDigit;
     }
+
+    getCircChain(circId: number): Promise<IdlObject> {
+        return this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.renewal_chain.retrieve_by_circ.summary',
+            this.auth.token(), circId
+        ).toPromise();
+    }
+
+    getPrevCircChain(circId: number): Promise<IdlObject> {
+
+        return this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary',
+            this.auth.token(), circId
+
+        ).toPromise();
+    }
+
+    getLatestCirc(copyId: number, ops?: PcrudQueryOps): Promise<IdlObject> {
+
+        if (!ops) {
+            ops = {
+                flesh: 2,
+                flesh_fields: {
+                    aacs: [
+                        'usr',
+                        'workstation',
+                        'checkin_workstation',
+                        'duration_rule',
+                        'max_fine_rule',
+                        'recurring_fine_rule'
+                    ],
+                    au: ['card']
+                }
+            };
+        }
+
+        ops.order_by = {aacs: 'xact_start desc'};
+        ops.limit = 1;
+
+        return this.pcrud.search('aacs', {target_copy : copyId}, ops).toPromise();
+    }
+
+    getItemCircInfo(item: IdlObject): Promise<ItemCircInfo> {
+
+        const response: ItemCircInfo = {
+            maxHistoryCount: 0,
+            totalCircs: 0,
+            circsThisYear: 0,
+            circsPrevYear: 0
+        };
+
+        const copyOrg: number =
+            item.call_number().id() === -1 ?
+            item.circ_lib().id() :
+            item.call_number().owning_lib().id();
+
+        return this.pcrud.search('circbyyr',
+            {copy : item.id()}, null, {atomic : true}).toPromise()
+
+        .then(counts => {
+
+            const curYear = new Date().getFullYear();
+            const prevYear = curYear - 1;
+
+            counts.forEach(c => {
+                response.totalCircs += Number(c.count());
+                if (c.year() === curYear) {
+                    response.circsThisYear += Number(c.count());
+                }
+                if (c.year() === prevYear) {
+                    response.circsPrevYear += Number(c.count());
+                }
+            });
+        })
+        .then(_ => this.perms.hasWorkPermAt(['VIEW_COPY_CHECKOUT_HISTORY'], true))
+        .then(hasPerm => {
+            if (hasPerm['VIEW_COPY_CHECKOUT_HISTORY'].includes(copyOrg)) {
+                return this.org.settings('circ.item_checkout_history.max')
+                .then(sets => {
+                    response.maxHistoryCount = sets['circ.item_checkout_history.max'] || 4;
+                });
+            } else {
+                response.maxHistoryCount = 0;
+            }
+        })
+
+        .then(_ => this.getLatestCirc(item.id()))
+
+        .then(circ => {
+
+            if (!circ) { return response; }
+
+            response.currentCirc = circ;
+
+            return this.getCircChain(circ.id())
+            .then(summary => {
+                response.circSummary = summary;
+
+                if (response.maxHistoryCount <= 1) {
+                    return response;
+                }
+
+                return this.getPrevCircChain(circ.id())
+                .then(prevSummary => {
+                    if (!prevSummary) { return response; }
+
+                    response.prevCircSummary = prevSummary.summary;
+
+                    if (prevSummary.usr) { // aged circs have no 'usr'.
+
+                        return this.pcrud.retrieve('au', prevSummary.usr,
+                            {flesh : 1, flesh_fields : {au : ['card']}})
+                        .toPromise().then(user => response.prevCircUser = user);
+                    }
+                });
+            });
+        });
+    }
 }
 
+