From: Bill Erickson Date: Wed, 16 Jun 2021 21:55:08 +0000 (-0400) Subject: LP1936233 Item Status UI Angular Port WIP X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=0c98b4e0aa1101952a189d31b9f6ebce4551fd34;p=working%2FEvergreen.git LP1936233 Item Status UI Angular Port WIP Signed-off-by: Bill Erickson --- diff --git a/Open-ILS/src/eg2/src/app/staff/cat/item/item.module.ts b/Open-ILS/src/eg2/src/app/staff/cat/item/item.module.ts index 9c501fbf8e..f1046892c4 100644 --- a/Open-ILS/src/eg2/src/app/staff/cat/item/item.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/cat/item/item.module.ts @@ -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 index 0000000000..b089e5799d --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/item/recent-history.component.html @@ -0,0 +1,71 @@ + +
+
+
+ No Previous Circ Group +
+
+
+
+
+
+ No Recent Circ Group +
+
+ + +
+
Total Circs
+
{{circInfo.totalCircs}}
+
+ +
+
Checkout Date
+
+ + {{circInfo.circSummary.start_time() | date:format.dateTimeFormat}} + +
+
+ +
+
Checkout Workstation
+
+ + {{circInfo.circSummary.checkout_workstation()}} + +
+
+ +
+
Last Renewed On
+
+ + {{circInfo.circSummary.last_renewal_time() | date:format.dateTimeFormat}} + +
+
+ +
+
Renewal Workstation
+
+ + {{circInfo.circSummary.last_renewal_workstation()}} + +
+
+ +
+
+
+ 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 index 0000000000..210f0006d4 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/item/recent-history.component.ts @@ -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 { + return this.circs.getItemCircInfo(this.item) + .then(info => this.circInfo = info); + } +} + + diff --git a/Open-ILS/src/eg2/src/app/staff/cat/item/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/cat/item/routing.module.ts index b3e775957b..f7f4bb431d 100644 --- a/Open-ILS/src/eg2/src/app/staff/cat/item/routing.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/cat/item/routing.module.ts @@ -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 index 0000000000..ea75b82daa --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/item/status.component.html @@ -0,0 +1,51 @@ + + + + + + + +
+
+
+
+ Barcode +
+ +
+ +
+
+ + + +
+ +
+ + +
+ + +
+ +
+ 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 index 0000000000..7764ba1179 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/item/status.component.ts @@ -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 { + 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 { + + 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 index 0000000000..4d9065f3c0 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/item/summary.component.html @@ -0,0 +1,288 @@ + + + +
+ This item has been marked as Deleted. +
+ +
+ +
+
Precat Title
+
{{item.dummy_title()}}
+ +
Precat Author
+
{{item.dummy_author()}}
+
+ +
+
Barcode
+
{{item.barcode()}}
+ +
Circ Library
+
{{item.circ_lib().shortname()}}
+ +
Call # Prefix
+
+ {{item.call_number().prefix().label()}} +
+ +
Status
+
{{item.status().name()}}
+
+ +
+
Price
+
{{item.price()}}
+ +
Owning Library
+
{{item.call_number().owning_lib().shortname()}}
+ +
Call #
+
{{item.call_number().label()}}
+ +
Due Date
+
+ + {{circInfo.currentCirc | egDueDatePipe}} + +
+
+ +
+
Acquisition Cost
+
{{item.cost()}}
+ +
Shelving Location
+
{{item.location().name()}}
+ +
Call # Suffix
+
+ {{item.call_number().suffix().label()}} +
+ +
Checkout Date
+
+ + {{circInfo.circSummary.start_time() | date:'shortDate'}} + +
+
+ +
+
ISBN
+
+ {{item.call_number().record().simple_record().isbn() || item.dummy_isbn()}} +
+ +
Loan Duration
+
+
Short
+
Normal
+
Long
+
+ +
Renewal Type
+
+ +
OPAC
+
Desk
+
Phone
+
Automatic
+
+
+ +
Checkout Workstation
+
+ + {{circInfo.circSummary.checkout_workstation()}} + +
+
+ +
+
Date Created
+
{{item.create_date() | date:'shortDate'}}
+
Fine Level
+
+
Low
+
Normal
+
High
+
+ +
Total Circs
+
{{circInfo.totalCircs}}
+ +
Duration Rule
+
+ + {{circInfo.currentCirc.duration_rule().name()}} + +
+
+ +
+
Date Active
+
{{item.active_date() | date:'shortDate'}}
+ +
Reference
+
+ +
Total Circs - Current Year
+
{{circInfo.circsThisYear}}
+ +
Recurring Fine Rule
+
+ + {{circInfo.currentCirc.recurring_fine_rule().name()}} + +
+
+ +
+
Status Changed
+
{{item.status_changed_time() | date:'shortDate'}}
+ +
OPAC Visible
+
+ +
Total Circs - Prev Year
+
{{circInfo.circsPrevYear}}
+ +
Max Fine Rule
+
+ + {{circInfo.currentCirc.max_fine_rule().name()}} + +
+
+ +
+
Item ID
+
{{item.id()}}
+ +
Holdable
+
+ +
In-House Uses
+
{{item._inHouseUseCount}}
+ +
Checkin Time
+
+ + {{circInfo.currentCirc.checkin_time() || + circInfo.circSummary.last_checkin_time() | date:'shortDate'}} + +
+
+ +
+
Circulate
+
+ +
Renewal Workstation
+
+ + {{circInfo.circSummary.last_renewal_workstation()}} + +
+ +
Remaining Renewals
+
+ + {{circInfo.currentCirc.renewal_remaining()}} + +
+ +
Checkin Scan Time
+
+ + {{circInfo.currentCirc.checkin_scan_time() || + circInfo.circSummary.last_checkin_scan_time() | date:'shortDate'}} + +
+
+ +
+
Floating
+
+ + {{item.floating().name()}} + +
+ +
Circ Modifier
+
+ + {{item.circ_modifier().name()}} + +
+ +
Age-based Hold Protection
+
+ + {{item.age_protect().name()}} + +
+ +
Checkin Workstation
+
+ + + {{circInfo.currentCirc.checkin_workstation().name()}} + + + {{circInfo.circSummary.last_checkin_workstation().name()}} + + +
+
+ +
+
Inventory Date
+
+ + {{item.latest_inventory().inventory_date() | date:'shortDate'}} + +
+ +
Inventory Workstation
+
+ + {{item.latest_inventory().inventory_workstation().name()}} + +
+ +
+
+
+
+
+ +
+
Item Alerts
+
+ + +
+ + +
+
+
+
+
+
+
+ +
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 index 0000000000..3b02cbf08d --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/item/summary.component.ts @@ -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 { + 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(); + } +} + 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 61ae54926e..314c748a5d 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 @@ -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; @@ -208,6 +209,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; @@ -232,6 +244,7 @@ export class CircService { private auth: AuthService, private holdings: HoldingsService, private worklog: WorkLogService, + private perms: PermService, private bib: BibRecordService ) {} @@ -1316,5 +1329,126 @@ export class CircService { return checkDigit; } + + getCircChain(circId: number): Promise { + 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 { + + 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 { + + 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 { + + 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); + } + }); + }); + }); + } } +