- Add DB and IDL structure for Academic Departments, Department Contacts, and Course/Department Map
- Add Departments to Course List
- Add Edit page for Departments similar to Course Edit Page, including Department Contacts Tab
Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
Changes to be committed:
modified: Open-ILS/examples/fm_IDL.xml
new file: Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list-department.component.html
new file: Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list-department.component.ts
modified: Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list.component.html
modified: Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list.component.ts
modified: Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-reserves.module.ts
new file: Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-contact-grid.component.html
new file: Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-contact-grid.component.ts
new file: Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-page.component.html
new file: Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-page.component.ts
modified: Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/routing.module.ts
modified: Open-ILS/src/eg2/src/app/staff/share/course.service.ts
modified: Open-ILS/src/sql/Pg/040.schema.asset.sql
modified: Open-ILS/src/sql/Pg/950.data.seed-values.sql
new file: Open-ILS/src/sql/Pg/upgrade/xxxx.schema.lp1922388-academic-departments.sql
<field reporter:label="Course Number" name="course_number" reporter:datatype="text" oils_obj:required="true" />
<field reporter:label="Section Number" name="section_number" reporter:datatype="text" />
<field reporter:label="Owning Library" name="owning_lib" reporter:datatype="link" oils_obj:required="true" />
+ <field reporter:label="Department" name="department" reporter:datatype="link" oils_obj:required="true" />
<field reporter:label="Course Members" name="members" oils_persist:virtual="true" reporter:datatype="link" />
<field reporter:label="Course Materials" name="materials" oils_persist:virtual="true" reporter:datatype="link" />
<field reporter:label="Is Archived?" name="is_archived" reporter:datatype="bool" />
<field reporter:label="Terms Taught" name="terms_map" oils_persist:virtual="true" reporter:datatype="link" config_field="true" />
+ <field reporter:label="Departments" name="departments_map" oils_persist:virtual="true" reporter:datatype="link" config_field="true" />
</fields>
<links>
<link field="owning_lib" reltype="has_a" key="id" map="" class="aou" />
+ <link field="department" reltype="has_a" key="id" map="" class="acmd" />
<link field="members" reltype="has_many" key="course" map="" class="acmcu" />
<link field="materials" reltype="has_many" key="course" map="" class="acmcm" />
<link field="terms_map" reltype="has_many" key="course" map="" class="acmtcm" />
+ <link field="departments_map" reltype="has_many" key="course" map="" class="acmtcm" />
</links>
<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
<actions>
</actions>
</permacrud>
</class>
+ <class id="acmd" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::course_module_department" oils_persist:tablename="asset.course_module_department" reporter:label="Academic Department">
+ <fields oils_persist:primary="id" oils_persist:sequence="asset.course_module_department_id_seq">
+ <field reporter:label="ID" name="id" reporter:datatype="id" />
+ <field reporter:label="Name" name="name" reporter:datatype="text" oils_obj:required="true" />
+ <field reporter:label="Owning Library" name="owning_lib" reporter:datatype="link" oils_obj:required="true" />
+ <field reporter:label="Members" name="members" oils_persist:virtual="true" reporter:datatype="link" />
+ <field reporter:label="Courses" name="courses" reporter:datatype="link" oils_persist:virtual="true"/>
+ <field reporter:label="Course Maps" name="maps" reporter:datatype="link" oils_persist:virtual="true"/>
+ </fields>
+ <links>
+ <link field="owning_lib" reltype="has_a" key="id" map="" class="aou" />
+ <link field="members" reltype="has_many" key="course" map="" class="acmc" />
+ <link field="materials" reltype="has_many" key="course" map="" class="acmcm" />
+ <link field="terms_map" reltype="has_many" key="course" map="" class="acmtcm" />
+ <link field="courses" reltype="has_many" key="department" map="course" class="acmtcm"/>
+ <link field="maps" reltype="has_many" key="department" map="" class="acmtcm"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="MANAGE_RESERVES" context_field="owning_lib"/>
+ <retrieve/>
+ <update permission="MANAGE_RESERVES" context_field="owning_lib"/>
+ <delete permission="MANAGE_RESERVES" context_field="owning_lib"/>
+ </actions>
+ </permacrud>
+ </class>
+ <class id="acmdc" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::course_module_department_contacts" oils_persist:tablename="asset.course_module_department_contacts" reporter:label="Department Contacts">
+ <fields oils_persist:primary="id" oils_persist:sequence="asset.course_module_department_contacts_id_seq">
+ <field reporter:label="ID" name="id" reporter:datatype="id" />
+ <field reporter:label="Department" name="department" reporter:datatype="link" />
+ <field reporter:label="User" name="usr" reporter:datatype="link" />
+ <field reporter:label="User Role" name="usr_role" reporter:datatype="link" />
+ </fields>
+ <links>
+ <link field="department" reltype="has_a" key="id" map="" class="acmd" />
+ <link field="usr" reltype="has_a" key="id" map="" class="au" />
+ <link field="usr_role" reltype="has_a" key="id" map="" class="acmr" />
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="MANAGE_RESERVES">
+ <context link="department" field="owning_lib" />
+ </create>
+ <retrieve/>
+ <update permission="MANAGE_RESERVES">
+ <context link="department" field="owning_lib" />
+ </update>
+ <delete permission="MANAGE_RESERVES">
+ <context link="department" field="owning_lib" />
+ </delete>
+ </actions>
+ </permacrud>
+ </class>
+ <class id="acmdcm" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::course_module_department_course_map" oils_persist:tablename="asset.course_module_department_course_map" reporter:label="Course Department Map" oils_persist:field_safe="true">
+ <fields oils_persist:primary="id" oils_persist:sequence="asset.course_module_department_course_map_id_seq">
+ <field reporter:label="Course Term Map ID" name="id" reporter:datatype="id"/>
+ <field reporter:label="Department" name="department" oils_obj:required="true" reporter:datatype="link"/>
+ <field reporter:label="Course" name="course" reporter:datatype="link" oils_obj:required="true"/>
+ </fields>
+ <links>
+ <link field="department" reltype="has_a" key="id" map="" class="acmd"/>
+ <link field="course" reltype="has_a" key="id" map="" class="acmc"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="MANAGE_RESERVES">
+ <context link="course" field="owning_lib" />
+ </create>
+ <retrieve/>
+ <update permission="MANAGE_RESERVES">
+ <context link="course" field="owning_lib" />
+ </update>
+ <delete permission="MANAGE_RESERVES">
+ <context link="course" field="owning_lib" />
+ </delete>
+ </actions>
+ </permacrud>
+ </class>
<class id="acnc" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::call_number_class" oils_persist:tablename="asset.call_number_class" reporter:label="Call number classification scheme">
<fields oils_persist:primary="id" oils_persist:sequence="asset.call_number_class_id_seq">
--- /dev/null
+<ng-container>\r
+ <div class="row">\r
+ <div class="col-lg-6">\r
+ <eg-org-family-select\r
+ [limitPerms]="['MANAGE_RESERVES']"\r
+ [selectedOrgId]="defaultOuId"\r
+ [(ngModel)]="searchOrgs"\r
+ (ngModelChange)="grid.reload()">\r
+ </eg-org-family-select>\r
+ </div>\r
+ </div>\r
+ <hr/>\r
+\r
+ <div class="w-100 mt-2 mb-2">\r
+ <eg-grid #grid idlClass={{idlClass}}\r
+ [dataSource]="grid_source"\r
+ [sortable]="true">\r
+ <eg-grid-toolbar-button\r
+ label="Create {{tableName}}" (onClick)="createNew()" i18n-label>\r
+ </eg-grid-toolbar-button>\r
+ <eg-grid-toolbar-action label="Edit Selected" i18n-label (onClick)="editSelected($event)">\r
+ </eg-grid-toolbar-action>\r
+ <eg-grid-toolbar-action label="Delete Selected" i18n-label (onClick)="deleteSelected($event)">\r
+ </eg-grid-toolbar-action>\r
+ <eg-grid-column label="ID" path="id" [index]=true [hidden]="true" i18n-label></eg-grid-column>\r
+ <eg-grid-column label="Department Name" name="name" i18n-label></eg-grid-column>\r
+ </eg-grid>\r
+ </div>\r
+</ng-container>
\ No newline at end of file
--- /dev/null
+import {Component, Input, ViewChild, OnInit, AfterViewInit} from '@angular/core';\r
+import {Router} from '@angular/router';\r
+import {IdlObject, IdlService} from '@eg/core/idl.service';\r
+import {PcrudService} from '@eg/core/pcrud.service';\r
+import {CourseService} from '@eg/staff/share/course.service';\r
+import {GridComponent} from '@eg/share/grid/grid.component';\r
+import {Pager} from '@eg/share/util/pager';\r
+import {GridDataSource, GridColumn} from '@eg/share/grid/grid';\r
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';\r
+import {StringComponent} from '@eg/share/string/string.component';\r
+import {ToastService} from '@eg/share/toast/toast.service';\r
+import {LocaleService} from '@eg/core/locale.service';\r
+import {AuthService} from '@eg/core/auth.service';\r
+import {OrgService} from '@eg/core/org.service';\r
+import {OrgFamily} from '@eg/share/org-family-select/org-family-select.component';\r
+\r
+import {DepartmentContactGridComponent\r
+ } from './department-contact-grid.component';\r
+\r
+@Component({\r
+ selector: 'eg-department-list',\r
+ templateUrl: './course-list-department.component.html'\r
+})\r
+\r
+export class DepartmentListComponent implements OnInit, AfterViewInit {\r
+\r
+ @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent;\r
+ @ViewChild('grid') grid: GridComponent;\r
+ @ViewChild('successString', { static: true }) successString: StringComponent;\r
+ @ViewChild('createString') createString: StringComponent;\r
+ @ViewChild('createErrString') createErrString: StringComponent;\r
+ @ViewChild('updateFailedString') updateFailedString: StringComponent;\r
+ @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent;\r
+ @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent;\r
+ @ViewChild('departmentContactGrid', {static: true})\r
+ private departmentContactGrid: DepartmentContactGridComponent;\r
+\r
+ @Input() sortField: string;\r
+ @Input() idlClass = 'acmd';\r
+ @Input() dialog_size: 'sm' | 'lg' = 'lg';\r
+ @Input() tableName = 'Academic Department';\r
+ grid_source: GridDataSource = new GridDataSource();\r
+ search_value = '';\r
+ defaultOuId: number;\r
+ searchOrgs: OrgFamily;\r
+\r
+\r
+ constructor(\r
+ private courseSvc: CourseService,\r
+ private locale: LocaleService,\r
+ private auth: AuthService,\r
+ private idl: IdlService,\r
+ private org: OrgService,\r
+ private pcrud: PcrudService,\r
+ private router: Router,\r
+ private toast: ToastService\r
+ ) {}\r
+\r
+ ngOnInit() {\r
+ this.getSource();\r
+ this.defaultOuId = this.auth.user().ws_ou() || this.org.root().id();\r
+ this.searchOrgs = {primaryOrgId: this.defaultOuId};\r
+ }\r
+\r
+ ngAfterViewInit() {\r
+ this.grid.onRowActivate.subscribe((department: IdlObject) => {\r
+ const idToEdit = department.id();\r
+ this.navigateToDepartmentPage(idToEdit);\r
+ });\r
+\r
+ }\r
+\r
+ /**\r
+ * Gets the data, specified by the class, that is available.\r
+ */\r
+ getSource() {\r
+ this.grid_source.getRows = (pager: Pager, sort: any[]) => {\r
+ const orderBy: any = {};\r
+ if (sort.length) {\r
+ // Sort specified from grid\r
+ orderBy[this.idlClass] = sort[0].name + ' ' + sort[0].dir;\r
+ } else if (this.sortField) {\r
+ // Default sort field\r
+ orderBy[this.idlClass] = this.sortField;\r
+ }\r
+ const search: any = new Array();\r
+ const orgFilter: any = {};\r
+ orgFilter['owning_lib'] =\r
+ this.searchOrgs.orgIds || [this.defaultOuId];\r
+ search.push(orgFilter);\r
+ const searchOps = {\r
+ offset: pager.offset,\r
+ limit: pager.limit,\r
+ order_by: orderBy\r
+ };\r
+ return this.pcrud.search(this.idlClass, search, searchOps, {fleshSelectors: true});\r
+ };\r
+ }\r
+\r
+ navigateToDepartmentPage(id_arr: IdlObject[]) {\r
+ if (typeof id_arr === 'number') { id_arr = [id_arr]; }\r
+ const urls = [];\r
+ id_arr.forEach(id => {console.log(this.router.url);\r
+ urls.push([this.locale.currentLocaleCode() + this.router.url + '/' + id]);\r
+ });\r
+ if (id_arr.length === 1) {\r
+ this.router.navigate([this.router.url + '/department/' + id_arr[0]]);\r
+ } else {\r
+ urls.forEach(url => {\r
+ window.open(url);\r
+ });\r
+ }\r
+ }\r
+\r
+ createNew() {\r
+ this.editDialog.mode = 'create';\r
+ const course_module_department = this.idl.create('acmd');\r
+ course_module_department.owning_lib(this.auth.user().ws_ou());\r
+ this.editDialog.recordId = null;\r
+ this.editDialog.record = course_module_department;\r
+ this.editDialog.open({size: this.dialog_size}).subscribe(\r
+ ok => {\r
+ this.createString.current()\r
+ .then(str => this.toast.success(str));\r
+ this.grid.reload();\r
+ },\r
+ rejection => {\r
+ if (!rejection.dismissed) {\r
+ this.createErrString.current()\r
+ .then(str => this.toast.danger(str));\r
+ }\r
+ }\r
+ );\r
+ }\r
+\r
+ editSelected(fields: IdlObject[]) {\r
+ // Edit each IDL thing one at a time\r
+ const department_ids = [];\r
+ fields.forEach(field => {\r
+ if (typeof field['id'] === 'function') {\r
+ department_ids.push(field.id());\r
+ } else {\r
+ department_ids.push(field['id']);\r
+ }\r
+ });\r
+ this.navigateToDepartmentPage(department_ids);\r
+ }\r
+\r
+ deleteSelected(idlObject: IdlObject[]) {\r
+ idlObject.forEach(object => {\r
+ object.isdeleted(true);\r
+ });\r
+ this.pcrud.autoApply(idlObject).subscribe(\r
+ val => {\r
+ console.debug('deleted: ' + val);\r
+ this.deleteSuccessString.current()\r
+ .then(str => this.toast.success(str));\r
+ },\r
+ err => {\r
+ this.deleteFailedString.current()\r
+ .then(str => this.toast.danger(str));\r
+ },\r
+ () => this.grid.reload()\r
+ );\r
+ }\r
+}\r
+\r
<eg-admin-page idlClass="acmr"></eg-admin-page>
</ng-template>
</li>
+ <li ngbNavItem>
+ <a ngbNavLink i18n>Departments</a>
+ <ng-template ngbNavContent>
+ <eg-department-list></eg-department-list>
+ </ng-template>
+ </li>
</ul>
<div [ngbNavOutlet]="courseListNav"></div>
import {OrgService} from '@eg/core/org.service';
import {OrgFamily} from '@eg/share/org-family-select/org-family-select.component';
+import {DepartmentListComponent
+ } from './course-list-department.component';
+
import {CourseAssociateMaterialComponent
} from './course-associate-material.component';
import {PatronModule} from '@eg/staff/share/patron/patron.module';
import {CourseTermMapComponent} from './course-term-map.component';
import {CourseTermMapGridComponent} from './course-term-map-grid.component';
+import {DepartmentContactGridComponent} from './department-contact-grid.component';
+import {DepartmentPageComponent} from './department-page.component';
+import {DepartmentListComponent} from './course-list-department.component';
@NgModule({
declarations: [
CourseAssociateMaterialComponent,
CourseAssociateUsersComponent,
CourseTermMapComponent,
- CourseTermMapGridComponent
+ CourseTermMapGridComponent,
+ DepartmentContactGridComponent,
+ DepartmentPageComponent,
+ DepartmentListComponent
],
imports: [
StaffCommonModule,
--- /dev/null
+<eg-string #userDeleteFailedString i18n-text text="Removal of Contact failed or was not allowed"></eg-string>\r
+<eg-string #userDeleteSuccessString i18n-text text="Removal of Contact succeeded"></eg-string>\r
+<eg-string #userAddSuccessString i18n-text text="Addition of Contact succeeded"></eg-string>\r
+<eg-string #userAddFailedString i18n-text text="Addition of Contact failed or was not allowed"></eg-string>\r
+<eg-string #userEditSuccessString i18n-text text="Update of Contact succeeded"></eg-string>\r
+<eg-string #userEditFailedString i18n-text text="Update of Contact failed or was not allowed"></eg-string>\r
+\r
+<eg-patron-search-dialog #patronSearch>\r
+</eg-patron-search-dialog>\r
+\r
+<div [ngClass]="isDialog() ? 'modal-body' : ''">\r
+ <div class="row">\r
+ <div [ngClass]="isDialog() ? 'col-md-12' : 'col-md-4'">\r
+ <div class="row" [ngClass]="isDialog() ? '' : 'mt-3'">\r
+ <div class="d-flex" [ngClass]="isDialog() ? 'col-md-6' : 'col-md-12'">\r
+ <div class="input-group">\r
+ <div class="input-group-prepend">\r
+ <label for="associate-user-barcode" class="input-group-text" i18n>Patron Barcode</label>\r
+ </div>\r
+ <input type="text" class="flex-grow-1" id="associate-user-barcode"\r
+ [(ngModel)]="contactBarcode" (click)="$event.target.select()"\r
+ (keyup.enter)="associateContacts(contactBarcode)" />\r
+ <button class="btn btn-outline-dark btn-sm" (click)="searchPatrons()">\r
+ <span class="material-icons mat-icon-in-button align-middle"\r
+ i18n-title title="Search for Patron">search</span>\r
+ <span class="align-middle" i18n>Search for Patron</span>\r
+ </button>\r
+ </div>\r
+ </div>\r
+ <div class="d-flex" [ngClass]="isDialog() ? 'col-md-6' : 'col-md-12 mt-3'">\r
+ <div class="input-group">\r
+ <div class="input-group-prepend">\r
+ <label for="associate-user-role" class="input-group-text" i18n>Role</label>\r
+ </div>\r
+ <eg-combobox idlClass="acmr" [(ngModel)]="contactRoleInput"></eg-combobox>\r
+ </div>\r
+ </div>\r
+ </div>\r
+ <div class="row mt-3">\r
+ <div class="text-right" [ngClass]="isDialog() ? 'col-md-2' : 'col-md-6'">\r
+ <button class="btn btn-primary"\r
+ i18n [disabled]="!contactBarcode" (click)="associateContacts(contactBarcode)">\r
+ Add Contact\r
+ </button>\r
+ </div>\r
+ </div>\r
+ </div>\r
+ <div class="mt-3" [ngClass]="isDialog() ? 'col-md-12' : 'col-md-8'">\r
+ <eg-grid #contactsGrid [dataSource]="contactsDataSource" [useLocalSort]="true">\r
+ <eg-grid-toolbar-action label="Remove Selected" i18n-label (onClick)="deleteSelectedContacts($event)">\r
+ </eg-grid-toolbar-action>\r
+ <eg-grid-toolbar-action label="Edit Selected" i18n-label (onClick)="editSelectedContacts($event)">\r
+ </eg-grid-toolbar-action>\r
+ <eg-grid-column label="Contact ID" path="usr.id" [index]=true [hidden]="true" i18n-label></eg-grid-column>\r
+ <eg-grid-column label="First Name" path="usr.first_given_name" i18n-label></eg-grid-column>\r
+ <eg-grid-column label="Second Name" path="usr.second_given_name" i18n-label></eg-grid-column>\r
+ <eg-grid-column label="Last Name" path="usr.family_name" i18n-label></eg-grid-column>\r
+ <eg-grid-column label="Prefix" path="usr.pref_prefix" [hidden]="true" i18n-label></eg-grid-column>\r
+ <eg-grid-column label="Preferred First Name" path="usr.pref_first_given_name"[hidden]="true" i18n-label></eg-grid-column>\r
+ <eg-grid-column label="Preferred Second Name" path="usr.pref_second_given_name"[hidden]="true" i18n-label></eg-grid-column>\r
+ <eg-grid-column label="Preferred Family Name" path="usr.pref_family_name"[hidden]="true" i18n-label></eg-grid-column>\r
+ <eg-grid-column label="Preferred Suffix" path="usr.pref_suffix" [hidden]="true" i18n-label></eg-grid-column>\r
+ <eg-grid-column label="User Role" path="usr_role.name" i18n-label></eg-grid-column>\r
+ </eg-grid>\r
+ </div>\r
+ </div>\r
+</div>\r
+\r
+<ng-container *ngIf="!isDialog()">\r
+ <!-- in "inline" mode, render the grid pane right here -->\r
+ <ng-container *ngTemplateOutlet="dialogContent">\r
+ </ng-container>\r
+</ng-container>\r
+\r
+<eg-fm-record-editor #editDialog\r
+ idlClass='acmdc'\r
+ [fieldOptions]="{department: {linkedSearchField: 'name'}}"\r
+ [preloadLinkedValues]="true"\r
+ hiddenFields="id,usr">\r
+</eg-fm-record-editor>
\ No newline at end of file
--- /dev/null
+import {Component, Input, ViewChild, OnInit} from '@angular/core';\r
+import {DialogComponent} from '@eg/share/dialog/dialog.component';\r
+import {AuthService} from '@eg/core/auth.service';\r
+import {NetService} from '@eg/core/net.service';\r
+import {PcrudService} from '@eg/core/pcrud.service';\r
+import {Pager} from '@eg/share/util/pager';\r
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';\r
+import {GridDataSource} from '@eg/share/grid/grid';\r
+import {GridComponent} from '@eg/share/grid/grid.component';\r
+import {IdlObject} from '@eg/core/idl.service';\r
+import {StringComponent} from '@eg/share/string/string.component';\r
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';\r
+import {PatronSearchDialogComponent} from '@eg/staff/share/patron/search-dialog.component';\r
+import {ToastService} from '@eg/share/toast/toast.service';\r
+import {CourseService} from '@eg/staff/share/course.service';\r
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';\r
+\r
+@Component({\r
+ selector: 'eg-department-contact-grid',\r
+ templateUrl: './department-contact-grid.component.html'\r
+})\r
+\r
+export class DepartmentContactGridComponent extends DialogComponent implements OnInit {\r
+ @Input() currentDepartment: IdlObject;\r
+ @Input() departmentId: number;\r
+ @Input() displayMode: String;\r
+ users: any[] = [];\r
+ @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent;\r
+ @ViewChild('patronSearch') patronSearch: PatronSearchDialogComponent;\r
+ @ViewChild('contactsGrid') contactsGrid: GridComponent;\r
+ @ViewChild('userDeleteFailedString', { static: true })\r
+ userDeleteFailedString: StringComponent;\r
+ @ViewChild('userDeleteSuccessString', { static: true })\r
+ userDeleteSuccessString: StringComponent;\r
+ @ViewChild('userAddSuccessString', { static: true })\r
+ userAddSuccessString: StringComponent;\r
+ @ViewChild('userAddFailedString', { static: true })\r
+ userAddFailedString: StringComponent;\r
+ @ViewChild('userEditSuccessString', { static: true })\r
+ userEditSuccessString: StringComponent;\r
+ @ViewChild('userEditFailedString', { static: true })\r
+ userEditFailedString: StringComponent;\r
+ contactsDataSource: GridDataSource;\r
+ contactBarcode: String;\r
+ contactRoleInput: ComboboxEntry;\r
+\r
+ constructor(\r
+ private auth: AuthService,\r
+ private course: CourseService,\r
+ private net: NetService,\r
+ private pcrud: PcrudService,\r
+ private toast: ToastService,\r
+ private modal: NgbModal\r
+ ) {\r
+ super(modal);\r
+ this.contactsDataSource = new GridDataSource();\r
+ }\r
+\r
+ ngOnInit() {\r
+ this.contactsDataSource.getRows = (pager: Pager, sort: any[]) => {\r
+ return this.course.getDepartmentContacts([this.departmentId]);\r
+ };\r
+ }\r
+\r
+ isDialog(): boolean {\r
+ return this.displayMode === 'dialog';\r
+ }\r
+\r
+ editSelectedContacts(contactFields: IdlObject[]) {\r
+ // Edit each IDL thing one at a time\r
+ const editOneThing = (contact: IdlObject) => {\r
+ if (!contact) { return; }\r
+\r
+ this.showEditDialog(contact).then(\r
+ () => editOneThing(contactFields.shift()));\r
+ };\r
+\r
+ editOneThing(contactFields.shift());\r
+ }\r
+\r
+ searchPatrons() {\r
+ this.patronSearch.open({size: 'xl'}).toPromise().then(\r
+ patrons => {\r
+ if (!patrons || patrons.length === 0) { return; }\r
+ const contact = patrons[0];\r
+ this.contactBarcode = contact.card().barcode();\r
+ }\r
+ );\r
+ }\r
+\r
+ associateContacts(barcode) {\r
+ if (barcode) {\r
+ const args = {\r
+ currentDepartment: this.currentDepartment,\r
+ barcode: barcode.trim(),\r
+ };\r
+\r
+ if (this.contactRoleInput) {\r
+ args['role'] = this.contactRoleInput.id;\r
+ }\r
+\r
+ this.contactBarcode = null;\r
+\r
+ this.net.request(\r
+ 'open-ils.actor',\r
+ 'open-ils.actor.user.retrieve_id_by_barcode_or_username',\r
+ this.auth.token(), barcode.trim()\r
+ ).subscribe(patron => {\r
+ this.course.associateContacts(patron, args)\r
+ .then(() => this.contactsGrid.reload());\r
+ }, err => {\r
+ this.userAddFailedString.current().then(str => this.toast.danger(str));\r
+ }\r
+ );\r
+ }\r
+ \r
+ }\r
+\r
+ showEditDialog(contact: IdlObject): Promise<any> {\r
+ this.editDialog.mode = 'update';\r
+ this.editDialog.recordId = contact.id();\r
+ return new Promise((resolve, reject) => {\r
+ this.editDialog.open({size: 'lg'}).subscribe(\r
+ result => {\r
+ this.userEditSuccessString.current()\r
+ .then(str => this.toast.success(str));\r
+ this.contactsGrid.reload();\r
+ resolve(result);\r
+ },\r
+ error => {\r
+ this.userEditFailedString.current()\r
+ .then(str => this.toast.danger(str));\r
+ reject(error);\r
+ }\r
+ );\r
+ });\r
+ }\r
+\r
+ deleteSelectedContacts(contacts) {\r
+ const acmdc_ids = contacts.map(u => u.id());\r
+ this.pcrud.search('acmdc', {id: acmdc_ids}).subscribe(contact => {\r
+ contact.isdeleted(true);\r
+ this.pcrud.autoApply(contact).subscribe(\r
+ val => {\r
+ console.debug('deleted: ' + val);\r
+ this.userDeleteSuccessString.current().then(str => this.toast.success(str));\r
+ this.contactsGrid.reload();\r
+ },\r
+ err => {\r
+ this.userDeleteFailedString.current()\r
+ .then(str => this.toast.danger(str));\r
+ }\r
+ );\r
+ }).add(() => this.contactsGrid.reload());\r
+ }\r
+\r
+}\r
--- /dev/null
+<eg-staff-banner\r
+ bannerText=" {{currentDepartment.name()}}"\r
+ i18n-bannerText class="mb-3" *ngIf="currentDepartment"\r
+ [bannerStyle]="alert-secondary">\r
+</eg-staff-banner>\r
+\r
+<div class="row">\r
+ <div class="col text-right">\r
+ <a class="btn btn-warning ml-3" routerLink="/staff/admin/local/asset/course_list" i18n>\r
+ <i class="material-icons align-middle">keyboard_return</i>\r
+ <span class="align-middle">Return to Course List</span>\r
+ </a>\r
+ </div>\r
+</div>\r
+<ul ngbNav #coursePageNav="ngbNav" class="nav-tabs">\r
+\r
+ <!-- Edit Tab -->\r
+ <li [ngbNavItem]="'edit'">\r
+ <a ngbNavLink i18n>Edit department</a>\r
+ <ng-template ngbNavContent>\r
+ <div class="row">\r
+ <div class="col-lg-3 mt-3">\r
+ </div>\r
+ <div class="col-lg-6 mt-3">\r
+ <eg-fm-record-editor displayMode="inline"\r
+ mode="update"\r
+ hiddenFieldsList="id"\r
+ idlClass="acmd"\r
+ fieldOrder="name,owning_lib"\r
+ [preloadLinkedValues]="true"\r
+ [record]="currentDepartment">\r
+ </eg-fm-record-editor>\r
+ </div>\r
+ </div>\r
+ </ng-template>\r
+ </li>\r
+\r
+ <!-- Department Contacts Tab -->\r
+ <li [ngbNavItem]="'departmentContacts'">\r
+ <a ngbNavLink i18n>Department contacts</a>\r
+ <ng-template ngbNavContent>\r
+ <eg-department-contact-grid [departmentId]="departmentId"\r
+ [currentDepartment]="currentDepartment" displayMode="inline">\r
+ </eg-department-contact-grid>\r
+ </ng-template>\r
+ </li>\r
+</ul>\r
+<div [ngbNavOutlet]="coursePageNav" class="mb-3"></div>\r
--- /dev/null
+import {Component, ViewChild, OnInit} from '@angular/core';\r
+import {ActivatedRoute} from '@angular/router';\r
+import {PcrudService} from '@eg/core/pcrud.service';\r
+import {IdlObject} from '@eg/core/idl.service';\r
+import {StringComponent} from '@eg/share/string/string.component';\r
+import {ToastService} from '@eg/share/toast/toast.service';\r
+import {CourseService} from '@eg/staff/share/course.service';\r
+import {DepartmentContactGridComponent} from './department-contact-grid.component';\r
+\r
+@Component({\r
+ selector: 'eg-department-page',\r
+ templateUrl: './department-page.component.html'\r
+})\r
+\r
+export class DepartmentPageComponent implements OnInit {\r
+\r
+ currentDepartment: IdlObject;\r
+ departmentId: any;\r
+\r
+ // Supplemental Tabs - possibly add a tab for a grid displaying all courses associated with the department here\r
+ @ViewChild('departmentContactGrid', {static: true})\r
+ private departmentContactGrid: DepartmentContactGridComponent;\r
+\r
+ constructor(\r
+ private course: CourseService,\r
+ private pcrud: PcrudService,\r
+ private route: ActivatedRoute,\r
+ private toast: ToastService\r
+ ) {\r
+ }\r
+\r
+ ngOnInit() {\r
+ this.departmentId = +this.route.snapshot.paramMap.get('id');\r
+ this.course.getDepartments([this.departmentId]).then(department => {\r
+ this.currentDepartment = department[0];\r
+ });\r
+ }\r
+\r
+}\r
import {RouterModule, Routes} from '@angular/router';
import {CourseListComponent} from './course-list.component';
import {CoursePageComponent} from './course-page.component';
+import {DepartmentPageComponent} from './department-page.component';
const routes: Routes = [{
path: ':id',
}, {
path: '',
component: CourseListComponent
+}, {
+ path: 'department/:id',
+ component: DepartmentPageComponent
}];
@NgModule({
}
}
+ getDepartments(department_ids?: Number[]): Promise<IdlObject> {
+ const flesher = {flesh: 2, flesh_fields: {
+ 'acmc': ['owning_lib'],
+ 'aou': ['ou_type']}};
+ if (!department_ids) {
+ return this.pcrud.retrieveAll('acmd',
+ flesher, {atomic: true}).toPromise();
+ } else {
+ return this.pcrud.search('acmd', {id: department_ids},
+ flesher, {atomic: true}).toPromise();
+ }
+ }
+
+ getDepartmentContacts(department_ids?: Number[]): Observable<IdlObject> {
+ const flesher = {
+ flesh: 1,
+ flesh_fields: {'acmdc': ['usr', 'usr_role']}
+ };
+ if (!department_ids) {
+ return this.pcrud.retrieveAll('acmdc',
+ flesher);
+ } else {
+ return this.pcrud.search('acmdc', {department: department_ids},
+ flesher);
+ }
+ }
+
getCoursesFromMaterial(copy_id): Promise<any> {
const id_list = [];
return new Promise((resolve, reject) => {
return this.pcrud.create(new_user).toPromise();
}
+ associateContacts(patron_id, args) {
+ const new_user = this.idl.create('acmdc');
+ if (args.role) { new_user.usr_role(args.role); }
+ new_user.department(args.currentDepartment.id());
+ new_user.usr(patron_id);
+ return this.pcrud.create(new_user).toPromise();
+ }
+
disassociateMaterials(courses) {
return new Promise((resolve, reject) => {
const course_ids = [];
name TEXT NOT NULL,
course_number TEXT NOT NULL,
section_number TEXT,
+ department INT REFERENCES asset.course_module_department (id),
owning_lib INT REFERENCES actor.org_unit (id),
is_archived BOOLEAN DEFAULT false
);
course INT NOT NULL REFERENCES asset.course_module_course (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
);
+CREATE TABLE asset.course_module_department (
+ id SERIAL PRIMARY KEY,
+ name TEXT NOT NULL,
+ owning_lib INT REFERENCES actor.org_unit (id)
+);
+
+CREATE TABLE asset.course_module_department_contacts (
+ id SERIAL PRIMARY KEY,
+ department INT NOT NULL REFERENCES asset.course_module_department (id),
+ usr INT NOT NULL REFERENCES actor.usr (id),
+ usr_role INT REFERENCES asset.course_module_role (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
+);
+
+CREATE TABLE asset.course_module_department_course_map (
+ id BIGSERIAL PRIMARY KEY,
+ department INT NOT NULL REFERENCES asset.course_module_department (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ course INT NOT NULL REFERENCES asset.course_module_course (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
+);
+
COMMIT;
INSERT INTO asset.course_module_role (id, name, is_public) VALUES
(1, oils_i18n_gettext(1, 'Instructor', 'acmr', 'name'), true),
(2, oils_i18n_gettext(2, 'Teaching assistant', 'acmr', 'name'), true),
-(3, oils_i18n_gettext(3, 'Student', 'acmr', 'name'), false);
+(3, oils_i18n_gettext(3, 'Student', 'acmr', 'name'), false),
+(4, oils_i18n_gettext(4, 'Department Head', 'acmr', 'name'), true);
SELECT SETVAL('asset.course_module_role_id_seq'::TEXT, 100);
--- /dev/null
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+CREATE TABLE asset.course_module_department (
+ id SERIAL PRIMARY KEY,
+ name TEXT NOT NULL,
+ owning_lib INT REFERENCES actor.org_unit (id)
+);
+
+CREATE TABLE asset.course_module_department_contacts (
+ id SERIAL PRIMARY KEY,
+ department INT NOT NULL REFERENCES asset.course_module_department (id),
+ usr INT NOT NULL REFERENCES actor.usr (id),
+ usr_role INT REFERENCES asset.course_module_role (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
+);
+
+CREATE TABLE asset.course_module_department_course_map (
+ id BIGSERIAL PRIMARY KEY,
+ department INT NOT NULL REFERENCES asset.course_module_department (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ course INT NOT NULL REFERENCES asset.course_module_course (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
+);
+
+ALTER TABLE asset.course_module_course
+ ADD COLUMN department INT REFERENCES asset.course_module_department (id);
+
+INSERT INTO asset.course_module_role (id, name, is_public) VALUES
+(4, oils_i18n_gettext(4, 'Department Head', 'acmr', 'name'), true);
+
+COMMIT;
\ No newline at end of file