<eg-link-table-link i18n-label label="Circulation Limit Sets"
url="/eg/staff/admin/local/config/circ_limit_set"></eg-link-table-link>
<eg-link-table-link i18n-label label="Circulation Policies"
- url="/eg/staff/admin/local/config/circ_matrix_matchpoint"></eg-link-table-link>
+ routerLink="/staff/admin/local/config/circ_matrix_matchpoint"></eg-link-table-link>
<eg-link-table-link i18n-label label="Closed Dates Editor"
url="/eg/staff/admin/local/actor/closed_dates"></eg-link-table-link>
<!-- do-able with a list of IDL classes to add to the edit dialog -->
--- /dev/null
+<ng-template #dialogContent>
+ <div class="modal-body">
+ <ng-content>
+ </ng-content>
+ <button type="button" class="btn btn-warning ml-2 mb-6" [ngStyle]="{marginTop:'-12%'}"
+ (click)="closeEditor()" i18n>Cancel
+ </button>
+ </div>
+</ng-template>
\ No newline at end of file
--- /dev/null
+import {Component, OnInit, Input,
+ Output, EventEmitter} from '@angular/core';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {Observable} from 'rxjs';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+
+@Component({
+ selector: 'eg-circ-matrix-matchpoint-dialog',
+ templateUrl: './circ-matrix-matchpoint-dialog.component.html'
+})
+export class CircMatrixMatchpointDialogComponent extends DialogComponent implements OnInit {
+
+ // Emit the modified object when the save action completes.
+ @Output() recordSaved = new EventEmitter<any>();
+
+ // Emit the original object when the save action is canceled.
+ @Output() recordCanceled = new EventEmitter<any>();
+
+ constructor(
+ private modal: NgbModal // required for passing to parent
+ ) {
+ super(modal);
+ }
+
+ ngOnInit() {
+ }
+
+ open(args?: NgbModalOptions): Observable<any> {
+ if (!args) {
+ args = {};
+ }
+ // ensure we don't hang on to our copy of the record
+ // if the user dismisses the dialog
+ args.beforeDismiss = () => {
+ return true;
+ };
+ return super.open(args);
+ }
+
+ cancel() {
+ this.recordCanceled.emit();
+ this.close();
+ }
+
+ closeEditor() {
+ this.recordCanceled.emit();
+ this.close();
+ }
+
+ save() {
+ this.recordSaved.emit();
+ }
+}
--- /dev/null
+<eg-title i18n-prefix prefix="Circulation Policy Configuration"></eg-title>
+<eg-staff-banner bannerText="Circulation Policy Configuration" i18n-bannerText>
+</eg-staff-banner>
+
+<eg-string #successString i18n-text text="Circulation Policy Update Succeeded"></eg-string>
+<eg-string #createString i18n-text text="Circulation Policy Creation Succeeded"></eg-string>
+
+<eg-grid #grid idlClass="ccmm"
+ [dataSource]="dataSource"
+ [sortable]="true"
+ (onRowActivate)="editSelected([$event])"
+ [showFields]='"is_renewal,active,org_unit,copy_circ_lib,copy_owning_lib,user_home_ou"'>
+ <eg-grid-toolbar-button
+ label="New Circ Matrix Matchpoint" i18n-label (onClick)="createNew()">
+ </eg-grid-toolbar-button>
+ <eg-grid-toolbar-action label="Edit Selected" i18n-label (onClick)="editSelected($event)">
+ </eg-grid-toolbar-action>
+</eg-grid>
+
+<eg-circ-matrix-matchpoint-dialog #matchpointDialog
+ (recordCanceled)="clearLinkedCircLimitSets()"
+ (recordError)="clearLinkedCircLimitSets()">
+ <div #limitSets [ngStyle]="{marginBottom:'10px'}">
+ <ng-container>
+ <eg-linked-circ-limit-sets
+ #circLimitSets
+ (outputLinkedLimitSet)="setLimitSets($event)">
+ </eg-linked-circ-limit-sets>
+ </ng-container>
+ </div>
+
+ <ng-template #active let-idPrefix="idPrefix" let-field="field" let-record="record">
+ <div class="col-lg-9">
+ <input
+ class="form-check-input"
+ type="checkbox"
+ name="{{field.name}}"
+ id="{{idPrefix}}-{{field.name}}"
+ [disabled]="field.readOnly"
+ [ngModel]="record[field.name]()"
+ (ngModelChange)="record[field.name]($event)"/>
+ </div>
+ <div class="row" [ngStyle]="{
+ width:'150%',
+ backgroundColor:'black',
+ marginLeft:'-43%',
+ marginTop:'6%',
+ marginBottom:'-1%',
+ opacity:'90%',
+ fontSize:'18px',
+ textAlign:'center'}">
+ <div [ngStyle]="{width:'100%', color:'white'}">Circulation Policies</div>
+ </div>
+ </ng-template>
+
+ <ng-template #item_age let-idPrefix="idPrefix" let-field="field" let-record="record">
+ <div class="col-lg-9">
+ <input
+ class="form-control"
+ id="{{idPrefix}}-{{field.name}}" name="{{field.name}}"
+ type="text"
+ placeholder="{{field.label}}..." i18n-placeholder
+ [required]="field.isRequired()"
+ [ngModel]="record[field.name]()"
+ (ngModelChange)="record[field.name]($event)"/>
+ </div>
+ <div class="row" [ngStyle]="{
+ width:'150%',
+ backgroundColor:'black',
+ marginLeft:'-43%',
+ marginTop:'3%',
+ marginBottom:'-1%',
+ opacity:'90%',
+ fontSize:'18px',
+ textAlign:'center',
+ paddingTop:'3.5%',
+ paddingBottom:'5.5%',
+ paddingLeft:'3.5%',
+ paddingRight:'3.5%'}">
+ <div [ngStyle]="{width:'100%', color:'white'}">Circulation Policy Effects</div>
+ </div>
+ </ng-template>
+
+ <eg-fm-record-editor #editDialog
+ idlClass="ccmm"
+ [preloadLinkedValues]="true"
+ readonlyFields="name"
+ displayMode="inline"
+ fieldOrder="id,active,grp,org_unit,copy_circ_lib,copy_owning_lib,user_home_ou,is_renewal,juvenile_flag,circ_modifier,copy_location,marc_type,marc_form,marc_bib_level,marc_vr_format,ref_flag,usr_age_lower_bound,usr_age_upper_bound,item_age,circulate,duration_rule,renewals,hard_due_date,recurring_fine_rule,grace_period,max_fine_rule,available_copy_hold_ratio,total_copy_hold_ratio,script_test,description"
+ requiredFields="active,grp,org_unit"
+ (recordSaved)="configureLimitSets($event); clearLinkedCircLimitSets(); closeDialog()"
+ [fieldOptions]="{active:{customTemplate:{template:active}}, item_age:{customTemplate:{template:item_age}}}">
+ </eg-fm-record-editor>
+</eg-circ-matrix-matchpoint-dialog>
\ No newline at end of file
--- /dev/null
+import {Pager} from '@eg/share/util/pager';
+import {Component, OnInit, AfterViewInit, Input, ViewChild, ElementRef} from '@angular/core';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource, GridColumn, GridRowFlairEntry} from '@eg/share/grid/grid';
+import {IdlObject} from '@eg/core/idl.service';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {LinkedCircLimitSetsComponent} from './linked-circ-limit-sets.component';
+import {CircMatrixMatchpointDialogComponent} from './circ-matrix-matchpoint-dialog.component';
+import {StringComponent} from '@eg/share/string/string.component';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+
+ @Component({
+ templateUrl: './circ-matrix-matchpoint.component.html'
+})
+export class CircMatrixMatchpointComponent implements OnInit {
+ recId: number;
+ gridDataSource: GridDataSource;
+ initDone = false;
+ dataSource: GridDataSource = new GridDataSource();
+ showLinkLimitSets = false;
+ usedSetLimitList = {};
+ linkedLimitSets = [];
+ limitSetNames = {};
+ dividerStyle = {
+ width: '30%',
+ marginTop: '25px',
+ marginLeft: '73%'
+ };
+
+ @ViewChild('limitSets', { static: false }) limitSets: ElementRef;
+ @ViewChild('circLimitSets', { static: true }) limitSetsComponent: LinkedCircLimitSetsComponent;
+ @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent;
+ @ViewChild('matchpointDialog', { static: true }) matchpointDialog: CircMatrixMatchpointDialogComponent;
+ @ViewChild('grid', { static: true }) grid: GridComponent;
+ @ViewChild('successString', { static: true }) successString: StringComponent;
+ @ViewChild('createString', { static: false }) createString: StringComponent;
+ @ViewChild('createErrString', { static: false }) createErrString: StringComponent;
+ @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent;
+
+ @Input() idlClass = 'ccmm';
+ // Default sort field, used when no grid sorting is applied.
+ @Input() sortField: string;
+
+ @Input() dialogSize: 'sm' | 'lg' = 'lg';
+
+ constructor(
+ private pcrud: PcrudService,
+ private toast: ToastService
+ ) {
+ this.gridDataSource = new GridDataSource();
+ }
+
+ ngOnInit() {
+ this.initDone = true;
+ this.dataSource.getRows = (pager: Pager, sort: any[]) => {
+ const orderBy: any = {};
+ if (sort.length) {
+ // Sort specified from grid
+ orderBy[this.idlClass] = sort[0].name + ' ' + sort[0].dir;
+ } else if (this.sortField) {
+ // Default sort field
+ orderBy[this.idlClass] = this.sortField;
+ }
+
+ const searchOps = {
+ offset: pager.offset,
+ limit: pager.limit,
+ order_by: orderBy
+ };
+ return this.pcrud.retrieveAll('ccmm', searchOps, {fleshSelectors: true});
+ };
+ }
+
+ clearLinkedCircLimitSets() {
+ this.limitSetsComponent.usedSetLimitList = {};
+ this.limitSetsComponent.linkedSetList = [];
+ this.linkedLimitSets = [];
+ }
+
+ closeDialog() {
+ this.matchpointDialog.closeEditor();
+ this.grid.reload();
+ }
+
+ editSelected(fields: IdlObject[]) {
+ // Edit each IDL thing one at a time
+ const editOneThing = (field: IdlObject) => {
+ if (!field) { return; }
+ this.showEditDialog(field).then(
+ () => editOneThing(fields.shift()));
+ };
+ editOneThing(fields.shift());
+ }
+
+ showEditDialog(field: IdlObject): Promise<any> {
+ this.limitSetsComponent.showLinkLimitSets = true;
+ this.getLimitSets(field.id());
+ this.editDialog.mode = 'update';
+ this.editDialog.recordId = field['id']();
+ return new Promise((resolve, reject) => {
+ this.matchpointDialog.open({size: this.dialogSize}).subscribe(
+ result => {
+ this.successString.current()
+ .then(str => this.toast.success(str));
+ this.grid.reload();
+ resolve(result);
+ },
+ error => {
+ this.updateFailedString.current()
+ .then(str => this.toast.danger(str));
+ reject(error);
+ }
+ );
+ });
+ }
+
+ createNew() {
+ this.getLimitSets(null);
+ this.limitSetsComponent.showLinkLimitSets = true;
+ this.editDialog.mode = 'create';
+ this.editDialog.recordId = null;
+ this.editDialog.record = null;
+ this.editDialog.handleRecordChange();
+ // We reuse the same editor for all actions. Be sure
+ // create action does not try to modify an existing record.
+ this.matchpointDialog.open({size: this.dialogSize}).subscribe(
+ ok => {
+ this.createString.current()
+ .then(str => this.toast.success(str));
+ this.limitSetsComponent.showLinkLimitSets = false;
+ this.grid.reload();
+ },
+ rejection => {
+ if (!rejection.dismissed) {
+ this.createErrString.current()
+ .then(str => this.toast.danger(str));
+ }
+ this.limitSetsComponent.showLinkLimitSets = false;
+ }
+ );
+ }
+
+ setLimitSets(sets) {
+ this.linkedLimitSets = sets;
+ }
+
+ /**
+ * Runs through the different CRUD operations, specified by the object that is passed into each.
+ * @param matchpoint
+ */
+ configureLimitSets(matchpoint) {
+ const linkedSets = this.linkedLimitSets;
+ Object.keys(linkedSets).forEach((key) => {
+ const ls = linkedSets[key];
+ if (ls.created) {
+ this.deleteLimitSets(ls).then(() => {
+ if (ls.isNew && !ls.isDeleted) {
+ this.pcrud.create(this.createLimitSets(ls.linkedLimitSet, matchpoint)).subscribe(() => {});
+ } else if (!ls.isNew && !ls.isDeleted) {
+ this.updateLimitSets(ls.linkedLimitSet);
+ }
+ });
+ }
+ });
+ }
+
+ getLimitSets(id) {
+ this.clearLinkedCircLimitSets();
+ this.pcrud.retrieveAll('ccmlsm').subscribe((res) => {
+ /**
+ * If the limit set's matchpoint equals the matchpoint given
+ * by the user, then add that to the set limit list
+ */
+ this.limitSetsComponent.usedSetLimitList[res.limit_set()] = res.id();
+ if (res.matchpoint() === id) {
+ this.limitSetsComponent.createFilledLimitSetObject(res);
+ }
+ });
+ /**
+ * Retrives all limit set names
+ */
+ this.pcrud.retrieveAll('ccls').subscribe((res) => {
+ this.limitSetsComponent.limitSetNames[res.id()] = res.name();
+ });
+ }
+
+ createLimitSets(limitSet, matchpoint) {
+ if (typeof matchpoint === 'number' || typeof matchpoint === 'string') {
+ limitSet.matchpoint(matchpoint);
+ } else {
+ limitSet.matchpoint(matchpoint.id());
+ }
+ return limitSet;
+ }
+
+ updateLimitSets(limitSet) {
+ this.pcrud.update(limitSet).subscribe(() => {});
+ }
+
+ deleteLimitSets(limitSet) {
+ return new Promise((resolve, reject) => {
+ if (limitSet.isDeleted) {
+ if (limitSet.linkedLimitSet.id()) {
+ this.pcrud.remove(limitSet.linkedLimitSet).subscribe(res => {
+ resolve();
+ });
+ } else {
+ resolve();
+ }
+ } else {
+ resolve();
+ }
+ });
+ }
+}
--- /dev/null
+import {NgModule} from '@angular/core';
+import {TreeModule} from '@eg/share/tree/tree.module';
+import {CircMatrixMatchpointRoutingModule} from './routing.module';
+import {AdminCommonModule} from '@eg/staff/admin/common.module';
+import {CircMatrixMatchpointComponent} from './circ-matrix-matchpoint.component';
+import {LinkedCircLimitSetsComponent} from './linked-circ-limit-sets.component';
+import {CircMatrixMatchpointDialogComponent} from './circ-matrix-matchpoint-dialog.component';
+
+@NgModule({
+ declarations: [
+ CircMatrixMatchpointComponent,
+ LinkedCircLimitSetsComponent,
+ CircMatrixMatchpointDialogComponent
+ ],
+ imports: [
+ AdminCommonModule,
+ CircMatrixMatchpointRoutingModule,
+ TreeModule
+ ],
+ exports: [
+ ],
+ providers: [
+ ]
+})
+
+export class CircMatrixMatchpointModule {
+}
+
+
--- /dev/null
+
+<eg-string #errorString i18n-text text="The Linked Set Name Already Exists on Another Matchpoint"></eg-string>
+
+<div *ngIf="showLinkLimitSets">
+ <div class="col-lg-15 bg-info d-flex justify-content-center"><h3 class="modal-title mt-3 mb-3" i18n>Linked Limit Sets</h3></div>
+ <ng-container *ngIf="getObjectKeys().length > 0">
+ <ng-container *ngFor="let key of getObjectKeys(); let i = index">
+ <div *ngIf="!linkedSetList[i].isDeleted" class="col-lg-15 d-flex justify-content-center">
+ <div *ngIf="linkedSetList[i].created" class="col-lg-2 mt-3 mb-3 form-group form-check">
+ <label for="name" i18n>Name</label>
+ <div class="d-flex justify-content-center">
+ <span>{{limitSetNames[linkedSetList[i].linkedLimitSet.limit_set()]}}</span>
+ </div>
+ </div>
+ <div *ngIf="linkedSetList[i].created" class="col-lg-2 mt-3 mb-3 form-group form-check">
+ <label for="{{fallthrough+i}}" i18n>Fallthrough</label>
+ <div class="d-flex justify-content-center">
+ <input
+ class="form-check-input"
+ type="checkbox"
+ name="{{fallthrough+i}}"
+ [ngModel]="linkedSetList[i].linkedLimitSet.fallthrough()"
+ (ngModelChange)="linkedSetList[i].linkedLimitSet.fallthrough($event); this.emitLimitSet();"/>
+ </div>
+ </div>
+ <div *ngIf="linkedSetList[i].created" class="col-lg-2 mt-3 mb-3 form-group form-check">
+ <label for="{{active+i}}" i18n>Active</label>
+ <div class="d-flex justify-content-center">
+ <input
+ class="form-check-input"
+ type="checkbox"
+ [ngModel]="linkedSetList[i].linkedLimitSet.active()"
+ (ngModelChange)="linkedSetList[i].linkedLimitSet.active($event); this.emitLimitSet();"/>
+ </div>
+
+ </div>
+ <div *ngIf="linkedSetList[i].created" class="col-lg-2 mt-3 mb-3 form-group form-check">
+ <button
+ type="button"
+ class="btn btn-warning"
+ ng-disabled="!linkedSet"
+ (click)="removeLinkedSet(i)"
+ i18n-title title="Remove" i18n>Remove
+ </button>
+ </div>
+ </div>
+ </ng-container>
+ </ng-container>
+ <div class="input-group mt-3 col-md-12">
+ <div class="input-group-prepend">
+ <div class="input-group-text" i18n>Circ Limit Set Name</div>
+ <eg-combobox
+ name="linkedLimitName"
+ idlClass="ccls"
+ idlField="name"
+ asyncSupportsEmptyTermClick="true"
+ (onChange)="onChange($event)">
+ </eg-combobox>
+ </div>
+ <div class="input-group-append">
+ <button
+ type="button"
+ class="btn btn-info"
+ (click)="addLinkedSet()"
+ i18n-title title="Add" i18n>Add
+ </button>
+ <eg-help-popover [helpText]="'Go to Local Admin -> Circulation Limit Sets to Create a Link Limit Set'" ></eg-help-popover>
+ </div>
+ </div>
+</div>
--- /dev/null
+
+import {Component, OnInit, Input, Output, EventEmitter, ViewChild} from '@angular/core';
+import { IdlService} from '@eg/core/idl.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {StringComponent} from '@eg/share/string/string.component';
+
+class LinkedLimitSetObjects {
+ linkedLimitSet: any;
+ created: boolean;
+ isDeleted: boolean;
+ isNew: boolean;
+}
+
+@Component({
+ selector: 'eg-linked-circ-limit-sets',
+ templateUrl: './linked-circ-limit-sets.component.html'
+})
+
+export class LinkedCircLimitSetsComponent implements OnInit {
+
+ @ViewChild('errorString', { static: true }) errorString: StringComponent;
+
+ @Input() usedSetLimitList = {};
+ @Input() limitSetNames = {};
+ @Output() outputLinkedLimitSet: EventEmitter<any>;
+ linkedSetList = {};
+ linkedSet: any;
+ showLinkLimitSets: boolean;
+
+ constructor(
+ private idl: IdlService,
+ private toast: ToastService
+ ) {
+ this.outputLinkedLimitSet = new EventEmitter();
+ }
+
+ ngOnInit() {}
+
+ displayLinkedLimitSets() {
+ this.createEmptyLimitSetObject();
+ }
+
+ createFilledLimitSetObject(element) {
+ const newLinkedSetObject = new LinkedLimitSetObjects();
+ if (element.fallthrough() === 'f') { element.fallthrough(false); }
+ if (element.fallthrough() === 't') { element.fallthrough(true); }
+ if (element.active() === 'f') { element.active(false); }
+ if (element.active() === 't') { element.active(true); }
+ newLinkedSetObject.linkedLimitSet = element;
+ newLinkedSetObject.created = true;
+ newLinkedSetObject.isNew = false;
+ newLinkedSetObject.isDeleted = false;
+ this.linkedSetList[this.getObjectKeys().length] = newLinkedSetObject;
+ }
+
+ createEmptyLimitSetObject() {
+ const object = this.idl.create('ccmlsm');
+ const newLinkedSetObject = new LinkedLimitSetObjects();
+ newLinkedSetObject.linkedLimitSet = object;
+ newLinkedSetObject.linkedLimitSet.fallthrough(false);
+ newLinkedSetObject.linkedLimitSet.active(true);
+ newLinkedSetObject.isNew = true;
+ newLinkedSetObject.created = false;
+ newLinkedSetObject.isDeleted = false;
+ this.linkedSetList[this.getObjectKeys().length] = newLinkedSetObject;
+ }
+
+ onChange(object: any) {
+ this.linkedSet = object;
+ }
+
+ getObjectKeys() {
+ if (this.linkedSetList) {
+ return Object.keys(this.linkedSetList);
+ } else {
+ this.linkedSetList = {};
+ return Object.keys({});
+ }
+ }
+
+ addLinkedSet() {
+ if (this.linkedSet) {
+ if ( !this.usedSetLimitList[this.linkedSet.id]) {
+ this.createEmptyLimitSetObject();
+ this.linkedSetList[this.getObjectKeys().length - 1].linkedLimitSet.limit_set(this.linkedSet.id);
+ this.linkedSetList[this.getObjectKeys().length - 1].created = true;
+ this.emitLimitSet();
+ this.usedSetLimitList[this.linkedSet.id] = this.linkedSet.label;
+ } else {
+ this.errorString.current()
+ .then(str => this.toast.danger(str));
+ }
+ }
+ }
+
+ emitLimitSet() {
+ this.outputLinkedLimitSet.emit(this.linkedSetList);
+ }
+
+ removeLinkedSet(index) {
+ delete this.usedSetLimitList[this.linkedSetList[index].linkedLimitSet.limit_set()];
+ this.linkedSetList[index].isDeleted = true;
+ this.emitLimitSet();
+ }
+}
+
--- /dev/null
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {CircMatrixMatchpointComponent} from './circ-matrix-matchpoint.component';
+const routes: Routes = [{
+ path: '',
+ component: CircMatrixMatchpointComponent
+}];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+
+export class CircMatrixMatchpointRoutingModule {}
loadChildren: () =>
import('./survey/survey.module').then(m => m.SurveyModule)
}, {
+ path: 'config/circ_matrix_matchpoint',
+ loadChildren: () =>
+ import('./circ_matrix_matchpoint/circ-matrix-matchpoint.module').then(m => m.CircMatrixMatchpointModule)
+}, {
path: ':schema/:table',
component: BasicAdminPageComponent
}];