LPXXX Angular Volcopy
authorBill Erickson <berickxx@gmail.com>
Wed, 1 Jul 2020 18:32:39 +0000 (14:32 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 1 Jul 2020 18:32:39 +0000 (14:32 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
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/catalog/record/holdings.component.html
Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.ts
Open-ILS/src/eg2/src/app/staff/share/holdings/copy-tags-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/holdings/copy-tags-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.module.ts

index 4245d1e..bede5f5 100644 (file)
     <div class="p-1"><h4 class="font-weight-bold" i18n>Statistics</h4></div>
 
     <div class="border rounded m-1" *ngIf="displayAttr('copy_tags')">
-      <!--
       <eg-copy-tags-dialog #copyTagsDialog></eg-copy-tags-dialog>
-      -->
       <div class="batch-header font-weight-bold p-2" i18n>Add Item Tags</div>
       <div class="p-1">
-        <button class="btn btn-outline-dark" (click)="openCopyAlerts()" i18n>
+        <button class="btn btn-outline-dark" (click)="openCopyTags()" i18n>
           Item Tags
         </button>
       </div>
index e520736..241a25f 100644 (file)
@@ -17,6 +17,8 @@ import {FormatService} from '@eg/core/format.service';
 import {StringComponent} from '@eg/share/string/string.component';
 import {CopyAlertsDialogComponent
     } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
+import {CopyTagsDialogComponent
+    } from '@eg/staff/share/holdings/copy-tags-dialog.component';
 import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
 import {BatchItemAttrComponent} from '@eg/staff/share/holdings/batch-item-attr.component';
 import {FileExportService} from '@eg/share/util/file-export.service';
@@ -67,6 +69,9 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
     @ViewChild('copyAlertsDialog', {static: false})
         private copyAlertsDialog: CopyAlertsDialogComponent;
 
+    @ViewChild('copyTagsDialog', {static: false})
+        private copyTagsDialog: CopyTagsDialogComponent;
+
     @ViewChild('copyTemplateCbox', {static: false})
         copyTemplateCbox: ComboboxComponent;
 
@@ -366,6 +371,29 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
         );
     }
 
+    openCopyTags() {
+        this.copyTagsDialog.mode = 'create';
+        this.copyTagsDialog.inPlaceMode = true;
+
+        this.copyTagsDialog.open({size: 'lg'}).subscribe(newTags => {
+            if (!newTags || newTags.length === 0) { return; }
+
+            newTags.forEach(tag => {
+                this.context.copyList().forEach(copy => {
+                    console.log('ADDING TAG ', tag);
+                    /*
+                    const a = this.idl.clone(newTag);
+                    a.isnew(true);
+                    a.copy(copy.id());
+                    if (!copy.copy_alerts()) { copy.copy_alerts([]); }
+                    copy.copy_alerts().push(a);
+                    copy.ischanged(true);
+                    */
+                });
+            });
+        });
+    }
+
     applyTemplate() {
         const entry = this.copyTemplateCbox.selected;
         if (!entry) { return; }
index 86f35cb..15040fd 100644 (file)
@@ -47,6 +47,7 @@
 <eg-mark-damaged-dialog #markDamagedDialog></eg-mark-damaged-dialog>
 <eg-mark-missing-dialog #markMissingDialog></eg-mark-missing-dialog>
 <eg-copy-alerts-dialog #copyAlertsDialog></eg-copy-alerts-dialog>
+<eg-copy-tags-dialog #copyTagsDialog></eg-copy-tags-dialog>
 <eg-replace-barcode-dialog #replaceBarcode></eg-replace-barcode-dialog>
 <eg-delete-holding-dialog #deleteHolding></eg-delete-holding-dialog>
 <eg-bucket-dialog #bucketDialog></eg-bucket-dialog>
     </eg-grid-toolbar-action>
 
     <eg-grid-toolbar-action
+      i18n-group group="Add" i18n-label label="Add/Manage Item Tags"
+      (onClick)="openItemTags($event)">
+    </eg-grid-toolbar-action>
+
+    <eg-grid-toolbar-action
       i18n-group group="Add" i18n-label label="Add Items To Bucket"
       (onClick)="openBucketDialog($event)">
     </eg-grid-toolbar-action>
index ea22f29..320f579 100644 (file)
@@ -23,6 +23,8 @@ import {AnonCacheService} from '@eg/share/util/anon-cache.service';
 import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
 import {CopyAlertsDialogComponent
     } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
+import {CopyTagsDialogComponent
+    } from '@eg/staff/share/holdings/copy-tags-dialog.component';
 import {ReplaceBarcodeDialogComponent
     } from '@eg/staff/share/holdings/replace-barcode-dialog.component';
 import {DeleteHoldingDialogComponent
@@ -106,6 +108,8 @@ export class HoldingsMaintenanceComponent implements OnInit {
         private markMissingDialog: MarkMissingDialogComponent;
     @ViewChild('copyAlertsDialog', { static: true })
         private copyAlertsDialog: CopyAlertsDialogComponent;
+    @ViewChild('copyTagsDialog', {static: false})
+        private copyTagsDialog: CopyTagsDialogComponent;
     @ViewChild('replaceBarcode', { static: true })
         private replaceBarcode: ReplaceBarcodeDialogComponent;
     @ViewChild('deleteHolding', { static: true })
@@ -851,6 +855,20 @@ export class HoldingsMaintenanceComponent implements OnInit {
         );
     }
 
+    openItemTags(rows: HoldingsEntry[], mode: string) {
+        const copyIds = this.selectedCopyIds(rows);
+        if (copyIds.length === 0) { return; }
+
+        this.copyTagsDialog.copyIds = copyIds;
+        this.copyTagsDialog.open({size: 'lg'}).subscribe(
+            modified => {
+                if (modified) {
+                    this.hardRefresh();
+                }
+            }
+        );
+    }
+
     openReplaceBarcodeDialog(rows: HoldingsEntry[]) {
         const ids = this.selectedCopyIds(rows);
         if (ids.length === 0) { return; }
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holdings/copy-tags-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/holdings/copy-tags-dialog.component.html
new file mode 100644 (file)
index 0000000..b2c1ffa
--- /dev/null
@@ -0,0 +1,106 @@
+<eg-string #successMsg text="Successfully Modified Item Tags" i18n-text></eg-string>
+<eg-string #errorMsg text="Failed To Modify Item Tags" i18n-text></eg-string>
+
+<ng-template #dialogContent>
+  <div class="modal-header">
+    <h4 class="modal-title">
+      <ng-container *ngIf="mode == 'create'">
+        <span i18n>Adding tags for {{copies.length}} item(s).</span>
+      </ng-container>
+      <ng-container *ngIf="mode == 'manage'">
+        <span i18n>Managing tags for item {{copies[0].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">
+    <div class="row mt-2 p-2" *ngFor="let tag of newTags">
+      <div class="col-lg-4">
+        {{tag.tag_type()}}
+      </div>
+      <div class="col-lg-5">
+        {{tag.label()}}
+      </div>
+      <div class="col-lg-3">
+        <button class="btn btn-outline-danger" (click)="removeTag(tag)" i18n>
+          Remove
+        </button>
+      </div>
+    </div>
+
+    <div class="row mt-2 p-2 rounded border border-success">
+      <div class="col-lg-4">
+        <eg-combobox [entries]="tagTypes" [(ngModel)]="curTagType"
+          i18n-placeholder placeholder="Select Tag Type...">
+        </eg-combobox>
+      </div>
+      <div class="col-lg-5">
+        <eg-combobox [asyncDataSource]="tagDataSource" [(ngModel)]="curTag"
+          [allowFreeText]="true"
+          i18n-placeholder placeholder="Select Tag Type...">
+        </eg-combobox>
+      </div>
+      <div class="col-lg-3">
+        <div class="pt-2">
+          <button class="btn btn-success" (click)="addNew()" i18n>Add Tag</button>
+        </div>  
+      </div>
+    </div>
+    <ng-container *ngIf="mode == 'manage'">
+      <!-- in manage mode list all of the tags linked to the copy -->
+      <!--
+      <div class="row mt-2" 
+        *ngFor="let tag of copy.copy_tags()">
+        <div class="col-lg-12 pb-2"><hr/></div>
+        <div class="col-lg-4">
+          <eg-combobox [entries]="tagTypes" [startId]="tag.tag_type()"
+            i18n-placeholder placeholder="Tag Type..."
+            [required]="true"
+            (onChange)="tag.tag_type($event ? $event.id : null); tag.ischanged(true)">
+          </eg-combobox>
+          <div class="pl-2 pt-2" i18n>
+            Added: {{tag.create_time() | date:'shortDate'}}
+          </div>
+        </div>
+        <div class="col-lg-5">
+          <textarea class="form-control" rows="2" 
+            i18n-placeholder placeholder="Tag Note..."
+            (ngModelChange)="tag.note($event); tag.ischanged(true)"
+            [ngModel]="tag.note()">
+          </textarea>
+        </div>
+        <div class="col-lg-3">
+          <div class="d-flex flex-column">
+            <div class="form-check">
+              <input class="form-check-input" type="checkbox" 
+                [ngModel]="tag.temp() == 't'" 
+                (ngModelChange)="tag.temp($event ? 't' : 'f'); tag.ischanged(true)"
+                id="tag-temporary-{{tag.id()}}">
+              <label class="form-check-label" for="tag-temporary-{{tag.id()}}" i18n>
+                Temporary?
+              </label>
+            </div>
+            <div class="form-check pt-2">
+              <input class="form-check-input" type="checkbox" 
+                [ngModel]="tag.ack_time() != null" 
+                (ngModelChange)="tag.ack_time($event ? 'now' : null); tag.ischanged(true)"
+                id="tag-temporary-{{tag.id()}}">
+              <label class="form-check-label" for="tag-temporary-{{tag.id()}}" i18n>
+                Clear?
+              </label>
+            </div>
+          </div>
+        </div>
+      </div>
+      -->
+    </ng-container>
+  </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-tags-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/holdings/copy-tags-dialog.component.ts
new file mode 100644 (file)
index 0000000..e008b81
--- /dev/null
@@ -0,0 +1,236 @@
+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';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+/**
+ * Dialog for managing copy tags.
+ */
+
+@Component({
+  selector: 'eg-copy-tags-dialog',
+  templateUrl: 'copy-tags-dialog.component.html'
+})
+
+export class CopyTagsDialogComponent
+    extends DialogComponent implements OnInit {
+
+    static autoId = -1;
+
+    @Input() copyIds: number[] = [];
+
+    // If there are multiple copyIds, only new tags may be applied.
+    // If there is only one copyId, then tags may be applied or removed.
+    mode: string; // create | manage
+
+    // If true, no attempt is made to save the new tags to the
+    // database.  It's assumed this takes place in the calling code.
+    // This is useful for creating tags 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;
+
+    tagTypes: ComboboxEntry[];
+
+    curTag: ComboboxEntry = null;
+    curTagType: ComboboxEntry = null;
+    newTags: IdlObject[] = [];
+    tagMap: {[id: number]: IdlObject} = {};
+
+    tagDataSource: (term: string) => Observable<ComboboxEntry>;
+
+    @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() {
+
+       this.tagDataSource = term => {
+            if (!this.curTagType) { return empty(); }
+
+            return this.pcrud.search(
+                'acpt', {
+                    tag_type: this.curTagType.id,
+                    '-or': [
+                        {value: {'ilike': `%${term}%`}},
+                        {label: {'ilike': `%${term}%`}}
+                    ]
+                },
+                {order_by: {acpt: 'label'}}
+            ).pipe(map(copyTag => {
+                this.tagMap[copyTag.id()] = copyTag;
+                return {id: copyTag.id(), label: copyTag.label()};
+            }));
+        };
+    }
+
+    /**
+     */
+    open(args: NgbModalOptions): Observable<IdlObject[]> {
+        this.copy = null;
+        this.copies = [];
+        this.newTags = [];
+
+        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 tags to multiple copies.
+
+        if (this.copyIds.length === 1) {
+            this.mode = 'manage';
+        } else {
+            this.mode = 'create';
+        }
+
+        // Observify data loading
+        const obs = from(
+            this.getTagTypes()
+            .then(_ => this.getCopies())
+        );
+
+        // Return open() observable to caller
+        return obs.pipe(switchMap(_ => super.open(args)));
+    }
+
+    getTagTypes(): Promise<any> {
+        if (this.tagTypes) { return Promise.resolve(); }
+
+        this.tagTypes = [];
+        return this.pcrud.search('cctt',
+            {owner: this.org.ancestors(this.auth.user().ws_ou(), true)},
+            {order_by: {cctt: 'label'}}
+        ).pipe(tap(tag =>
+            this.tagTypes.push({id: tag.code(), label: tag.label()})
+        )).toPromise();
+    }
+
+    getCopies(): Promise<any> {
+        if (this.inPlaceMode) { return Promise.resolve(); }
+
+        return this.pcrud.search('acp', {id: this.copyIds},
+            {flesh: 1, flesh_fields: {acp: ['tags']}}, {atomic: true})
+        .toPromise().then(copies => {
+            this.copies = copies;
+            if (copies.length === 1) {
+                this.copy = copies[0];
+            }
+        });
+    }
+
+    removeTag(tag: IdlObject) {
+        this.newTags = this.newTags.filter(t => t.id() !== tag.id());
+
+        // TODO: delete existing maps where needed.
+    }
+
+    addNew() {
+        if (!this.curTagType || !this.curTag) { return; }
+
+        let tag;
+
+        if (this.curTag.freetext) {
+            // Create a new tag w/ the provided tag text.
+            tag = this.idl.create('acpt');
+            tag.id(CopyTagsDialogComponent.autoId--);
+            tag.isnew(true);
+            tag.tag_type(this.curTagType.id);
+            tag.label(this.curTag.label);
+            tag.owner(this.auth.user().ws_ou());
+            tag.pub('t');
+        } else {
+            tag = this.tagMap[this.curTag.id];
+        }
+
+        this.newTags.push(tag);
+    }
+
+    createNewTags(): Promise<any> {
+        let promise = Promise.resolve();
+
+        this.newTags.forEach(tag => {
+            if (!tag.isnew()) { return; }
+
+            promise = promise.then(_ => {
+                return this.pcrud.create(tag).toPromise().then(id => {
+                    console.log('create returned ', id);
+                    tag.id(id);
+                });
+            });
+        });
+
+        return promise;
+    }
+
+    applyChanges() {
+
+        if (this.inPlaceMode) {
+            this.close(this.newTags);
+            return;
+        }
+
+        // Create the tags then map them to our copies
+
+        let promise = this.createNewTags();
+
+        this.newTags.forEach(tag => {
+            this.copies.forEach(copy => {
+
+                if (copy.tags() && copy.tags().filter(
+                    t => t.tag_type() === tag.id()).length > 0) {
+                    return; // map already exists
+                }
+
+                promise = promise.then(_ => {
+                    const map = this.idl.create('acptcm');
+                    map.isnew(true);
+                    map.copy(copy.id());
+                    map.tag(tag.id());
+                    return this.pcrud.create(map).toPromise();
+                });
+            })
+        });
+
+        promise.then(_ => {
+            this.successMsg.current().then(msg => this.toast.success(msg));
+            this.close(this.newTags.length > 0);
+        });
+    }
+
+    /*
+    applyChanges() {
+        const tags = this.copy.copy_tags().filter(a => a.ischanged());
+        if (tags.length === 0) { return; }
+        this.pcrud.update(tags).toPromise().then(
+            ok => this.successMsg.current().then(msg => this.toast.success(msg)),
+            err => this.errorMsg.current().then(msg => this.toast.danger(msg))
+        );
+    }
+    */
+}
+
index b33863b..b84a4ab 100644 (file)
@@ -4,6 +4,7 @@ import {HoldingsService} from './holdings.service';
 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 {ReplaceBarcodeDialogComponent} from './replace-barcode-dialog.component';
 import {DeleteHoldingDialogComponent} from './delete-volcopy-dialog.component';
 import {ConjoinedItemsDialogComponent} from './conjoined-items-dialog.component';
@@ -16,6 +17,7 @@ import {BatchItemAttrComponent} from './batch-item-attr.component';
       MarkDamagedDialogComponent,
       MarkMissingDialogComponent,
       CopyAlertsDialogComponent,
+      CopyTagsDialogComponent,
       ReplaceBarcodeDialogComponent,
       DeleteHoldingDialogComponent,
       ConjoinedItemsDialogComponent,
@@ -30,6 +32,7 @@ import {BatchItemAttrComponent} from './batch-item-attr.component';
       MarkDamagedDialogComponent,
       MarkMissingDialogComponent,
       CopyAlertsDialogComponent,
+      CopyTagsDialogComponent,
       ReplaceBarcodeDialogComponent,
       DeleteHoldingDialogComponent,
       ConjoinedItemsDialogComponent,