LP1888723 Item notes dialog and volcopy entry point
authorBill Erickson <berickxx@gmail.com>
Thu, 24 Sep 2020 19:39:45 +0000 (15:39 -0400)
committerGalen Charlton <gmc@equinoxOLI.org>
Sun, 15 Aug 2021 23:55:38 +0000 (19:55 -0400)
Support for adding new item notes to one or more items in the copy
editor interface.

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Ruth Frasur <rfrasur@library.in.gov>
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Open-ILS/src/eg2/src/app/staff/cat/volcopy/config.component.html
Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.html
Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.ts
Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.component.ts
Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.service.ts
Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.ts
Open-ILS/src/eg2/src/app/staff/share/holdings/copy-notes-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/holdings/copy-notes-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.module.ts

index 1fc031a..f6bb854 100644 (file)
         <li class="list-group-item">
           <div class="form-check form-check-inline">
             <input class="form-check-input" type="checkbox" 
+              id="show-copy_notes-attr" 
+              [(ngModel)]="volcopy.defaults.hidden.copy_notes">
+            <label class="form-check-label" for="show-copy_notes-attr" i18n>
+              Add Item Notes
+            </label>
+          </div>
+        </li>
+        <li class="list-group-item">
+          <div class="form-check form-check-inline">
+            <input class="form-check-input" type="checkbox" 
               id="show-statcat_filter-attr" 
               [(ngModel)]="volcopy.defaults.hidden.statcat_filter">
             <label class="form-check-label" for="show-statcat_filter-attr" i18n>
index af6c51e..e7e4bd7 100644 (file)
       </div>
     </div>
 
+    <div class="border rounded m-1" *ngIf="displayAttr('copy_notes')">
+      <eg-copy-notes-dialog #copyNotesDialog></eg-copy-notes-dialog>
+      <div class="batch-header font-weight-bold p-2" i18n>Add Item Notes</div>
+      <div class="p-1">
+        <button class="btn btn-outline-dark" (click)="openCopyNotes()" i18n>
+          Item Notes
+        </button>
+      </div>
+    </div>
+
     <div class="border rounded m-1" *ngIf="displayAttr('statcat_filter')">
       <div class="batch-header font-weight-bold p-2" i18n>Stat Cat Filter</div>
       <div class="p-1">
index fdd8365..ed49aae 100644 (file)
@@ -19,6 +19,8 @@ import {CopyAlertsDialogComponent
     } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
 import {CopyTagsDialogComponent
     } from '@eg/staff/share/holdings/copy-tags-dialog.component';
+import {CopyNotesDialogComponent
+    } from '@eg/staff/share/holdings/copy-notes-dialog.component';
 import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
 import {BatchItemAttrComponent, BatchChangeSelection
     } from '@eg/staff/share/holdings/batch-item-attr.component';
@@ -75,6 +77,9 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
     @ViewChild('copyTagsDialog', {static: false})
         private copyTagsDialog: CopyTagsDialogComponent;
 
+    @ViewChild('copyNotesDialog', {static: false})
+        private copyNotesDialog: CopyNotesDialogComponent;
+
     @ViewChild('copyTemplateCbox', {static: false})
         copyTemplateCbox: ComboboxComponent;
 
@@ -458,6 +463,25 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
         });
     }
 
+    openCopyNotes() {
+        this.copyNotesDialog.inPlaceMode = true;
+        this.copyNotesDialog.copyIds = this.context.copyList().map(c => c.id());
+
+        this.copyNotesDialog.open({size: 'lg'}).subscribe(newNotes => {
+            if (!newNotes || newNotes.length === 0) { return; }
+
+            console.log(newNotes);
+            newNotes.forEach(note => {
+                this.context.copyList().forEach(copy => {
+                    const n = this.idl.clone(note);
+                    n.owning_copy(copy.id());
+                    copy.notes().push(n);
+                    copy.ischanged(true);
+                });
+            });
+        });
+    }
+
     applyTemplate() {
         const entry = this.copyTemplateCbox.selected;
         if (!entry) { return; }
index 1864f76..397cc2f 100644 (file)
@@ -20,7 +20,7 @@ const COPY_FLESH = {
     flesh_fields: {
         acp: [
             'call_number', 'location', 'parts', 'tags',
-            'creator', 'editor', 'stat_cat_entries'
+            'creator', 'editor', 'stat_cat_entries', 'notes'
         ],
         acptcm: ['tag'],
         acpt: ['tag_type']
index 8f3559f..85b16e4 100644 (file)
@@ -234,6 +234,7 @@ export class VolCopyService {
 
         copy.parts([]);
         copy.tags([]);
+        copy.notes([]);
         copy.stat_cat_entries([]);
 
         return copy;
index 4c0564d..c150b73 100644 (file)
@@ -56,7 +56,7 @@ export class BibSummaryComponent implements OnInit {
         .then(_ => {
             if (this.summary) {
                 return this.loadCourseInformation(this.summary.record.id())
-                .then(_ => this.summary.getBibCallNumber());
+                .then(__ => this.summary.getBibCallNumber());
             } else {
                 if (this.recordId) {
                     return this.loadSummary();
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holdings/copy-notes-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/holdings/copy-notes-dialog.component.html
new file mode 100644 (file)
index 0000000..f71b9f5
--- /dev/null
@@ -0,0 +1,77 @@
+<eg-string #successMsg text="Successfully Modified Item Notes" i18n-text></eg-string>
+<eg-string #errorMsg text="Failed To Modify Item Notes" i18n-text></eg-string>
+
+<ng-template #dialogContent>
+  <div class="modal-header">
+    <h4 class="modal-title">
+      <ng-container *ngIf="mode == 'create'">
+        <span i18n>Adding notes for {{copyIds.length}} item(s).</span>
+      </ng-container>
+      <ng-container *ngIf="mode == 'manage'">
+        <span i18n>Managing notes for item {{copy.barcode()}}</span>
+      </ng-container>
+      <span i18n></span>
+    </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 p-4 form-validated">
+
+    <ng-container *ngIf="mode == 'manage' && copy.notes().length">
+      <h4 i18n>Existing Notes</h4>
+      <div class="row mt-2 p-2" *ngFor="let note of copy.notes()">
+        <div class="col-lg-4">{{note.title()}}</div>
+        <div class="col-lg-5">{{note.value()}}</div>
+        <div class="col-lg-3">
+          <button class="btn btn-outline-danger" 
+            (click)="removeNote(note)" i18n>Remove</button>
+        </div>
+      </div>
+      <hr/>
+    </ng-container>
+
+    <h4 i18n>New Notes</h4>
+    <div class="row mt-2 p-2" *ngFor="let note of newNotes">
+      <div class="col-lg-4">{{note.title()}}</div>
+      <div class="col-lg-5">{{note.value()}}</div>
+      <div class="col-lg-3">
+        <button class="btn btn-outline-danger" (click)="removeNote(note)" i18n>
+          Remove
+        </button>
+      </div>
+    </div>
+
+    <div class="row mt-2 p-2 rounded border border-success">
+      <div class="col-lg-12">
+        <div class="row">
+          <div class="col-lg-6">
+            <input type="text" class="form-control" [(ngModel)]="curNoteTitle"
+              i18n-placeholder placeholder="Note title..."/>
+          </div>
+          <div class="col-lg-6">
+            <div class="form-check">
+              <input class="form-check-input" type="checkbox" 
+                [(ngModel)]="curNotePublic" id="pub-check">
+              <label class="form-check-label" for="pub-check">Public Note</label>
+            </div>
+          </div>
+        </div>
+        <div class="row mt-3">
+          <div class="col-lg-9">
+            <textarea class="form-control" [(ngModel)]="curNote"
+              i18n-placeholder placeholder="Enter note value..."></textarea>
+          </div>
+          <div class="col-lg-3">
+            <button class="btn btn-success" (click)="addNew()" i18n>Add Note</button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-secondary" (click)="close()" i18n>Cancel</button>
+    <button class="btn btn-success mr-2" (click)="applyChanges()" i18n>Apply Changes</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holdings/copy-notes-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/holdings/copy-notes-dialog.component.ts
new file mode 100644 (file)
index 0000000..3f9fff5
--- /dev/null
@@ -0,0 +1,172 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Observable, throwError, from, empty} from 'rxjs';
+import {tap, map, switchMap} from 'rxjs/operators';
+import {NetService} from '@eg/core/net.service';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+
+/**
+ * Dialog for managing copy notes.
+ */
+
+@Component({
+  selector: 'eg-copy-notes-dialog',
+  templateUrl: 'copy-notes-dialog.component.html'
+})
+
+export class CopyNotesDialogComponent
+    extends DialogComponent implements OnInit {
+
+    // If there are multiple copyIds, only new notes may be applied.
+    // If there is only one copyId, then notes may be applied or removed.
+    @Input() copyIds: number[] = [];
+
+    mode: string; // create | manage
+
+    // If true, no attempt is made to save the new notes to the
+    // database.  It's assumed this takes place in the calling code.
+    // This is useful for creating notes for new copies.
+    @Input() inPlaceMode = false;
+
+    // In 'create' mode, we may be adding notes to multiple copies.
+    copies: IdlObject[] = [];
+
+    // In 'manage' mode we only handle a single copy.
+    copy: IdlObject;
+
+    curNote: string;
+    curNoteTitle: string;
+    curNotePublic = false;
+    newNotes: IdlObject[] = [];
+    delNotes: IdlObject[] = [];
+
+    autoId = -1;
+
+    @ViewChild('successMsg', { static: true }) private successMsg: StringComponent;
+    @ViewChild('errorMsg', { static: true }) private errorMsg: StringComponent;
+
+    constructor(
+        private modal: NgbModal, // required for passing to parent
+        private toast: ToastService,
+        private net: NetService,
+        private idl: IdlService,
+        private pcrud: PcrudService,
+        private org: OrgService,
+        private auth: AuthService) {
+        super(modal); // required for subclassing
+    }
+
+    ngOnInit() {
+    }
+
+    /**
+     */
+    open(args: NgbModalOptions): Observable<IdlObject[]> {
+        this.copy = null;
+        this.copies = [];
+        this.newNotes = [];
+
+        if (this.copyIds.length === 0 && !this.inPlaceMode) {
+            return throwError('copy ID required');
+        }
+
+        // In manage mode, we can only manage a single copy.
+        // But in create mode, we can add notes to multiple copies.
+
+        if (this.copyIds.length === 1 && !this.inPlaceMode) {
+            this.mode = 'manage';
+        } else {
+            this.mode = 'create';
+        }
+
+        // Observify data loading
+        const obs = from(this.getCopies());
+
+        // Return open() observable to caller
+        return obs.pipe(switchMap(_ => super.open(args)));
+    }
+
+    getCopies(): Promise<any> {
+        if (this.inPlaceMode) { return Promise.resolve(); }
+
+        return this.pcrud.search('acp', {id: this.copyIds},
+            {flesh: 1, flesh_fields: {acp: ['notes']}},
+            {atomic: true}
+        )
+        .toPromise().then(copies => {
+            this.copies = copies;
+            if (copies.length === 1) {
+                this.copy = copies[0];
+            }
+        });
+    }
+
+    removeNote(note: IdlObject) {
+        this.newNotes = this.newNotes.filter(t => t.id() !== note.id());
+
+        if (note.isnew() || this.mode === 'create') { return; }
+
+        const existing = this.copy.notes().filter(n => n.id() === note.id())[0];
+        if (!existing) { return; }
+
+        existing.isdeleted(true);
+        this.delNotes.push(existing);
+
+        // Remove from copy for dialog display
+        this.copy.notes(this.copy.notes().filter(n => n.id() !== note.id()));
+    }
+
+    addNew() {
+        if (!this.curNoteTitle || !this.curNote) { return; }
+
+        const note = this.idl.create('acpn');
+        note.isnew(true);
+        note.creator(this.auth.user().id());
+        note.pub(this.curNotePublic ? 't' : 'f');
+        note.title(this.curNoteTitle);
+        note.value(this.curNote);
+        note.id(this.autoId--);
+
+        this.newNotes.push(note);
+
+        this.curNote = '';
+        this.curNoteTitle = '';
+        this.curNotePublic = false;
+    }
+
+    applyChanges() {
+
+        if (this.inPlaceMode) {
+            this.close(this.newNotes);
+            return;
+        }
+
+        const notes = [];
+        this.newNotes.forEach(note => {
+            this.copies.forEach(copy => {
+                const n = this.idl.clone(note);
+                n.id(null); // remove temp ID, it will be duped
+                n.owning_copy(copy.id());
+                notes.push(n);
+            });
+        });
+
+        this.pcrud.create(notes).toPromise()
+        .then(_ => {
+            if (this.delNotes.length) {
+                return this.pcrud.remove(this.delNotes).toPromise();
+            }
+        }).then(_ => {
+            this.successMsg.current().then(msg => this.toast.success(msg));
+            this.close(this.newNotes.length > 0 || this.delNotes.length > 0);
+        });
+    }
+}
+
index b84a4ab..d8272ba 100644 (file)
@@ -5,6 +5,7 @@ import {MarkDamagedDialogComponent} from './mark-damaged-dialog.component';
 import {MarkMissingDialogComponent} from './mark-missing-dialog.component';
 import {CopyAlertsDialogComponent} from './copy-alerts-dialog.component';
 import {CopyTagsDialogComponent} from './copy-tags-dialog.component';
+import {CopyNotesDialogComponent} from './copy-notes-dialog.component';
 import {ReplaceBarcodeDialogComponent} from './replace-barcode-dialog.component';
 import {DeleteHoldingDialogComponent} from './delete-volcopy-dialog.component';
 import {ConjoinedItemsDialogComponent} from './conjoined-items-dialog.component';
@@ -18,6 +19,7 @@ import {BatchItemAttrComponent} from './batch-item-attr.component';
       MarkMissingDialogComponent,
       CopyAlertsDialogComponent,
       CopyTagsDialogComponent,
+      CopyNotesDialogComponent,
       ReplaceBarcodeDialogComponent,
       DeleteHoldingDialogComponent,
       ConjoinedItemsDialogComponent,
@@ -33,6 +35,7 @@ import {BatchItemAttrComponent} from './batch-item-attr.component';
       MarkMissingDialogComponent,
       CopyAlertsDialogComponent,
       CopyTagsDialogComponent,
+      CopyNotesDialogComponent,
       ReplaceBarcodeDialogComponent,
       DeleteHoldingDialogComponent,
       ConjoinedItemsDialogComponent,