--- /dev/null
+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 {}
--- /dev/null
+<eg-staff-banner i18n-bannerText bannerText="Mark Item Missing Pieces">
+</eg-staff-banner>
+
+<eg-patron-penalty-dialog #penaltyDialog></eg-patron-penalty-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 class="mt-3 mb-3 p-2" *ngIf="item">
+ <div class="row">
+ <div class="col-lg-2" i18n>Title: </div>
+ <div class="col-lg-10">{{display('title')}}</div>
+ </div>
+ <div class="row">
+ <div class="col-lg-2" i18n>Author: </div>
+ <div class="col-lg-10">{{display('author')}}</div>
+ </div>
+ <div class="row">
+ <div class="col-lg-2" i18n>Call Number: </div>
+ <div class="col-lg-10">{{item.call_number().label()}}</div>
+ </div>
+ <div class="row mt-2">
+ <div class="col-lg-12">
+ <button class="btn btn-success" (click)="processItem()" i18n>
+ Mark Item as Missing Pieces?
+ </button>
+ <button class="btn btn-warning ml-2" (click)="reset()" i18n>
+ Cancel
+ </button>
+ </div>
+ </div>
+</div>
+
+<div class="row m-1" *ngIf="noSuchItem">
+ <div class="col-lg-6 offset-lg-3">
+ <div class="alert alert-warning" i18n>
+ No item with barcode "{{itemBarcode}}".
+ </div>
+ </div>
+</div>
+
+<div class="row m-1" *ngIf="circNotFound">
+ <div class="col-lg-6 offset-lg-3">
+ <div class="alert alert-warning" i18n>
+ No circulation found for item with barcode {{itemBarcode}}.
+ Item not modified.
+ </div>
+ </div>
+</div>
+
+<div class="row m-1" *ngIf="processing">
+ <div class="col-lg-6 offset-lg-3">
+ <eg-progress-inline></eg-progress-inline>
+ </div>
+</div>
+
+<div *ngIf="letter">
+ <div class="row">
+ <div class="col-lg-3">
+ <button class="btn btn-outline-dark" (click)="printLetter()" i18n>
+ Print Letter
+ </button>
+ </div>
+ </div>
+ <div class="row m-1">
+ <div class="col-lg-8">
+ <textarea [(ngModel)]="letter"
+ rows="{{letterRowCount()}}" class="form-control">
+ </textarea>
+ </div>
+ </div>
+</div>
--- /dev/null
+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<any> {
+ 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<any> {
+ 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;
+ }
+}
+
+
+
--- /dev/null
+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 {}
+
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
}
<span class="material-icons" aria-hidden="true">question_answer</span>
<span i18n>Item Status</span>
</a>
- <a class="dropdown-item" href="/eg/staff/cat/item/missing_pieces">
+ <a class="dropdown-item" routerLink="/staff/cat/item/missing_pieces">
<span class="material-icons" aria-hidden="true">grid_on</span>
<span i18n>Scan Item as Missing Pieces</span>
</a>
</div>
<div class="mt-4 mb-4">
+ <h4>Add Patron Penalty</h4>
+ <eg-patron-penalty-dialog #penaltyDialog patronId="1"></eg-patron-penalty-dialog>
+ <button class="btn btn-outline-dark" (click)="openPenalty()" i18n>
+ Open Penalty Dialog
+ </button>
+</div>
+
+<div class="mt-4 mb-4">
<h4>Grid Stock Selector Display and Filtering</h4>
<eg-grid #eventsGrid idlClass="atevdef"
[dataSource]="eventsDataSource"
@ViewChild('bresvEditor', { static: true })
private bresvEditor: FmRecordEditorComponent;
+ @ViewChild('penaltyDialog', {static: false}) penaltyDialog;
+
// @ViewChild('helloStr') private helloStr: StringComponent;
printContext: 'default'
});
}
+
+ openPenalty() {
+ this.penaltyDialog.open()
+ .subscribe(val => console.log('penalty value', val));
+ }
}
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: [
OrgFamilySelectModule,
ItemLocationSelectModule,
SandboxRoutingModule,
- ReactiveFormsModule
+ ReactiveFormsModule,
+ PatronModule
],
providers: [
SampleDataService
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 {
constructor(
private net: NetService,
private auth: AuthService,
+ private pcrud: PcrudService,
private evt: EventService,
private anonCache: AnonCacheService
) {}
});
});
}
+
+ // Using open-ils.actor.get_barcodes
+ getItemIdFromBarcode(barcode: string): Promise<number> {
+ 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;
+ }
+ });
+ }
}
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,
exports: [
PatronSearchComponent,
PatronSearchDialogComponent,
- ProfileSelectComponent
+ ProfileSelectComponent,
+ PatronPenaltyDialogComponent
],
providers: [
PatronService
--- /dev/null
+<eg-string #successMsg i18n-text text="Penalty Successfully Applied"></eg-string>
+<eg-string #errorMsg i18n-text text="Failed To Apply New Penalty"></eg-string>
+
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h4 class="modal-title">
+ <span i18n>Apply Standing Penalty / Message</span>
+ </h4>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="close()">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <div class="row d-flex p-3" *ngIf="dataLoaded">
+ <span i18n>
+ Apply penalty to patron <b>{{patron.family_name()}}, {{patron.first_given_name()}}</b>
+ </span>
+ </div>
+ <div class="row d-flex p-3">
+ <div>
+ <button class="btn mr-1 {{buttonClass(SILENT_NOTE)}}"
+ (click)="penaltyTypeFromButton=SILENT_NOTE" i18n>Note</button>
+ <button class="btn mr-1 {{buttonClass(ALERT_NOTE)}}"
+ (click)="penaltyTypeFromButton=ALERT_NOTE" i18n >Alert</button>
+ <button class="btn mr-1 {{buttonClass(STAFF_CHR)}}"
+ (click)="penaltyTypeFromButton=STAFF_CHR" i18n>Block</button>
+ </div>
+ <div class="flex-1"></div>
+ <div>
+ <select class="form-control"
+ [(ngModel)]="penaltyTypeFromSelect">
+ <option value='' i18n>Penalty Type...</option>
+ <option value="{{pen.id()}}" *ngFor="let pen of penaltyTypes">
+ {{pen.label()}}
+ </option>
+ </select>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-12">
+ <textarea class="form-control" [(ngModel)]="noteText"></textarea>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer flex">
+ <div *ngIf="requireInitials" class="form-validated">
+ <input type="text" class="form-control" size="3" required
+ i18n-placeholder placeholder="Initials..." [(ngModel)]="initials"/>
+ </div>
+ <div class="flex-1"></div>
+ <!-- initials.. disable -->
+ <button type="button" class="btn btn-success"
+ [disabled]="requireInitials && !initials" (click)="apply()" i18n>OK</button>
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Cancel</button>
+ </div>
+</ng-template>
+
--- /dev/null
+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
+ *
+ * <eg-patron-penalty-dialog [patronId]="myPatronId">
+ * </eg-patron-penalty-dialog>
+ */
+
+@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<any> {
+ 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';
+ }
+}
+
+
+
</a>
</li>
<li>
- <a href="./cat/item/missing_pieces" target="_self">
+ <a href="/eg2/staff/cat/item/missing_pieces">
<span class="glyphicon glyphicon-th" aria-hidden="true"></span>
<span>[% l('Scan Item as Missing Pieces') %]</span>
</a>