View Course Materials Dialog
authorKyle Huckins <khuckins@catalyte.io>
Wed, 13 Nov 2019 18:23:08 +0000 (18:23 +0000)
committerJane Sandberg <sandbej@linnbenton.edu>
Wed, 26 Aug 2020 17:55:49 +0000 (10:55 -0700)
- Implement dialog to view course materials associated with
a particular course in the course list admin UI.
- Implement actions to associate and disassociate materials with
a specific course.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
new file:   Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-associate-material.component.html
new file:   Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-associate-material.component.ts
modified:   Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-list.component.html
modified:   Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-list.component.ts
modified:   Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-reserves.module.ts

Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-associate-material.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-associate-material.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-list.component.html
Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-list.component.ts
Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-reserves.module.ts

diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-associate-material.component.html b/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-associate-material.component.html
new file mode 100644 (file)
index 0000000..43fdc82
--- /dev/null
@@ -0,0 +1,70 @@
+<eg-string #deleteFailedString i18n-text text="Disassociation of Course Material failed or was not allowed"></eg-string>
+<eg-string #deleteSuccessString i18n-text text="Disassociation of Course Material succeeded"></eg-string>
+<eg-string #successString i18n-text text="Association of Course Material succeeded"></eg-string>
+<eg-string #failedString i18n-text text="Association of Course Material failed or was not allowed"></eg-string>
+<eg-string #differentLibraryString i18n-text text="Material exists at a different library"></eg-string>
+
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>Course Materials</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">
+    <div class="row">
+      <div class="col-md-4">
+        <div class="input-group">
+          <div class="input-group-prepend">
+            <span class="input-group-text" i18n>Barcode</span>
+          </div>
+          <input type="text" [(ngModel)]="barcodeInput" />
+        </div>
+      </div>
+      <div class="col-md-5">
+        <div class="input-group">
+          <div class="input-group-prepend">
+            <span class="input-group-text" i18n>Relationship</span>
+          </div>
+          <input type="text" [(ngModel)]="relationshipInput" placeholder-i18n placeholder="e.g. Required" />
+        </div>
+      </div>
+      <div class="col-md-3">
+        <button class="btn btn-outline-dark" (click)="associateItem(barcodeInput, relationshipInput)" i18n [disabled]="!barcodeInput">Add Material</button>
+      </div>
+    </div>
+    <div class="mt-3">
+      <eg-grid #materialGrid [dataSource]="gridDataSource">
+        <eg-grid-toolbar-action label="Delete Selected" i18n-label (onClick)="deleteSelected($event)">
+        </eg-grid-toolbar-action>
+
+        <eg-grid-column path="id" [index]=true [hidden]="true" label="ID" i18n-label></eg-grid-column>
+        <eg-grid-column label="Barcode" i18n-label name="barcode" [cellTemplate]="barcodeCellTemplate"></eg-grid-column>
+        <eg-grid-column label="Title" i18n-label name="title" [cellTemplate]="titleCellTemplate"></eg-grid-column>
+        <eg-grid-column path="call_number.label" label="Call Number" i18n-label></eg-grid-column>
+        <eg-grid-column path="call_number.prefix.label" [hidden]="true" label="Call Number Prefix" i18n-label hidden></eg-grid-column>
+        <eg-grid-column path="call_number.suffix.label" [hidden]="true" label="Call Number Suffix" i18n-label hidden></eg-grid-column>
+        <eg-grid-column path="circ_modifier" label="Circulation Modifier" i18n-label></eg-grid-column>
+        <eg-grid-column path="circ_lib.shortname" label="Circulation Library" i18n-label></eg-grid-column>
+        <eg-grid-column path="_relationship" label="Relationship" i18n-label></eg-grid-column>
+      </eg-grid>
+    </div>
+  </div>
+  <ng-template #barcodeCellTemplate let-entry="row">
+    <span>
+      <a class="pl-1"
+        href="/eg/staff/cat/item/{{entry.id()}}">
+        {{entry.barcode()}}
+      </a>
+    </span>
+  </ng-template>
+  <ng-template #titleCellTemplate let-entry="row">
+    <span>
+      <a class="pl-1"
+        href="/eg/staff/cat/catalog/record/{{entry.call_number().record()}}">
+        {{entry._title}}
+      </a>
+    </span>
+  </ng-template>
+</ng-template>
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-associate-material.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-associate-material.component.ts
new file mode 100644 (file)
index 0000000..cba573f
--- /dev/null
@@ -0,0 +1,130 @@
+import {Component, Input, ViewChild, OnInit, TemplateRef} from '@angular/core';
+import {Observable, Observer, of} from 'rxjs';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {AuthService} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {Pager} from '@eg/share/util/pager';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+
+@Component({
+    selector: 'eg-course-associate-material-dialog',
+    templateUrl: './course-associate-material.component.html'
+})
+
+export class CourseAssociateMaterialComponent extends DialogComponent {
+
+    @ViewChild('materialsGrid', {static: true}) materialsGrid: GridComponent;
+    @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent;
+    @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent;
+    @ViewChild('successString', { static: true }) successString: StringComponent;
+    @ViewChild('failedString', { static: true }) failedString: StringComponent;
+    @ViewChild('differentLibraryString', { static: true }) differentLibraryString: StringComponent;
+    @Input() table_name = "Course Materials";
+    @Input() barcodeInput: String;
+    @Input() relationshipInput: String;
+    currentCourse: IdlObject;
+    materials: any[];
+    gridDataSource: GridDataSource;
+
+    constructor(
+        private auth: AuthService,
+        private idl: IdlService,
+        private net: NetService,
+        private pcrud: PcrudService,
+        private org: OrgService,
+        private modal: NgbModal,
+        private toast: ToastService
+    ) {
+        super(modal);
+        this.gridDataSource = new GridDataSource();
+    }
+
+    ngOnInit() {
+        this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
+            return this.fetchMaterials(pager);
+        }
+    }
+
+    deleteSelected(items) {
+        let item_ids = [];
+        items.forEach(item => {
+            this.gridDataSource.data.splice(this.gridDataSource.data.indexOf(item, 0), 1);
+            item_ids.push(item.id())
+        });
+        this.pcrud.search('acmcm', {course: this.currentCourse.id(), item: item_ids}).subscribe(res => {
+            res.isdeleted(true);
+            this.pcrud.autoApply(res).subscribe(
+                val => {
+                    console.debug('deleted: ' + val);
+                    this.deleteSuccessString.current().then(str => this.toast.success(str));
+                },
+                err => {
+                    this.deleteFailedString.current()
+                        .then(str => this.toast.danger(str));
+                }
+            );
+        });
+    }
+
+    associateItem(barcode, relationship) {
+        if (barcode) {
+            this.pcrud.search('acp', {barcode: barcode}).subscribe(item => {
+                let material = this.idl.create('acmcm');
+                material.item(item.id());
+                material.course(this.currentCourse.id());
+                if (relationship) material.relationship(relationship);
+                this.pcrud.create(material).subscribe(
+                val => {
+                   console.debug('created: ' + val);
+                if (item.circ_lib() != this.currentCourse.owning_lib()) {
+                    this.differentLibraryString.current().then(str => this.toast.warning(str));
+                } else {
+                    this.successString.current().then(str => this.toast.success(str));
+                }
+                    this.fetchItem(item.id(), relationship);
+                    this.barcodeInput = "";
+                    this.relationshipInput = "";
+                }, err => {
+                    this.failedString.current().then(str => this.toast.danger(str));
+                });
+            });
+        }
+    }
+
+    fetchMaterials(pager: Pager): Observable<any> {
+        return new Observable<any>(observer => {
+            this.materials.forEach(material => {
+                this.fetchItem(material.item, material.relationship);
+            });
+            observer.complete();
+        });
+    }
+
+    fetchItem(itemId, relationship): Promise<any> {
+        return new Promise((resolve, reject) => {
+            this.net.request(
+                'open-ils.circ',
+                'open-ils.circ.copy_details.retrieve',
+                this.auth.token(), itemId
+            ).subscribe(res => {
+                if (res) {
+                    let item = res.copy;
+                    item.call_number(res.volume);
+                    item.circ_lib(this.org.get(item.circ_lib()));
+                    item._title = res.mvr.title();
+                    item._relationship = relationship;
+                    this.gridDataSource.data.push(item);
+                }
+            }, err => {
+                reject(err);
+            }, () => resolve(this.gridDataSource.data));
+        });
+    }
+}
\ No newline at end of file
index 345197c..eb01af8 100644 (file)
@@ -7,6 +7,8 @@
 <eg-string #deleteSuccessString i18n-text text="Delete of {{table_name}} succeeded"></eg-string>
 <eg-string #flairTooltip i18n-text text="Limited Editing"></eg-string>
 
+<eg-course-associate-material-dialog #courseMaterialDialog>
+</eg-course-associate-material-dialog>
 <div class="w-100 mt-2 mb-2">
   <eg-grid #grid idlClass={{idl_class}}
     [dataSource]="grid_source"
@@ -14,6 +16,8 @@
     <eg-grid-toolbar-button
       label="Create {{table_name}}" (onClick)="createNew()" i18n-label>
     </eg-grid-toolbar-button>
+    <eg-grid-toolbar-action label="View Materials" i18n-label (onClick)="openMaterialsDialog($event)">
+    </eg-grid-toolbar-action>
     <eg-grid-toolbar-action label="Edit Selected" i18n-label (onClick)="editSelected($event)">
     </eg-grid-toolbar-action>
     <eg-grid-toolbar-action label="Delete Selected" i18n-label (onClick)="deleteSelected($event)">
index 05fe7c9..e806539 100644 (file)
@@ -1,6 +1,9 @@
 import {Component, Input, ViewChild, OnInit} from '@angular/core';
 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 {OrgService} from '@eg/core/org.service';
 import {GridComponent} from '@eg/share/grid/grid.component';
 import {Pager} from '@eg/share/util/pager';
 import {GridDataSource, GridColumn} from '@eg/share/grid/grid';
@@ -8,6 +11,9 @@ import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
 import {StringComponent} from '@eg/share/string/string.component';
 import {ToastService} from '@eg/share/toast/toast.service';
 
+import {CourseAssociateMaterialComponent
+    } from './course-associate-material.component';
+
 @Component({
     templateUrl: './course-list.component.html'
 })
@@ -22,14 +28,20 @@ export class CourseListComponent implements OnInit {
     @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent;
     @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent;
     @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent;
+    @ViewChild('courseMaterialDialog', {static: true})
+        private courseMaterialDialog: CourseAssociateMaterialComponent;
     @Input() sort_field: string;
     @Input() idl_class = "acmc";
     @Input() dialog_size: 'sm' | 'lg' = 'lg';
     @Input() table_name = "Course";
     grid_source: GridDataSource = new GridDataSource();
+    currentMaterials: any[] = [];
     search_value = '';
 
     constructor(
+            private auth: AuthService,
+            private net: NetService,
+            private org: OrgService,
             private pcrud: PcrudService,
             private toast: ToastService,
     ){}
@@ -124,5 +136,48 @@ export class CourseListComponent implements OnInit {
                 ()  => this.grid.reload()
             );
         };
+
+    fetchCourseMaterials(course, currentMaterials): Promise<any> {
+        return new Promise((resolve, reject) => {
+            this.pcrud.search('acmcm', {course: course}).subscribe(res => {
+                if (res) this.fleshItemDetails(res.item(), res.relationship());
+            }, err => {
+                reject(err);
+            }, () => resolve(this.courseMaterialDialog.gridDataSource.data));
+        });
+    }
+
+    fleshItemDetails(itemId, relationship): Promise<any> {
+        return new Promise((resolve, reject) => {
+            this.net.request(
+                'open-ils.circ',
+                'open-ils.circ.copy_details.retrieve',
+                this.auth.token(), itemId
+            ).subscribe(res => {
+                if (res) {
+                    let item = res.copy;
+                    item.call_number(res.volume);
+                    item._title = res.mvr.title();
+                    item.circ_lib(this.org.get(item.circ_lib()));
+                    item._relationship = relationship;
+                    this.courseMaterialDialog.gridDataSource.data.push(item);
+                }
+            }, err => {
+                reject(err);
+            }, () => resolve(this.courseMaterialDialog.gridDataSource.data));
+        });
+    }
+
+    openMaterialsDialog(course) {
+        let currentMaterials = []
+        this.courseMaterialDialog.gridDataSource.data = [];
+        this.fetchCourseMaterials(course[0].id(), currentMaterials).then(res => {
+            this.courseMaterialDialog.currentCourse = course[0];
+            this.courseMaterialDialog.materials = currentMaterials;
+            this.courseMaterialDialog.open({size: 'lg'}).subscribe(res => {
+                console.log(res);
+            });
+        });
+    }
 }
 
index 1702ba8..0518730 100644 (file)
@@ -2,11 +2,13 @@ import {NgModule} from '@angular/core';
 import {TreeModule} from '@eg/share/tree/tree.module';
 import {AdminCommonModule} from '@eg/staff/admin/common.module';
 import {CourseListComponent} from './course-list.component';
+import {CourseAssociateMaterialComponent} from './course-associate-material.component';
 import {CourseReservesRoutingModule} from './routing.module';
 
 @NgModule({
   declarations: [
-    CourseListComponent
+    CourseListComponent,
+    CourseAssociateMaterialComponent
   ],
   imports: [
     AdminCommonModule,