LP1910145 Angular Hold Detail Notes & Notifications
authorBill Erickson <berickxx@gmail.com>
Mon, 4 Jan 2021 21:12:50 +0000 (16:12 -0500)
committerGalen Charlton <gmc@equinoxOLI.org>
Tue, 8 Jun 2021 15:52:40 +0000 (11:52 -0400)
The hold detail view now displays hold notes and hold notification
records.  Notes and Notifications may be created by staff.  Notes may be
deleted by staff.

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Garry Collum <gcollum@gmail.com>
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Open-ILS/src/eg2/src/app/staff/share/holds/detail.component.html
Open-ILS/src/eg2/src/app/staff/share/holds/detail.component.ts
Open-ILS/src/eg2/src/app/staff/share/holds/holds.module.ts
Open-ILS/src/eg2/src/app/staff/share/holds/note-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/holds/note-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/holds/notify-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/holds/notify-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/styles.css

index bc7e4f6..e80d1ff 100644 (file)
@@ -1,11 +1,11 @@
-
-<eg-staff-banner bannerText="Hold Details (#{{hold.id}})" i18n-bannerText>
-</eg-staff-banner>
+<eg-hold-note-dialog #noteDialog [holdId]="holdId"></eg-hold-note-dialog>
+<eg-hold-notify-dialog #notifyDialog [holdId]="holdId"></eg-hold-notify-dialog>
 
 <div class="row">
-  <div class="col-lg-3">
+  <div class="col-lg-2">
     <button (click)="showListView()" class="btn btn-info" i18n>List View</button>
   </div>
+  <div class="col-lg-3 font-weight-bold" i18n><h4>Hold #{{holdId}}</h4></div>
 </div>
 
 <div class="well-table">
   </div>
 </div>
 
+<div class="row mt-2">
+  <div class="col-lg-12">
+    <ul ngbNav #detailNav="ngbNav" class="nav-tabs" [activeId]="detailTab">
+      <li ngbNavItem="notes">
+        <a ngbNavLink i18n>Notes</a>
+        <ng-template ngbNavContent>
+          <button class="btn btn-outline-dark mt-3" (click)="newNote()" i18n>New Note</button>
+          <div class="mt-3" *ngFor="let note of notes">
+            <div class="d-flex">
+              <div class="font-weight-bold">{{note.title()}}</div>
+              <div class="flex-1"></div>
+              <div>
+                <span *ngIf="note.slip() == 't'" 
+                  class="ml-2 badge badge-info p-1">Print on Slip</span>
+                <span *ngIf="note.pub() == 't'" 
+                  class="ml-2 badge badge-warning p-1">Patron Visible</span>
+                <span *ngIf="note.staff() == 't'" 
+                  class="ml-2 badge badge-info p-1">Staff Create</span>
+              </div>
+            </div>
+            <div class="well-table">
+              <div class="well-row">
+                <div class="well-value">{{note.body()}}</div>
+                <div class="well-label-no-flex">
+                  <button class="btn btn-warning" 
+                    (click)="deleteNote(note)" i18n>Delete</button>
+                </div>
+              </div>
+            </div>
+          </div>
+        </ng-template>
+      </li>
+      <li ngbNavItem="notifications">
+        <a ngbNavLink i18n>Staff Notifications</a>
+        <ng-template ngbNavContent>
+          <button class="btn btn-outline-dark mt-3" 
+            (click)="newNotify()" i18n>Add Record of Notification</button>
+          <div class="mt-3" *ngFor="let notify of notifies">
+            <div class="d-flex">
+              <div class="font-weight-bold">{{notify.method()}}</div>
+              <div class="flex-1"></div>
+              <div>
+                <span>{{notify.notify_time() | date:'short'}}</span>
+                <span class="ml-2" i18n>
+                  Created by {{notify.notify_staff().usrname()}}</span>
+              </div>
+              <div>
+              </div>
+            </div>
+            <div class="well-table">
+              <div class="well-row">
+                <div class="well-value">{{notify.note()}}</div>
+              </div>
+            </div>
+          </div>
+        </ng-template>
+      </li>
+    </ul>
+    <div [ngbNavOutlet]="detailNav"></div>
+  </div>
+</div>
index 67b3801..2f28c24 100644 (file)
@@ -1,8 +1,13 @@
 import {Component, OnInit, Input, Output, ViewChild, EventEmitter} from '@angular/core';
 import {Observable, Observer, of} from 'rxjs';
+import {tap} from 'rxjs/operators';
+import {IdlObject} from '@eg/core/idl.service';
 import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
 import {OrgService} from '@eg/core/org.service';
 import {AuthService} from '@eg/core/auth.service';
+import {HoldNoteDialogComponent} from './note-dialog.component';
+import {HoldNotifyDialogComponent} from './notify-dialog.component';
 
 /** Hold details read-only view */
 
@@ -11,25 +16,42 @@ import {AuthService} from '@eg/core/auth.service';
   templateUrl: 'detail.component.html'
 })
 export class HoldDetailComponent implements OnInit {
+    detailTab = 'notes';
+    notes: IdlObject[] = [];
+    notifies: IdlObject[] = [];
 
-    _holdId: number;
+    private _holdId: number;
     @Input() set holdId(id: number) {
-        this._holdId = id;
-        if (this.initDone) {
-            this.fetchHold();
+        if (this._holdId !== id) {
+            this._holdId = id;
+            if (this.initDone) {
+                this.fetchHold();
+            }
         }
     }
 
+    get holdId(): number {
+        return this._holdId;
+    }
+
     hold: any; // wide hold reference
     @Input() set wideHold(wh: any) {
         this.hold = wh;
     }
 
+    get wideHold(): any {
+        return this.hold;
+    }
+
     initDone: boolean;
     @Output() onShowList: EventEmitter<any>;
 
+    @ViewChild('noteDialog') noteDialog: HoldNoteDialogComponent;
+    @ViewChild('notifyDialog') notifyDialog: HoldNotifyDialogComponent;
+
     constructor(
         private net: NetService,
+        private pcrud: PcrudService,
         private org: OrgService,
         private auth: AuthService,
     ) {
@@ -42,15 +64,38 @@ export class HoldDetailComponent implements OnInit {
     }
 
     fetchHold() {
-        if (!this._holdId) { return; }
+        if (!this.holdId && !this.hold) { return; }
 
-        this.net.request(
-            'open-ils.circ',
-            'open-ils.circ.hold.wide_hash.stream',
-            this.auth.token(), {id: this._holdId}
-        ).subscribe(wideHold => {
+        const promise = this.hold ? Promise.resolve(this.hold) :
+            this.net.request(
+                'open-ils.circ',
+                'open-ils.circ.hold.wide_hash.stream',
+                this.auth.token(), {id: this.holdId}
+            ).toPromise();
+
+        return promise.then(wideHold => {
             this.hold = wideHold;
-        });
+            // avoid this.holdId = since it re-fires this fetch.
+            this._holdId = wideHold.id;
+        })
+        .then(_ => this.getNotes())
+        .then(_ => this.getNotifies());
+    }
+
+    getNotes(): Promise<any> {
+        this.notes = [];
+        return this.pcrud.search('ahrn', {hold: this.holdId})
+        .pipe(tap(note => this.notes.push(note))).toPromise();
+    }
+
+    getNotifies(): Promise<any> {
+        this.notifies = [];
+
+        return this.pcrud.search('ahn', {hold: this.holdId}, {
+            flesh: 1,
+            flesh_fields: {ahn: ['notify_staff']},
+            order_by: {ahn: 'notify_time DESC'}
+        }).pipe(tap(notify => this.notifies.push(notify))).toPromise();
     }
 
     getOrgName(id: number) {
@@ -62,6 +107,19 @@ export class HoldDetailComponent implements OnInit {
     showListView() {
         this.onShowList.emit();
     }
+
+    deleteNote(note: IdlObject) {
+        this.pcrud.remove(note).toPromise()
+        .then(ok => { if (ok) { this.getNotes(); } });
+    }
+
+    newNote() {
+        this.noteDialog.open().subscribe(note => this.notes.unshift(note));
+    }
+
+    newNotify() {
+        this.notifyDialog.open().subscribe(notify => this.getNotifies()); // fleshing
+    }
 }
 
 
index 5bcb68a..b5b4bb2 100644 (file)
@@ -9,6 +9,8 @@ import {HoldRetargetDialogComponent} from './retarget-dialog.component';
 import {HoldTransferDialogComponent} from './transfer-dialog.component';
 import {HoldCancelDialogComponent} from './cancel-dialog.component';
 import {HoldManageDialogComponent} from './manage-dialog.component';
+import {HoldNoteDialogComponent} from './note-dialog.component';
+import {HoldNotifyDialogComponent} from './notify-dialog.component';
 
 @NgModule({
     declarations: [
@@ -18,7 +20,9 @@ import {HoldManageDialogComponent} from './manage-dialog.component';
         HoldRetargetDialogComponent,
         HoldTransferDialogComponent,
         HoldCancelDialogComponent,
-        HoldManageDialogComponent
+        HoldManageDialogComponent,
+        HoldNoteDialogComponent,
+        HoldNotifyDialogComponent
     ],
     imports: [
         StaffCommonModule,
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/note-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/holds/note-dialog.component.html
new file mode 100644 (file)
index 0000000..fa3ed9e
--- /dev/null
@@ -0,0 +1,37 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title">Add Hold Note for Hold #{{holdId}}</h4>
+    <button type="button" class="close"
+      i18n-aria-label aria-label="Close" (click)="close()">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body form-validated">
+    <div class="form-group form-check">
+      <input type="checkbox" class="form-check-input" 
+        id="patron-visible-cbox" [(ngModel)]="pub">
+      <label class="form-check-label" 
+        for="patron-visible-cbox" i18n>Patron Visible?</label>
+    </div>
+    <div class="form-group form-check">
+      <input type="checkbox" class="form-check-input" 
+        id="hold-slip-cbox" [(ngModel)]="slip">
+      <label class="form-check-label" 
+        for="hold-slip-cbox" i18n>Print on Slip?</label>
+    </div>
+    <div class="form-group">
+      <label for="title-input" i18n>Note Title</label>
+      <input type="text" class="form-control" id="title-input" required [(ngModel)]="title"/>
+    </div>
+    <div class="form-group">
+      <label for="body-input" i18n>Note Body</label>
+      <textarea class="form-control" id="body-input" required [(ngModel)]="body">
+      </textarea>
+    </div>
+    <div class="w-100">
+      <button class="btn btn-success ml-auto" (click)="createNote()"
+        [disabled]="!title || !body" i18n>Create Note</button>
+      <button class="btn btn-warning ml-2" (click)="close()" i18n>Cancel</button>
+    </div>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/note-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/holds/note-dialog.component.ts
new file mode 100644 (file)
index 0000000..935dd5d
--- /dev/null
@@ -0,0 +1,48 @@
+import {Component, OnInit, Input, Output, ViewChild, EventEmitter} from '@angular/core';
+import {Observable, Observer, of} from 'rxjs';
+import {tap} from 'rxjs/operators';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+
+/** New hold note dialog */
+
+@Component({
+  selector: 'eg-hold-note-dialog',
+  templateUrl: 'note-dialog.component.html'
+})
+export class HoldNoteDialogComponent extends DialogComponent {
+    pub = false;
+    slip = false;
+    title: string;
+    body: string;
+
+    @Input() holdId: number;
+
+    constructor(
+        private modal: NgbModal,
+        private idl: IdlService,
+        private pcrud: PcrudService
+    ) { super(modal); }
+
+    createNote() {
+        const note = this.idl.create('ahrn');
+        note.staff('t');
+        note.hold(this.holdId);
+        note.title(this.title);
+        note.body(this.body);
+        note.slip(this.slip ? 't' : 'f');
+        note.pub(this.pub ? 't' : 'f');
+
+        this.pcrud.create(note).toPromise().then(
+            resp => this.close(resp), // new note object
+            err => console.error('Could not create note', err)
+        );
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/notify-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/holds/notify-dialog.component.html
new file mode 100644 (file)
index 0000000..5d8e0b0
--- /dev/null
@@ -0,0 +1,25 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title">Create Record of Hold Notification</h4>
+    <button type="button" class="close"
+      i18n-aria-label aria-label="Close" (click)="close()">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body form-validated">
+    <div class="form-group">
+      <label for="method-input" i18n>Notification Method</label>
+      <input type="text" class="form-control" id="method-input" required [(ngModel)]="method"/>
+    </div>
+    <div class="form-group">
+      <label for="note-input" i18n>Note</label>
+      <textarea class="form-control" id="note-input" required [(ngModel)]="note">
+      </textarea>
+    </div>
+    <div class="w-100">
+      <button class="btn btn-success ml-auto" (click)="createNotify()"
+        [disabled]="!method || !note" i18n>Create</button>
+      <button class="btn btn-warning ml-2" (click)="close()" i18n>Cancel</button>
+    </div>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holds/notify-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/holds/notify-dialog.component.ts
new file mode 100644 (file)
index 0000000..06baa34
--- /dev/null
@@ -0,0 +1,45 @@
+import {Component, OnInit, Input, Output, ViewChild, EventEmitter} from '@angular/core';
+import {Observable, Observer, of} from 'rxjs';
+import {tap} from 'rxjs/operators';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+
+/** New hold notify dialog */
+
+@Component({
+  selector: 'eg-hold-notify-dialog',
+  templateUrl: 'notify-dialog.component.html'
+})
+export class HoldNotifyDialogComponent extends DialogComponent {
+    method: string;
+    note: string;
+
+    @Input() holdId: number;
+
+    constructor(
+        private modal: NgbModal,
+        private idl: IdlService,
+        private auth: AuthService,
+        private pcrud: PcrudService
+    ) { super(modal); }
+
+    createNotify() {
+        const notify = this.idl.create('ahn');
+        notify.hold(this.holdId);
+        notify.notify_staff(this.auth.user().id());
+        notify.method(this.method);
+        notify.note(this.note);
+
+        this.pcrud.create(notify).toPromise().then(
+            resp => this.close(resp), // new notify object
+            err => console.error('Could not create notify', err)
+        );
+    }
+}
+
+
index 9ec9f67..5ff572a 100644 (file)
@@ -63,6 +63,14 @@ h5 {font-size: .95rem}
   min-height: 40px;
 }
 
+.well-table .well-label-no-flex {
+  display: flex;
+  align-items: center;
+  margin: 4px;
+  padding: 4px;
+  min-height: 40px;
+}
+
 .well-table .well-value {
   flex: 1;
   display: flex;