From e895aab613af5dffce45d7b1bd2796b053022e4d Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Mon, 2 Mar 2020 14:17:55 -0500 Subject: [PATCH] LP1865898 Scan Item as Missing Pieces Angular Port Port the 'Scan Item As Missing Pieces' staff client interface to Angular. Interface displays additional data (title/author/callnum) during the staff confirmation step. Signed-off-by: Bill Erickson Signed-off-by: Mike Risher Signed-off-by: Galen Charlton --- .../eg2/src/app/staff/cat/item/item.module.ts | 24 +++ .../cat/item/missing-pieces.component.html | 85 +++++++++ .../cat/item/missing-pieces.component.ts | 164 ++++++++++++++++++ .../src/app/staff/cat/item/routing.module.ts | 20 +++ .../eg2/src/app/staff/cat/routing.module.ts | 3 + .../src/eg2/src/app/staff/nav.component.html | 2 +- .../app/staff/sandbox/sandbox.component.html | 8 + .../app/staff/sandbox/sandbox.component.ts | 7 + .../src/app/staff/sandbox/sandbox.module.ts | 4 +- .../staff/share/holdings/holdings.service.ts | 20 +++ .../app/staff/share/patron/patron.module.ts | 7 +- .../patron/penalty-dialog.component.html | 59 +++++++ .../share/patron/penalty-dialog.component.ts | 130 ++++++++++++++ Open-ILS/src/templates/staff/navbar.tt2 | 2 +- 14 files changed, 530 insertions(+), 5 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/item/item.module.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/item/missing-pieces.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/item/missing-pieces.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/item/routing.module.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/share/patron/penalty-dialog.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/share/patron/penalty-dialog.component.ts 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 new file mode 100644 index 0000000000..9c501fbf8e --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/item/item.module.ts @@ -0,0 +1,24 @@ +import {NgModule} from '@angular/core'; +import {StaffCommonModule} from '@eg/staff/common.module'; +import {CommonWidgetsModule} from '@eg/share/common-widgets.module'; +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'; + +@NgModule({ + declarations: [ + MarkItemMissingPiecesComponent + ], + imports: [ + StaffCommonModule, + CommonWidgetsModule, + ItemRoutingModule, + HoldingsModule, + PatronModule + ], + providers: [ + ] +}) + +export class ItemModule {} diff --git a/Open-ILS/src/eg2/src/app/staff/cat/item/missing-pieces.component.html b/Open-ILS/src/eg2/src/app/staff/cat/item/missing-pieces.component.html new file mode 100644 index 0000000000..2d09622219 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/item/missing-pieces.component.html @@ -0,0 +1,85 @@ + + + + + +
+
+
+
+ Barcode +
+ +
+ +
+
+ +
+
+
Title:
+
{{display('title')}}
+
+
+
Author:
+
{{display('author')}}
+
+
+
Call Number:
+
{{item.call_number().label()}}
+
+
+
+ + +
+
+
+ +
+
+
+ No item with barcode "{{itemBarcode}}". +
+
+
+ +
+
+
+ No circulation found for item with barcode {{itemBarcode}}. + Item not modified. +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+
+ +
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/item/missing-pieces.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/item/missing-pieces.component.ts new file mode 100644 index 0000000000..638db28de8 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/item/missing-pieces.component.ts @@ -0,0 +1,164 @@ +import {Component, Input, AfterViewInit, ViewChild, Renderer2} 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'; + +@Component({ + templateUrl: 'missing-pieces.component.html' +}) +export class MarkItemMissingPiecesComponent implements AfterViewInit { + + itemId: number; + itemBarcode: string; + item: IdlObject; + letter: string; + circNotFound = false; + processing = false; + noSuchItem = false; + + @ViewChild('penaltyDialog', {static: false}) + penaltyDialog: PatronPenaltyDialogComponent; + + constructor( + private route: ActivatedRoute, + private renderer: Renderer2, + private net: NetService, + private printer: PrintService, + private pcrud: PcrudService, + private auth: AuthService, + private evt: EventService, + private holdings: HoldingsService + ) { + this.itemId = +this.route.snapshot.paramMap.get('id'); + } + + ngAfterViewInit() { + if (this.itemId) { this.getItemById(); } + this.renderer.selectRootElement('#item-barcode-input').focus(); + } + + getItemByBarcode(): Promise { + this.itemId = null; + this.item = null; + + if (!this.itemBarcode) { return Promise.resolve(); } + + return this.holdings.getItemIdFromBarcode(this.itemBarcode) + .then(id => { + this.noSuchItem = (id === null); + this.itemId = id; + return this.getItemById(); + }); + } + + selectInput() { + setTimeout(() => + this.renderer.selectRootElement('#item-barcode-input').select()); + } + + getItemById(): Promise { + this.circNotFound = false; + + if (!this.itemId) { + this.selectInput(); + return Promise.resolve(); + } + + const flesh = { + flesh: 3, + flesh_fields: { + acp: ['call_number'], + acn: ['record'], + bre: ['flat_display_entries'] + } + }; + + return this.pcrud.retrieve('acp', this.itemId, flesh) + .toPromise().then(item => { + this.item = item; + this.itemId = item.id(); + this.itemBarcode = item.barcode(); + this.selectInput(); + }); + } + + display(field: string): string { + if (!this.item) { return ''; } + + const entry = this.item.call_number().record() + .flat_display_entries() + .filter(fde => fde.name() === field)[0]; + + return entry ? entry.value() : ''; + } + + reset() { + this.item = null; + this.itemId = null; + this.itemBarcode = null; + this.circNotFound = false; + } + + processItem() { + this.circNotFound = false; + + if (!this.item) { return; } + + this.processing = true; + + this.net.request( + 'open-ils.circ', + 'open-ils.circ.mark_item_missing_pieces', + this.auth.token(), this.itemId + ).subscribe(resp => { + const evt = this.evt.parse(resp); // always returns event + this.processing = false; + + if (evt.textcode === 'ACTION_CIRCULATION_NOT_FOUND') { + this.circNotFound = true; + return; + } + + const payload = evt.payload; + + if (payload.letter) { + this.letter = payload.letter.template_output().data(); + } + + if (payload.slip) { + this.printer.print({ + printContext: 'default', + contentType: 'text/html', + text: payload.slip.template_output().data() + }); + } + + if (payload.circ) { + this.penaltyDialog.patronId = payload.circ.usr(); + this.penaltyDialog.open().subscribe( + penId => console.debug('Applied penalty ', penId)); + } + }); + } + + printLetter() { + this.printer.print({ + printContext: 'default', + contentType: 'text/plain', + text: this.letter + }); + } + + letterRowCount(): number { + return this.letter ? this.letter.split(/\n/).length + 2 : 20; + } +} + + + 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 new file mode 100644 index 0000000000..b3e775957b --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/item/routing.module.ts @@ -0,0 +1,20 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {MarkItemMissingPiecesComponent} from './missing-pieces.component'; + +const routes: Routes = [{ + path: 'missing_pieces', + component: MarkItemMissingPiecesComponent + }, { + path: 'missing_pieces/:id', + component: MarkItemMissingPiecesComponent +}]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [] +}) + +export class ItemRoutingModule {} + diff --git a/Open-ILS/src/eg2/src/app/staff/cat/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/cat/routing.module.ts index 67cf5c6fd3..084c01fe20 100644 --- a/Open-ILS/src/eg2/src/app/staff/cat/routing.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/cat/routing.module.ts @@ -14,6 +14,9 @@ const routes: Routes = [ path: 'marcbatch', loadChildren: () => import('./marcbatch/marcbatch.module').then(m => m.MarcBatchModule) + }, { + path: 'item', + loadChildren: () => import('./item/item.module').then(m => m.ItemModule) }, { path: 'bib-from/:identType', component: BibByIdentComponent diff --git a/Open-ILS/src/eg2/src/app/staff/nav.component.html b/Open-ILS/src/eg2/src/app/staff/nav.component.html index eead26d357..7215c628bc 100644 --- a/Open-ILS/src/eg2/src/app/staff/nav.component.html +++ b/Open-ILS/src/eg2/src/app/staff/nav.component.html @@ -125,7 +125,7 @@ Item Status - + Scan Item as Missing Pieces diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html index 9a93b3e14d..647cb0cf41 100644 --- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html +++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html @@ -413,6 +413,14 @@ +
+

Add Patron Penalty

+ + +
+

Grid Stock Selector Display and Filtering

console.log('penalty value', val)); + } } diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.module.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.module.ts index 15be7f31d5..57e0bd0474 100644 --- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.module.ts @@ -8,6 +8,7 @@ import {ReactiveFormsModule} from '@angular/forms'; import {SampleDataService} from '@eg/share/util/sample-data.service'; import {OrgFamilySelectModule} from '@eg/share/org-family-select/org-family-select.module'; import {ItemLocationSelectModule} from '@eg/share/item-location-select/item-location-select.module'; +import {PatronModule} from '@eg/staff/share/patron/patron.module'; @NgModule({ declarations: [ @@ -20,7 +21,8 @@ import {ItemLocationSelectModule} from '@eg/share/item-location-select/item-loca OrgFamilySelectModule, ItemLocationSelectModule, SandboxRoutingModule, - ReactiveFormsModule + ReactiveFormsModule, + PatronModule ], providers: [ SampleDataService diff --git a/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.service.ts b/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.service.ts index f61d3d4192..0f2070b0a3 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.service.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.service.ts @@ -4,7 +4,9 @@ import {Injectable, EventEmitter} from '@angular/core'; import {NetService} from '@eg/core/net.service'; import {AnonCacheService} from '@eg/share/util/anon-cache.service'; +import {PcrudService} from '@eg/core/pcrud.service'; import {AuthService} from '@eg/core/auth.service'; +import {IdlObject} from '@eg/core/idl.service'; import {EventService} from '@eg/core/event.service'; interface NewCallNumData { @@ -20,6 +22,7 @@ export class HoldingsService { constructor( private net: NetService, private auth: AuthService, + private pcrud: PcrudService, private evt: EventService, private anonCache: AnonCacheService ) {} @@ -59,5 +62,22 @@ export class HoldingsService { }); }); } + + // Using open-ils.actor.get_barcodes + getItemIdFromBarcode(barcode: string): Promise { + return this.net.request( + 'open-ils.actor', + 'open-ils.actor.get_barcodes', + this.auth.token(), this.auth.user().ws_ou(), 'asset', barcode + ).toPromise().then(resp => { + if (this.evt.parse(resp)) { + return Promise.reject(resp); + } else if (resp.length === 0) { + return null; + } else { + return resp[0].id; + } + }); + } } diff --git a/Open-ILS/src/eg2/src/app/staff/share/patron/patron.module.ts b/Open-ILS/src/eg2/src/app/staff/share/patron/patron.module.ts index ac6e9b30f4..9987c64a86 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/patron/patron.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/patron/patron.module.ts @@ -5,12 +5,14 @@ import {PatronService} from './patron.service'; import {PatronSearchComponent} from './search.component'; import {PatronSearchDialogComponent} from './search-dialog.component'; import {ProfileSelectComponent} from './profile-select.component'; +import {PatronPenaltyDialogComponent} from './penalty-dialog.component'; @NgModule({ declarations: [ PatronSearchComponent, PatronSearchDialogComponent, - ProfileSelectComponent + ProfileSelectComponent, + PatronPenaltyDialogComponent ], imports: [ StaffCommonModule, @@ -19,7 +21,8 @@ import {ProfileSelectComponent} from './profile-select.component'; exports: [ PatronSearchComponent, PatronSearchDialogComponent, - ProfileSelectComponent + ProfileSelectComponent, + PatronPenaltyDialogComponent ], providers: [ PatronService diff --git a/Open-ILS/src/eg2/src/app/staff/share/patron/penalty-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/patron/penalty-dialog.component.html new file mode 100644 index 0000000000..b66e562b85 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/patron/penalty-dialog.component.html @@ -0,0 +1,59 @@ + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/share/patron/penalty-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/patron/penalty-dialog.component.ts new file mode 100644 index 0000000000..12cdb5f559 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/patron/penalty-dialog.component.ts @@ -0,0 +1,130 @@ +import {Component, OnInit, Input, Output, ViewChild} from '@angular/core'; +import {merge, from, Observable} from 'rxjs'; +import {tap, take, switchMap} from 'rxjs/operators'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {OrgService} from '@eg/core/org.service'; +import {AuthService} from '@eg/core/auth.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 {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {DialogComponent} from '@eg/share/dialog/dialog.component'; +import {StringComponent} from '@eg/share/string/string.component'; + + +/** + * Dialog container for patron penalty/message application + * + * + * + */ + +@Component({ + selector: 'eg-patron-penalty-dialog', + templateUrl: 'penalty-dialog.component.html' +}) + +export class PatronPenaltyDialogComponent + extends DialogComponent implements OnInit { + + @Input() patronId: number; + @Input() penaltyNote = ''; + + ALERT_NOTE = 20; + SILENT_NOTE = 21; + STAFF_CHR = 25; + + staffInitials: string; + penaltyTypes: IdlObject[]; + penaltyTypeFromSelect = ''; + penaltyTypeFromButton; + patron: IdlObject; + dataLoaded = false; + requireInitials = false; + initials: string; + noteText = ''; + + @ViewChild('successMsg', {static: false}) successMsg: StringComponent; + @ViewChild('errorMsg', {static: false}) errorMsg: StringComponent; + + constructor( + private modal: NgbModal, + private idl: IdlService, + private org: OrgService, + private net: NetService, + private evt: EventService, + private toast: ToastService, + private auth: AuthService, + private pcrud: PcrudService) { + super(modal); + } + + ngOnInit() { + this.onOpen$.subscribe(_ => + this.init().subscribe(__ => this.dataLoaded = true)); + } + + init(): Observable { + this.dataLoaded = false; + + this.penaltyTypeFromButton = this.SILENT_NOTE; + + this.org.settings(['ui.staff.require_initials.patron_standing_penalty']) + .then(sets => this.requireInitials = + sets['ui.staff.require_initials.patron_standing_penalty']); + + const obs1 = this.pcrud.retrieve('au', this.patronId) + .pipe(tap(usr => this.patron = usr)); + + if (this.penaltyTypes) { return obs1; } + + return obs1.pipe(switchMap(_ => { + return this.pcrud.search('csp', {id: {'>': 100}}, {}, {atomic: true}) + + .pipe(tap(ptypes => { + this.penaltyTypes = + ptypes.sort((a, b) => a.label() < b.label() ? -1 : 1); + })); + })); + } + + apply() { + + const pen = this.idl.create('ausp'); + pen.usr(this.patronId); + pen.org_unit(this.auth.user().ws_ou()); + pen.set_date('now'); + pen.staff(this.auth.user().id()); + + pen.note(this.initials ? + `${this.noteText} [${this.initials}]` : this.noteText); + + pen.standing_penalty( + this.penaltyTypeFromSelect || this.penaltyTypeFromButton); + + this.net.request( + 'open-ils.actor', + 'open-ils.actor.user.penalty.apply', + this.auth.token(), pen + ).subscribe(resp => { + const e = this.evt.parse(resp); + if (e) { + this.errorMsg.current().then(msg => this.toast.danger(msg)); + this.error(e, true); + } else { + // resp == penalty ID on success + this.successMsg.current().then(msg => this.toast.success(msg)); + this.close(resp); + } + }); + } + + buttonClass(pType: number): string { + return this.penaltyTypeFromButton === pType ? + 'btn-primary' : 'btn-light'; + } +} + + + diff --git a/Open-ILS/src/templates/staff/navbar.tt2 b/Open-ILS/src/templates/staff/navbar.tt2 index ddaa169518..5daf52d401 100644 --- a/Open-ILS/src/templates/staff/navbar.tt2 +++ b/Open-ILS/src/templates/staff/navbar.tt2 @@ -223,7 +223,7 @@
  • - + [% l('Scan Item as Missing Pieces') %] -- 2.43.2