LP1849212: tidying up course reserves UIs
authorKyle Huckins <khuckins@catalyte.io>
Thu, 7 Nov 2019 18:38:04 +0000 (18:38 +0000)
committerGalen Charlton <gmc@equinoxinitiative.org>
Mon, 14 Sep 2020 22:16:04 +0000 (18:16 -0400)
- Remove RowFlair and ClassCallback from Course Reserves
Grid.
- 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.
- Optionally apply temporary Call Number, Circ Modifier, Item
Status, and Shelving Location when associating an item with a
course.
- Reapply original values of the above-mentioned fields when
disassociating an item from a course.
- Move disassociation code into Course Service.
- Automatically disassociate items and return them
to their original state when deleting a course.

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>
Signed-off-by: Michele Morgan <mmorgan@noblenet.org>
Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>

Open-ILS/examples/fm_IDL.xml
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
Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course.service.ts [new file with mode: 0644]
Open-ILS/src/sql/Pg/040.schema.asset.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.schema.course-materials-module.sql

index 146f0a8..4e678b4 100644 (file)
@@ -3169,10 +3169,18 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
             <field reporter:label="Course" name="course" reporter:datatype="link" />
             <field reporter:label="Item" name="item" reporter:datatype="link" />
             <field reporter:label="Item Relationship" name="relationship" reporter:datatype="text" />
+            <field reporter:label="Original Status" name="original_status" reporter:datatype="link" />
+            <field reporter:label="Original Circ Modifier" name="original_circ_modifier" reporter:datatype="link" />
+            <field reporter:label="Original Shelving Location" name="original_location" reporter:datatype="link" />
+            <field reporter:label="Original Callnumber" name="original_callnumber" reporter:datatype="link" />
         </fields>
         <links>
             <link field="course" reltype="has_a" key="id" map="" class="acmc" />
             <link field="item" reltype="has_a" key="id" map="" class="acp" />
+            <link field="original_callnumber" reltype="has_a" key="id" map="" class="acn" />
+            <link field="original_status" reltype="has_a" key="id" map="" class="ccs" />
+            <link field="original_circ_modifier" reltype="has_a" key="code" map="" class="ccm" />
+            <link field="original_location" reltype="has_a" key="id" map="" class="acpl" />
         </links>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
             <actions>
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..488eea7
--- /dev/null
@@ -0,0 +1,160 @@
+<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 mt-3">
+      <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="row justify-content-center mt-3">
+      <div class="col">
+        <h5 i18n>The following fields will be applied to the material added, and reverted once the course is no longer associated with the material.</h5>
+      </div>
+    </div>
+    <div class="row mt-3">
+      <div class="col-md-6">
+        <div class="input-group">
+          <div class="input-group-prepend">
+            <div class="input-group-text">
+              <span i18n>Call Number</span>
+            </div>
+          </div>
+          <input type="text" [(ngModel)]="tempCallNumber"
+            (input)="isModifyingCallNumber = true"/>
+          <div class="input-group-append">
+            <div class="input-group-text">
+              <input type="checkbox" [(ngModel)]="isModifyingCallNumber"
+                aria-label="Checkbox for setting a temporary Call Number" />
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="col-md-6">
+        <div class="input-group">
+          <div class="input-group-prepend">
+            <div class="input-group-text">
+              <span i18n>Circulation Modifier</span>
+            </div>
+          </div>
+          <eg-combobox i18n-placeholder placeholder="Circulation Modifier..."
+            idlClass="ccm" idlField="name" [displayTemplate]="idlClassLabel"
+            [asyncSupportsEmptyTermClick]="true"
+            (onChange)="tempCircMod = $event.id; isModifyingCircMod = true">
+          </eg-combobox>
+          <div class="input-group-append">
+            <div class="input-group-text">
+              <input type="checkbox" [(ngModel)]="isModifyingCircMod"
+                aria-label="Checkbox for setting a temporary Circulation Modifier" />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="row mt-3">
+      <div class="col-md-6">
+        <div class="input-group">
+          <div class="input-group-prepend">
+            <div class="input-group-text">
+              <span i18n>Item Status</span>
+            </div>
+          </div>
+          <eg-combobox i18n-placeholder placeholder="Item Status..."
+            idlClass="ccs" idlField="name" [displayTemplate]="idlClassLabel"
+            [asyncSupportsEmptyTermClick]="true"
+            (onChange)="tempStatus = $event.id; isModifyingStatus = true">
+          </eg-combobox>
+          <div class="input-group-append">
+            <div class="input-group-text">
+              <input type="checkbox" [(ngModel)]="isModifyingStatus"
+                aria-label="Checkbox for setting a temporary Item Status" />
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="col-md-6">
+        <div class="input-group">
+          <div class="input-group-prepend">
+            <div class="input-group-text">
+              <span i18n>Shelving Location</span>
+            </div>
+          </div>
+          <eg-item-location-select permFilter="MANAGE_RESERVES"
+            [(ngModel)]="tempLocation" (oninput)="isModifyingLocation = true">
+          </eg-item-location-select>
+          <div class="input-group-append">
+            <div class="input-group-text">
+              <input type="checkbox" [(ngModel)]="isModifyingLocation"
+                aria-label="Checkbox for setting a temporary Shelving Location" />
+            </div>
+          </div>
+        </div>
+      </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" [hidden]="true" 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="location.name" [hidden]="true" label="Shelving Location" i18n-label></eg-grid-column>
+        <eg-grid-column path="status.name" [hidden]="true" label="Copy Status" 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 #idlClassLabel let-r="result" i18n>
+    {{r.label}}
+  </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..cb8eeeb
--- /dev/null
@@ -0,0 +1,174 @@
+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 {EventService} from '@eg/core/event.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';
+import {CourseService} from './course.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;
+    @Input() tempCallNumber: String;
+    @Input() tempStatus: Number;
+    @Input() tempLocation: Number;
+    @Input() tempCircMod: String;
+    @Input() isModifyingStatus: Boolean;
+    @Input() isModifyingCircMod: Boolean;
+    @Input() isModifyingCallNumber: Boolean;
+    @Input() isModifyingLocation: Boolean;
+    currentCourse: IdlObject;
+    materials: any[];
+    gridDataSource: GridDataSource;
+
+    constructor(
+        private auth: AuthService,
+        private idl: IdlService,
+        private net: NetService,
+        private pcrud: PcrudService,
+        private org: OrgService,
+        private evt: EventService,
+        private modal: NgbModal,
+        private toast: ToastService,
+        private courseSvc: CourseService
+    ) {
+        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(material => {
+            material.isdeleted(true);
+            this.pcrud.autoApply(material).subscribe(
+                val => {
+                    this.courseSvc.resetItemFields(material, this.currentCourse.owning_lib());
+                    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},
+              {flesh: 3, flesh_fields: {acp: ['call_number']}}).subscribe(item => {
+                let material = this.idl.create('acmcm');
+                material.item(item.id());
+                material.course(this.currentCourse.id());
+                if (relationship) material.relationship(relationship);
+                if (this.isModifyingStatus && this.tempStatus) {
+                    material.original_status(item.status());
+                    item.status(this.tempStatus);
+                }
+                if (this.isModifyingLocation && this.tempLocation) {
+                    material.original_location(item.location());
+                    item.location(this.tempLocation);
+                }
+                if (this.isModifyingCircMod) {
+                    material.original_circ_modifier(item.circ_modifier());
+                    item.circ_modifier(this.tempCircMod);
+                    if (!this.tempCircMod) item.circ_modifier(null);
+                }
+                if (this.isModifyingCallNumber) {
+                    material.original_callnumber(item.call_number());
+                }
+                this.pcrud.create(material).subscribe(
+                val => {
+                   console.debug('created: ' + val);
+                   let new_cn = item.call_number().label();
+                   if (this.tempCallNumber) new_cn = this.tempCallNumber;
+                    this.courseSvc.updateItem(item, this.currentCourse.owning_lib(), new_cn, this.isModifyingCallNumber).then(res => {
+                        this.fetchItem(item.id(), relationship);                        
+                        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));
+                        }
+                    });
+
+                    // Cleaning up inputs
+                    this.barcodeInput = "";
+                    this.relationshipInput = "";
+                    this.tempStatus = null;
+                    this.tempCircMod = null;
+                    this.tempCallNumber = null;
+                    this.tempLocation = null;
+                    this.isModifyingCallNumber = false;
+                    this.isModifyingCircMod = false;
+                    this.isModifyingLocation = false;
+                    this.isModifyingStatus = false;
+                }, 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 471a94e..eb01af8 100644 (file)
@@ -7,16 +7,17 @@
 <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"
-    [rowFlairIsEnabled]="true"
-    [rowFlairCallback]="rowFlairCallback"
-    [cellClassCallback]="gridCellClassCallback"
     [sortable]="true">
     <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 9e4bb40..ea5dc2f 100644 (file)
@@ -1,13 +1,20 @@
 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 {CourseService} from './course.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, GridRowFlairEntry} from '@eg/share/grid/grid';
+import {GridDataSource, GridColumn} from '@eg/share/grid/grid';
 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,23 +29,27 @@ export class CourseListComponent implements OnInit {
     @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent;
     @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent;
     @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent;
-    @ViewChild('flairTooltip', { static: true }) private flairTooltip: StringComponent;
-    rowFlairCallback: (row: any) => GridRowFlairEntry;
+    @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 pcrud: PcrudService,
-            private toast: ToastService,
+        private auth: AuthService,
+        private courseSvc: CourseService,
+        private net: NetService,
+        private org: OrgService,
+        private pcrud: PcrudService,
+        private toast: ToastService,
     ){}
 
     ngOnInit() {
         this.getSource();
-        this.rowFlair();
     }
 
     /**
@@ -63,24 +74,6 @@ export class CourseListComponent implements OnInit {
         };
     }
 
-    rowFlair() {
-        this.rowFlairCallback = (row: any): GridRowFlairEntry => {
-            const flair = {icon: null, title: null};
-            if (row.id() < 100) {
-                flair.icon = 'not_interested';
-                flair.title = this.flairTooltip.text;
-            }
-            return flair;
-        };
-    }
-
-    gridCellClassCallback = (row: any, col: GridColumn): string => {
-        if (col.name === 'id' && row.a[0] < 100) {
-            return 'text-danger';
-        }
-        return '';
-    }
-
     showEditDialog(standingPenalty: IdlObject): Promise<any> {
         this.editDialog.mode = 'update';
         this.editDialog.recordId = standingPenalty['id']();
@@ -131,7 +124,11 @@ export class CourseListComponent implements OnInit {
     }
 
     deleteSelected(idl_object: IdlObject[]) {
-            idl_object.forEach(idl_object => idl_object.isdeleted(true));
+        this.courseSvc.disassociateMaterials(idl_object).then(res => {
+            console.log(res);
+            idl_object.forEach(idl_object => {
+                idl_object.isdeleted(true)
+            });
             this.pcrud.autoApply(idl_object).subscribe(
                 val => {
                     console.debug('deleted: ' + val);
@@ -142,8 +139,52 @@ export class CourseListComponent implements OnInit {
                     this.deleteFailedString.current()
                         .then(str => this.toast.danger(str));
                 },
-                ()  => this.grid.reload()
+                () => 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..6f6eed5 100644 (file)
@@ -2,20 +2,25 @@ 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';
-
+import {ItemLocationSelectModule} from '@eg/share/item-location-select/item-location-select.module';
+import {CourseService} from './course.service'
 @NgModule({
   declarations: [
-    CourseListComponent
+    CourseListComponent,
+    CourseAssociateMaterialComponent
   ],
   imports: [
     AdminCommonModule,
     CourseReservesRoutingModule,
+    ItemLocationSelectModule,
     TreeModule
   ],
   exports: [
   ],
   providers: [
+    CourseService
   ]
 })
 
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course.service.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course.service.ts
new file mode 100644 (file)
index 0000000..c8b60cc
--- /dev/null
@@ -0,0 +1,100 @@
+import {AuthService} from '@eg/core/auth.service';
+import {EventService} from '@eg/core/event.service';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+
+export class CourseService {
+
+    constructor(
+        private auth: AuthService,
+        private evt: EventService,
+        private idl: IdlService,
+        private net: NetService,
+        private pcrud: PcrudService
+    ) {}
+
+    disassociateMaterials(courses) {
+        return new Promise((resolve, reject) => {
+            let course_ids = [];
+            let course_library_hash = {};
+            courses.forEach(course => {
+                course_ids.push(course.id());
+                course_library_hash[course.id()] = course.owning_lib();
+            });
+            this.pcrud.search('acmcm', {course: course_ids}).subscribe(material => {
+                material.isdeleted(true);
+                this.resetItemFields(material, course_library_hash[material.course()]);
+                this.pcrud.autoApply(material).subscribe(res => {
+                    console.log(res);
+                }, err => {
+                    reject(err);
+                }, () => {
+                    resolve(material);
+                });
+            }, err => {
+                reject(err)
+            }, () => {
+                resolve(courses);
+            });
+        });
+    }
+
+    resetItemFields(material, course_lib) {
+        this.pcrud.retrieve('acp', material.item(),
+            {flesh: 3, flesh_fields: {acp: ['call_number']}}).subscribe(copy => {
+            if (material.original_status()) {
+                copy.status(material.original_status());
+            }
+            if (copy.circ_modifier() != material.original_circ_modifier()) {
+                copy.circ_modifier(material.original_circ_modifier());
+            }
+            if (material.original_location()) {
+                copy.location(material.original_location());
+            }
+            if (material.original_callnumber()) {
+                this.pcrud.retrieve('acn', material.original_callnumber()).subscribe(cn => {
+                    this.updateItem(copy, course_lib, cn.label(), true);
+                });
+            } else {
+                this.updateItem(copy, course_lib, copy.call_number().label(), false);
+            }
+        });
+    }
+
+    updateItem(item: IdlObject, course_lib, call_number, updatingVolume) {
+        return new Promise((resolve, reject) => {
+            this.pcrud.update(item).subscribe(item_id => {
+                if (updatingVolume) {
+                    let cn = item.call_number();
+                    return this.net.request(
+                        'open-ils.cat', 'open-ils.cat.call_number.find_or_create',
+                        this.auth.token(), call_number, cn.record(),
+                        course_lib, cn.prefix(), cn.suffix(),
+                        cn.label_class()
+                    ).subscribe(res => {
+                        let event = this.evt.parse(res);
+                        if (event) return;
+                        return this.net.request(
+                            'open-ils.cat', 'open-ils.cat.transfer_copies_to_volume',
+                            this.auth.token(), res.acn_id, [item.id()]
+                        ).subscribe(transfered_res => {
+                            console.debug("Copy transferred to volume with code " + transfered_res);
+                        }, err => {
+                            reject(err);
+                        }, () => {
+                            resolve(item);
+                        });
+                    }, err => {
+                        reject(err);
+                    }, () => {
+                        resolve(item);
+                    });
+                } else {
+                    return this.pcrud.update(item);
+                }
+            });
+        });
+    }
+
+}
\ No newline at end of file
index 2d21799..2091a69 100644 (file)
@@ -1124,7 +1124,11 @@ CREATE TABLE asset.course_module_course_materials (
     id              SERIAL PRIMARY KEY,
     course          INT NOT NULL REFERENCES asset.course_module_course (id),
     item            INT NOT NULL REFERENCES asset.copy (id),
-    relationship    TEXT
+    relationship    TEXT,
+    original_location      INT REFERENCES asset.copy_location,
+    original_status        INT REFERENCES config.copy_status,
+    original_circ_modifier INT REFERENCES config.circ_modifier,
+    original_callnumber    INT REFERENCES asset.call_number
 );
 
 CREATE TABLE asset.course_module_non_cat_course_materials (
index 23dd272..8f813f2 100644 (file)
@@ -21,7 +21,11 @@ CREATE TABLE asset.course_module_course_materials (
     id              SERIAL PRIMARY KEY,
     course          INT NOT NULL REFERENCES asset.course_module_course (id),
     item            INT NOT NULL REFERENCES asset.copy (id),
-    relationship    TEXT
+    relationship    TEXT,
+    original_location        INT REFERENCES asset.copy_location,
+    original_status          INT REFERENCES config.copy_status,
+    original_circ_modifier   TEXT REFERENCES config.circ_modifier,
+    original_callnumber      INT REFERENCES asset.call_number
 );
 
 CREATE TABLE asset.course_module_non_cat_course_materials (