Angular port of the holdings and item attributes editors interfaces.
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Ruth Frasur <rfrasur@library.in.gov>
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
-<ng-container
- *ngFor="let val of getDisplayStrings(); let first = first">
- <ng-container *ngIf="joiner && !first">{{joiner}} </ng-container>
- <span [innerHTML]="val"></span>
+<ng-container *ngIf="routerLink">
+ <a [routerLink]="routerLink">
+ <ng-container
+ *ngFor="let val of getDisplayStrings(); let first = first">
+ <ng-container *ngIf="joiner && !first">{{joiner}} </ng-container>
+ <span [innerHTML]="val"></span>
+ </ng-container>
+ </a>
+</ng-container>
+
+<ng-container *ngIf="!routerLink">
+ <ng-container
+ *ngFor="let val of getDisplayStrings(); let first = first">
+ <ng-container *ngIf="joiner && !first">{{joiner}} </ng-container>
+ <span [innerHTML]="val"></span>
+ </ng-container>
</ng-container>
// If true, replace empty values with a non-collapsing space.
@Input() usePlaceholder: boolean;
+ // If provided, turn the display value into a link
+ @Input() routerLink: string;
+
constructor() {}
ngOnInit() {}
}
iconFormatLabel(code: string): string {
- if (this.ccvmMap) {
+ if (this.ccvmMap && this.ccvmMap.icon_format) {
const ccvm = this.ccvmMap.icon_format.filter(
format => format.code() === code)[0];
if (ccvm) {
<div class="d-flex">
<input type="text"
class="form-control"
- [ngClass]="{'text-success font-italic font-weight-bold': selected && selected.freetext}"
+ [id]="domId"
+ [ngClass]="{
+ 'text-success font-italic font-weight-bold': selected && selected.freetext,
+ 'form-control-sm': smallFormControl
+ }"
[placeholder]="placeholder"
[name]="name"
[disabled]="isDisabled"
}]
})
export class ComboboxComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges {
+ static domIdAuto = 0;
selected: ComboboxEntry;
click$: Subject<string>;
@ViewChild('defaultDisplayTemplate', { static: true}) defaultDisplayTemplate: TemplateRef<any>;
@ViewChildren(IdlClassTemplateDirective) idlClassTemplates: QueryList<IdlClassTemplateDirective>;
+ @Input() domId = 'eg-combobox-' + ComboboxComponent.domIdAuto++;
+
// Applies a name attribute to the input.
// Useful in forms.
@Input() name: string;
@Input() inputSize: number = null;
+ // If true, applies form-control-sm CSS
+ @Input() smallFormControl = false;
+
// Add a 'required' attribute to the input
isRequired: boolean;
@Input() set required(r: boolean) {
<eg-string #unsetString text="<Unset>" i18n-text></eg-string>
<eg-combobox #comboBox
+ [domId]="domId"
[startId]="startId"
[displayTemplate]="displayTemplate"
(onChange)="cboxChanged($event)"
})
export class ItemLocationSelectComponent
implements OnInit, AfterViewInit, ControlValueAccessor {
+ static domIdAuto = 0;
// Limit copy locations to those owned at or above org units where
// the user has work permissions for the provided permission code.
@Input() required: boolean;
+ @Input() domId = 'eg-item-location-select-' +
+ ItemLocationSelectComponent.domIdAuto++;
+
@ViewChild('comboBox', {static: false}) comboBox: ComboboxComponent;
@ViewChild('unsetString', {static: false}) unsetString: StringComponent;
path: 'item',
loadChildren: () => import('./item/item.module').then(m => m.ItemModule)
}, {
+ path: 'volcopy',
+ loadChildren: () =>
+ import('./volcopy/volcopy.module').then(m => m.VolCopyModule)
+ }, {
path: 'bib-from/:identType',
component: BibByIdentComponent
}
--- /dev/null
+<div class="d-flex">
+ <h3 class="mt-3" i18n>Holdings Preferences</h3>
+ <div class="flex-1"></div>
+ <div i18n class="font-italic">Changes are saved automatically.</div>
+</div>
+
+<div class="row">
+ <div class="col-lg-6">
+ <div class="row">
+ <div class="col-lg-12">
+ <div class="card">
+ <div class="card-header" i18n>Holdings Display Preferences</div>
+ <ul class="list-group list-group-flush">
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="hide-classification-column"
+ [(ngModel)]="volcopy.defaults.hidden.classification">
+ <label class="form-check-label" for="hide-classification-column" i18n>
+ Hide Call Number Classification Column
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="hide-prefix-column"
+ [(ngModel)]="volcopy.defaults.hidden.prefix">
+ <label class="form-check-label" for="hide-prefix-column" i18n>
+ Hide Call Number Prefix Column
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="hide-suffix-column"
+ [(ngModel)]="volcopy.defaults.hidden.suffix">
+ <label class="form-check-label" for="hide-suffix-column" i18n>
+ Hide Call Number Suffix Column
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="hide-generate_barcodes-column"
+ [(ngModel)]="volcopy.defaults.hidden.generate_barcodes">
+ <label class="form-check-label" for="hide-generate_barcodes-column" i18n>
+ Hide Generate Barcodes
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <!--
+ Copy_number_vc distinguishes from copy_number so the field
+ can appear in the volcopy UI and/or attr editor independently.
+ -->
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="hide-copy_number_vc-column"
+ [(ngModel)]="volcopy.defaults.hidden.copy_number_vc">
+ <label class="form-check-label" for="hide-copy_number_vc-column" i18n>
+ Hide Item Number
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="volcopy-unified-interface"
+ [(ngModel)]="volcopy.defaults.values.unified_display">
+ <label class="form-check-label" for="volcopy-unified-interface" i18n>
+ Unified Holdings and Item Attributes Display
+ </label>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-6">
+ <div class="row">
+ <div class="col-lg-12">
+ <div class="card">
+ <div class="card-header" i18n>Holdings Creation Defaults</div>
+ <ul class="list-group list-group-flush p-2">
+ <li class="list-group-item">
+ <div class="row">
+ <div class="col-lg-4" i18n>
+ <label for="default-classification" i18n>Default Classification</label>
+ </div>
+ <div class="col-lg-8">
+ <eg-combobox
+ domId="default-classification"
+ [selectedId]="volcopy.defaults.values.classification || 1"
+ [smallFormControl]="true"
+ (onChange)="volcopy.defaults.values.classification = $event ? $event.id : null">
+ <eg-combobox-entry *ngFor="let cls of volcopy.commonData.acn_class"
+ [entryId]="cls.id()" [entryLabel]="cls.name()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </div>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="row">
+ <div class="col-lg-4" i18n>
+ <label for="default-prefix" i18n>Default Prefix</label>
+ </div>
+ <div class="col-lg-8">
+ <eg-combobox
+ domId="default-prefix"
+ [smallFormControl]="true"
+ [startId]="volcopy.defaults.values.prefix || -1"
+ (onChange)="volcopy.defaults.values.prefix = $event ? $event.id : null">
+ <eg-combobox-entry
+ entryLabel="<None>" i18n-entryLabel [entryId]="-1">
+ </eg-combobox-entry>
+ <eg-combobox-entry *ngFor="let pfx of volcopy.commonData.acn_prefix"
+ [entryId]="pfx.id()" [entryLabel]="pfx.label()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </div>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="row">
+ <div class="col-lg-4" i18n>
+ <label for="default-suffix" i18n>Default Suffix</label>
+ </div>
+ <div class="col-lg-8">
+ <eg-combobox
+ domId="default-suffix"
+ [selectedId]="volcopy.defaults.values.suffix || -1"
+ [smallFormControl]="true"
+ (onChange)="volcopy.defaults.values.suffix = $event ? $event.id : null">
+ <eg-combobox-entry
+ entryLabel="<None>" i18n-entryLabel [entryId]="-1">
+ </eg-combobox-entry>
+ <eg-combobox-entry *ngFor="let sfx of volcopy.commonData.acn_suffix"
+ [entryId]="sfx.id()" [entryLabel]="sfx.label()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </div>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
+<hr class="p-2"/>
+<h3 i18n>Item Attribute Settings</h3>
+
+<div class="row">
+ <div class="col-lg-6">
+ <div class="card">
+ <div class="card-header" i18n>Item Attributes Behavior</div>
+ <ul class="list-group list-group-flush">
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="circ_lib_mod_with_owning_lib-column"
+ [(ngModel)]="volcopy.defaults.values.circ_lib_mod_with_owning_lib">
+ <label class="form-check-label"
+ for="circ_lib_mod_with_owning_lib-column" i18n>
+ Change Circ Lib When Owning Lib Changes
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <eg-org-select
+ domId="statcat_filter"
+ placeholder="Stat Cat Filter..." i18n-placeholder
+ [initialOrgId]="volcopy.defaults.values.statcat_filter"
+ (onChange)="volcopy.defaults.values.statcat_filter = $event ? $event.id() : null">
+ </eg-org-select>
+ <label class="ml-2" for="statcat_filter" i18n>
+ Default Stat Cat Library Filter
+ </label>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+</div>
+
+
+<hr class="p-2"/>
+
+<h3 i18n>Hide Item Attributes</h3>
+<span class="font-italic" i18n>
+ Selected Fields Will be <span class="font-weight-bold">Hidden</span>
+ from the Item Attributes Form.
+</span>
+
+<div class="row d-flex pb-5">
+
+ <!-- COLUMN 1 -->
+ <div class="flex-1 p-1">
+ <div class="card">
+ <div class="card-header" i18n>Identification</div>
+ <ul class="list-group list-group-flush">
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-status-attr"
+ [(ngModel)]="volcopy.defaults.hidden.status">
+ <label class="form-check-label" for="show-status-attr" i18n>
+ Status
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-barcode-attr"
+ [(ngModel)]="volcopy.defaults.hidden.barcode">
+ <label class="form-check-label" for="show-barcode-attr" i18n>
+ Barcode
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-create_date-attr"
+ [(ngModel)]="volcopy.defaults.hidden.create_date">
+ <label class="form-check-label" for="show-create_date-attr" i18n>
+ Creation Date
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-active_date-attr"
+ [(ngModel)]="volcopy.defaults.hidden.active_date">
+ <label class="form-check-label" for="show-active_date-attr" i18n>
+ Activation Date
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-creator-attr"
+ [(ngModel)]="volcopy.defaults.hidden.creator">
+ <label class="form-check-label" for="show-creator-attr" i18n>
+ Creator
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-edit_date-attr"
+ [(ngModel)]="volcopy.defaults.hidden.edit_date">
+ <label class="form-check-label" for="show-edit_date-attr" i18n>
+ Last Edit Date
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-editor-attr"
+ [(ngModel)]="volcopy.defaults.hidden.editor">
+ <label class="form-check-label" for="show-editor-attr" i18n>
+ Last Editor
+ </label>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <!-- COLUMN 2 -->
+ <div class="flex-1 p-1">
+ <div class="card">
+ <div class="card-header" i18n>Location</div>
+ <ul class="list-group list-group-flush">
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-location-attr"
+ [(ngModel)]="volcopy.defaults.hidden.location">
+ <label class="form-check-label" for="show-location-attr" i18n>
+ Location
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-circ_lib-attr"
+ [(ngModel)]="volcopy.defaults.hidden.circ_lib">
+ <label class="form-check-label" for="show-circ_lib-attr" i18n>
+ Circulating Library
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-owning_lib-attr"
+ [(ngModel)]="volcopy.defaults.hidden.owning_lib">
+ <label class="form-check-label" for="show-owning_lib-attr" i18n>
+ Owning Library
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-copy_number-attr"
+ [(ngModel)]="volcopy.defaults.hidden.copy_number">
+ <label class="form-check-label" for="show-copy_number-attr" i18n>
+ Copy Number
+ </label>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <!-- COLUMN 3 -->
+
+ <div class="flex-1 p-1">
+ <div class="card">
+ <div class="card-header" i18n>Circulation</div>
+ <ul class="list-group list-group-flush">
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-circulate-attr"
+ [(ngModel)]="volcopy.defaults.hidden.circulate">
+ <label class="form-check-label" for="show-circulate-attr" i18n>
+ Circulate
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-holdable-attr"
+ [(ngModel)]="volcopy.defaults.hidden.holdable">
+ <label class="form-check-label" for="show-holdable-attr" i18n>
+ Holdable
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-age_protect-attr"
+ [(ngModel)]="volcopy.defaults.hidden.age_protect">
+ <label class="form-check-label" for="show-age_protect-attr" i18n>
+ Aged-Based Hold Protection
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-floating-attr"
+ [(ngModel)]="volcopy.defaults.hidden.floating">
+ <label class="form-check-label" for="show-floating-attr" i18n>
+ Floating
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-loan_duration-attr"
+ [(ngModel)]="volcopy.defaults.hidden.loan_duration">
+ <label class="form-check-label" for="show-loan_duration-attr" i18n>
+ Loan Duration
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-fine_level-attr"
+ [(ngModel)]="volcopy.defaults.hidden.fine_level">
+ <label class="form-check-label" for="show-fine_level-attr" i18n>
+ Fine Level
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-circ_as_type-attr"
+ [(ngModel)]="volcopy.defaults.hidden.circ_as_type">
+ <label class="form-check-label" for="show-circ_as_type-attr" i18n>
+ Circulate As Type
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-circ_modifier-attr"
+ [(ngModel)]="volcopy.defaults.hidden.circ_modifier">
+ <label class="form-check-label" for="show-circ_modifier-attr" i18n>
+ Circulation Modifier
+ </label>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <!-- COLUMN 4 -->
+
+ <div class="flex-1 p-1">
+ <div class="card">
+ <div class="card-header" i18n>Miscellaneous</div>
+ <ul class="list-group list-group-flush">
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-copy_alerts-attr"
+ [(ngModel)]="volcopy.defaults.hidden.copy_alerts">
+ <label class="form-check-label" for="show-copy_alerts-attr" i18n>
+ Item Alerts
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-deposit-attr"
+ [(ngModel)]="volcopy.defaults.hidden.deposit">
+ <label class="form-check-label" for="show-deposit-attr" i18n>
+ Deposit
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-deposit_amount-attr"
+ [(ngModel)]="volcopy.defaults.hidden.deposit_amount">
+ <label class="form-check-label" for="show-deposit_amount-attr" i18n>
+ Deposit Amount
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-price-attr"
+ [(ngModel)]="volcopy.defaults.hidden.price">
+ <label class="form-check-label" for="show-price-attr" i18n>
+ Price
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-opac_visible-attr"
+ [(ngModel)]="volcopy.defaults.hidden.opac_visible">
+ <label class="form-check-label" for="show-opac_visible-attr" i18n>
+ OPAC Visible
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-ref-attr"
+ [(ngModel)]="volcopy.defaults.hidden.ref">
+ <label class="form-check-label" for="show-ref-attr" i18n>
+ Reference
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-cost-attr"
+ [(ngModel)]="volcopy.defaults.hidden.cost">
+ <label class="form-check-label" for="show-cost-attr" i18n>
+ Cost
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-mint_condition-attr"
+ [(ngModel)]="volcopy.defaults.hidden.mint_condition">
+ <label class="form-check-label" for="show-mint_condition-attr" i18n>
+ Quality
+ </label>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <!-- COLUMN 5 -->
+
+ <div class="flex-1 p-1">
+ <div class="card">
+ <div class="card-header" i18n>Statistics</div>
+ <ul class="list-group list-group-flush">
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-copy_tags-attr"
+ [(ngModel)]="volcopy.defaults.hidden.copy_tags">
+ <label class="form-check-label" for="show-copy_tags-attr" i18n>
+ Add Item Tags
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-statcat_filter-attr"
+ [(ngModel)]="volcopy.defaults.hidden.statcat_filter">
+ <label class="form-check-label" for="show-statcat_filter-attr" i18n>
+ Stat Cat Filter
+ </label>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="show-statcats-attr"
+ [(ngModel)]="volcopy.defaults.hidden.statcats">
+ <label class="form-check-label" for="show-statcats-attr" i18n>
+ Statistical Categories
+ </label>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+</div>
+
--- /dev/null
+import {Component, Input, OnInit, ViewChild, DoCheck} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {tap} from 'rxjs/operators';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {VolCopyContext} from './volcopy';
+import {VolCopyService} from './volcopy.service';
+
+@Component({
+ selector: 'eg-volcopy-config',
+ templateUrl: 'config.component.html'
+})
+export class VolCopyConfigComponent implements OnInit, DoCheck {
+
+ @Input() context: VolCopyContext;
+
+ defaultsCopy: any;
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private idl: IdlService,
+ public volcopy: VolCopyService
+ ) {}
+
+ ngOnInit() {
+ console.debug('DEFAULTS', this.volcopy.defaults);
+
+ // Not an IDL object, but clones just the same
+ this.defaultsCopy = this.idl.clone(this.volcopy.defaults);
+ }
+
+ // Watch for changes in the form and auto-save them.
+ ngDoCheck() {
+ const hidden = this.volcopy.defaults.hidden;
+ for (const key in hidden) {
+ if (hidden[key] !== this.defaultsCopy.hidden[key]) {
+ this.save();
+ return;
+ }
+ }
+
+ const values = this.volcopy.defaults.values;
+ for (const key in values) {
+ if (values[key] !== this.defaultsCopy.values[key]) {
+ this.save();
+ return;
+ }
+ }
+ }
+
+ save() {
+ this.volcopy.saveDefaults().then(_ =>
+ this.defaultsCopy = this.idl.clone(this.volcopy.defaults)
+ );
+ }
+}
+
+
--- /dev/null
+
+
+<!-- We ask this question a lot. Here's a handy template -->
+<ng-template #yesNoSelect let-field="field">
+ <eg-combobox domId="{{field}}-input"
+ [required]="true" [ngModel]="values['field']"
+ (ngModelChange)="values[field] = $event ? $event.id : null">
+ <eg-combobox-entry entryId="t" entryLabel="Yes" i18n-entryLabel>
+ </eg-combobox-entry>
+ <eg-combobox-entry entryId="f" entryLabel="No" i18n-entryLabel>
+ </eg-combobox-entry>
+ </eg-combobox>
+</ng-template>
+
+<!-- this one is also repeated a lot -->
+<ng-template #batchAttr let-field="field" let-required="required"
+ let-label="label" let-template="template" let-displayAs="displayAs">
+ <eg-batch-item-attr
+ [name]="field"
+ [label]="label || copyFieldLabel(field)"
+ [valueRequired]="required"
+ [displayAs]="displayAs"
+ [editInputDomId]="field + '-input'"
+ [editTemplate]="template"
+ [labelCounts]="itemAttrCounts(field)"
+ (valueCleared)="applyCopyValue(field, null)"
+ (changesSaved)="applyCopyValue(field, undefined, $event)">
+ </eg-batch-item-attr>
+</ng-template>
+
+<!-- Copy Templates -->
+<div class="row border rounded border-dark pt-2 pb-2 bg-faint">
+ <div class="col-lg-1 font-weight-bold" i18n>Templates:</div>
+ <div class="col-lg-4">
+ <eg-combobox #copyTemplateCbox domId="template-select"
+ [allowFreeText]="true" [entries]="volcopy.templateNames">
+ </eg-combobox>
+ </div>
+ <div class="col-lg-7 d-flex">
+ <button class="btn btn-outline-dark mr-2" (click)="applyTemplate()" i18n>Apply</button>
+ <button class="btn btn-outline-dark mr-2" (click)="saveTemplate()" i18n>Save</button>
+
+ <!--
+ The typical approach of wrapping a file input in a <label> results
+ in button-ish things that have slightly different dimensions.
+ Instead have a button activate a hidden file input.
+ -->
+ <button class="btn btn-outline-dark mr-2" (click)="templateFile.click()">
+ <input type="file" class="d-none" #templateFile
+ (change)="importTemplate($event)" id="template-file-upload"/>
+ <span i18n>Import</span>
+ </button>
+
+ <a (click)="exportTemplate($event)"
+ download="export_copy_template.json" [href]="exportTemplateUrl()">
+ <button class="btn btn-outline-dark mr-2" i18n>Export</button>
+ </a>
+
+ <div class="flex-1"> </div>
+ <button class="btn btn-outline-danger mr-2"
+ (click)="deleteTemplate()" i18n>Delete Template</button>
+ </div>
+</div>
+
+
+<div class="row d-flex">
+
+ <!-- COLUMN 1 -->
+ <div class="flex-1 p-1">
+ <div class="p-1"><h4 class="font-weight-bold" i18n>Identification</h4></div>
+
+
+ <div class="mb-1" *ngIf="displayAttr('status')">
+
+ <ng-container *ngIf="statusEditable(); else noEditStat">
+ <ng-template #statusTemplate>
+ <eg-combobox domId="status-input"
+ (ngModelChange)="values['status'] = $event ? $event.id : null"
+ [ngModel]="values['status']">
+ <eg-combobox-entry
+ *ngFor="let stat of volcopy.commonData.acp_status"
+ [entryId]="stat.id()" [entryLabel]="stat.name()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'status',template:statusTemplate}">
+ </ng-container>
+ </ng-container>
+
+ <ng-template #noEditStat>
+ <eg-batch-item-attr label="Status" i18n-label [readOnly]="true"
+ [labelCounts]="itemAttrCounts('status')">
+ </eg-batch-item-attr>
+ </ng-template>
+ </div>
+
+ <div class="mb-1" *ngIf="displayAttr('barcode')">
+ <eg-batch-item-attr label="Barcode" i18n-label
+ [readOnly]="true" [labelCounts]="itemAttrCounts('barcode')">
+ </eg-batch-item-attr>
+ </div>
+
+ <div class="mb-1" *ngIf="displayAttr('create_date')">
+ <eg-batch-item-attr label="Creation Date" i18n-label [readOnly]="true"
+ [labelCounts]="itemAttrCounts('create_date')">
+ </eg-batch-item-attr>
+ </div>
+
+ <div class="mb-1" *ngIf="displayAttr('active_date')">
+ <eg-batch-item-attr label="Active Date" i18n-label [readOnly]="true"
+ [labelCounts]="itemAttrCounts('active_date')">
+ </eg-batch-item-attr>
+ </div>
+
+ <div class="mb-1" *ngIf="displayAttr('creator')">
+ <eg-batch-item-attr label="Creator" i18n-label [readOnly]="true"
+ [labelCounts]="itemAttrCounts('creator')">
+ </eg-batch-item-attr>
+ </div>
+
+ <div class="mb-1" *ngIf="displayAttr('edit_date')">
+ <eg-batch-item-attr label="Last Edit Date" i18n-label [readOnly]="true"
+ [labelCounts]="itemAttrCounts('edit_date')">
+ </eg-batch-item-attr>
+ </div>
+
+ <div class="mb-1" *ngIf="displayAttr('editor')">
+ <eg-batch-item-attr label="Last Editor" i18n-label [readOnly]="true"
+ [labelCounts]="itemAttrCounts('editor')">
+ </eg-batch-item-attr>
+ </div>
+
+ </div>
+
+ <!-- COLUMN 2 -->
+ <div class="flex-1 p-1">
+ <div class="p-1"><h4 class="font-weight-bold" i18n>Location</h4></div>
+
+ <div *ngIf="displayAttr('location')">
+ <ng-template #locationTemplate>
+ <eg-item-location-select (valueChange)="values['location'] = $event"
+ domId='location-input' [required]="true" permFilter="UPDATE_COPY">
+ </eg-item-location-select>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'location',required:true,template:locationTemplate}">
+ </ng-container>
+ </div>
+
+ <div *ngIf="displayAttr('circ_lib')">
+ <ng-template #circLibTemplate>
+ <eg-org-select
+ domId="circ_lib-input"
+ (onChange)="values['circ_lib'] = $event ? $event.id() : null"
+ [hideOrgs]="volcopy.hideVolOrgs"
+ [limitPerms]="['UPDATE_COPY']">
+ </eg-org-select>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'circ_lib',required:true,template:circLibTemplate}">
+ </ng-container>
+ </div>
+
+ <div *ngIf="displayAttr('owning_lib')">
+ <eg-string #olLabel text="Owning Library" i18n-text></eg-string>
+ <ng-template #owningLibTemplate>
+ <eg-org-select
+ domId="owning_lib-input"
+ (onChange)="values['owning_lib'] = $event ? $event.id() : null"
+ [hideOrgs]="volcopy.hideVolOrgs"
+ [limitPerms]="['UPDATE_COPY']">
+ </eg-org-select>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'owning_lib',required:true,template:owningLibTemplate,label:olLabel.text}">
+ </ng-container>
+ </div>
+
+ <div *ngIf="displayAttr('copy_number')">
+ <ng-template #copyNumberTemplate>
+ <input type="number" class="form-control"
+ id="copy_number-input" [(ngModel)]="values['copy_number']"/>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'copy_number',template:copyNumberTemplate}">
+ </ng-container>
+ </div>
+ </div>
+
+ <!-- COLUMN 3 -->
+
+ <div class="flex-1 p-1">
+ <div class="p-1"><h4 class="font-weight-bold" i18n>Circulation</h4></div>
+
+ <div *ngIf="displayAttr('circulate')">
+ <ng-template #circulateTemplate>
+ <ng-container *ngTemplateOutlet="yesNoSelect;context:{field:'circulate'}">
+ </ng-container>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'circulate',required:true,template:circulateTemplate,displayAs:'bool'}">
+ </ng-container>
+ </div>
+
+ <div *ngIf="displayAttr('holdable')">
+ <ng-template #holdableTemplate>
+ <ng-container *ngTemplateOutlet="yesNoSelect;context:{field:'holdable'}">
+ </ng-container>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'holdable',required:true,template:holdableTemplate,displayAs:'bool'}">
+ </ng-container>
+ </div>
+
+ <div *ngIf="displayAttr('age_protect')">
+ <ng-template #ageProtectTemplate>
+ <eg-combobox domId="age_protect-input"
+ (ngModelChange)="values['age_protect'] = $event ? $event.id : null"
+ [ngModel]="values['age_protect']">
+ <eg-combobox-entry
+ *ngFor="let rule of volcopy.commonData.acp_age_protect"
+ [entryId]="rule.id()" [entryLabel]="rule.name()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'age_protect',template:ageProtectTemplate}">
+ </ng-container>
+ </div>
+
+ <div *ngIf="displayAttr('floating')">
+ <ng-template #floatingTemplate>
+ <eg-combobox domId="floating-input"
+ (ngModelChange)="values['floating'] = $event ? $event.id : null"
+ [ngModel]="values['floating']">
+ <eg-combobox-entry
+ *ngFor="let grp of volcopy.commonData.acp_floating_group"
+ [entryId]="grp.id()" [entryLabel]="grp.name()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'floating',template:floatingTemplate}">
+ </ng-container>
+ </div>
+
+ <div *ngIf="displayAttr('loan_duration')">
+ <eg-string #loanDurationShort i18n-text text="Short"></eg-string>
+ <eg-string #loanDurationNormal i18n-text text="Normal"></eg-string>
+ <eg-string #loanDurationLong i18n-text text="Long"></eg-string>
+
+ <ng-template #loanDurationTemplate>
+ <select class="form-control"
+ id="loan_duration-input" [(ngModel)]="values['loan_duration']">
+ <option value="1" i18n>{{loanDurationShort.text}}</option>
+ <option value="2" i18n>{{loanDurationNormal.text}}</option>
+ <option value="3" i18n>{{loanDurationLong.text}}</option>
+ </select>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'loan_duration',required:true,template:loanDurationTemplate}">
+ </ng-container>
+ </div>
+
+ <div *ngIf="displayAttr('fine_level')">
+ <eg-string #fineLevelLow i18n-text text="Low"></eg-string>
+ <eg-string #fineLevelNormal i18n-text text="Normal"></eg-string>
+ <eg-string #fineLevelHigh i18n-text text="High"></eg-string>
+
+ <ng-template #fineLevelTemplate>
+ <select class="form-control"
+ id="fine_level-input" [(ngModel)]="values['fine_level']">
+ <option value="1" i18n>{{fineLevelLow.text}}</option>
+ <option value="2" i18n>{{fineLevelNormal.text}}</option>
+ <option value="3" i18n>{{fineLevelHigh.text}}</option>
+ </select>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'fine_level',required:true,template:fineLevelTemplate}">
+ </ng-container>
+ </div>
+
+ <div *ngIf="displayAttr('circ_as_type')">
+ <ng-template #circAsTypeTemplate>
+ <eg-combobox domId="circ_as_type-input"
+ (ngModelChange)="values['circ_as_type'] = $event ? $event.id : null"
+ [ngModel]="values['circ_as_type']">
+ <eg-combobox-entry *ngFor="let map of volcopy.commonData.acp_item_type_map"
+ [entryId]="map.code()" [entryLabel]="map.value()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'circ_as_type',template:circAsTypeTemplate}">
+ </ng-container>
+ </div>
+
+ <div *ngIf="displayAttr('circ_modifier')">
+ <ng-template #circModifierTemplate>
+ <select class="form-control" id='circ_modifier-input'
+ [(ngModel)]="values['circ_modifier']">
+ <option [value]="null" i18n><Unset></option>
+ <option *ngFor="let mod of volcopy.commonData.acp_circ_modifier"
+ value="{{mod.code()}}">{{mod.name()}}</option>
+ </select>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'circ_modifier',template:circModifierTemplate}">
+ </ng-container>
+ </div>
+
+ </div>
+
+ <!-- COLUMN 4 -->
+
+ <div class="flex-1 p-1">
+ <div class="p-1"><h4 class="font-weight-bold" i18n>Miscellaneous</h4></div>
+
+ <!-- Adding this for sites that still use alert messages (we do)
+ <div>
+ <ng-template #alertMessageTemplate>
+ <textarea rows="3" class="form-control" id="alert-message-input"
+ [(ngModel)]="values['alert_message']">
+ </textarea>
+ </ng-template>
+ <eg-batch-item-attr label="Alert Message" i18n-label
+ editInputDomId="alert-message-input"
+ [editTemplate]="alertMessageTemplate"
+ [labelCounts]="itemAttrCounts('alert_message')"
+ (changesSaved)="applyCopyValue('alert_message')">
+ </eg-batch-item-attr>
+ </div>
+ -->
+
+ <div class="border rounded m-1" *ngIf="displayAttr('copy_alerts')">
+ <eg-copy-alerts-dialog #copyAlertsDialog></eg-copy-alerts-dialog>
+ <div class="batch-header font-weight-bold p-2" i18n>Add Item Alerts</div>
+ <div class="p-1">
+ <button class="btn btn-outline-dark" (click)="openCopyAlerts()" i18n>
+ Item Alerts
+ </button>
+ </div>
+ </div>
+
+ <div *ngIf="displayAttr('deposit')">
+ <ng-template #depositTemplate>
+ <ng-container *ngTemplateOutlet="yesNoSelect;context:{field:'deposit'}">
+ </ng-container>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'deposit',required:true,template:depositTemplate,displayAs:'bool'}">
+ </ng-container>
+ </div>
+
+ <div *ngIf="displayAttr('deposit_amount')">
+ <ng-template #depositAmountTemplate>
+ <input type="number" class="form-control"
+ id="deposit_amount-input" [(ngModel)]="values['deposit_amount']"/>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'deposit_amount',required:true,template:depositAmountTemplate,displayAs:'currency'}">
+ </ng-container>
+ </div>
+
+ <div *ngIf="displayAttr('price')">
+ <ng-template #priceTemplate>
+ <input type="number" class="form-control"
+ id="price-input" [(ngModel)]="values['price']"/>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'price',template:priceTemplate,displayAs:'currency'}">
+ </ng-container>
+ </div>
+
+ <div *ngIf="displayAttr('opac_visible')">
+ <ng-template #opacVisibleTemplate>
+ <ng-container *ngTemplateOutlet="yesNoSelect;context:{field:'opac_visible'}">
+ </ng-container>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'opac_visible',required:true,template:opacVisibleTemplate,displayAs:'bool'}">
+ </ng-container>
+ </div>
+
+ <div *ngIf="displayAttr('ref')">
+ <ng-template #refTemplate>
+ <ng-container *ngTemplateOutlet="yesNoSelect;context:{field:'ref'}">
+ </ng-container>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'ref',required:true,template:refTemplate,displayAs:'bool'}">
+ </ng-container>
+ </div>
+
+ <div *ngIf="displayAttr('cost')">
+ <ng-template #costTemplate>
+ <input type="number" class="form-control"
+ id="cost-input" [(ngModel)]="values['cost']"/>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'cost',template:costTemplate,displayAs:'currency'}">
+ </ng-container>
+ </div>
+
+ <div *ngIf="displayAttr('mint_condition')">
+ <eg-string #mintConditionYes i18n-text text="Good"></eg-string>
+ <eg-string #mintConditionNo i18n-text text="Damaged"></eg-string>
+
+ <ng-template #mintConditionTemplate>
+ <select class="form-control"
+ id="mint_condition-input" [(ngModel)]="values['mint_condition']">
+ <option value="t" i18n>{{mintConditionYes.text}}</option>
+ <option value="f" i18n>{{mintConditionNo.text}}</option>
+ </select>
+ </ng-template>
+ <ng-container *ngTemplateOutlet="batchAttr;
+ context:{field:'mint_condition',template:mintConditionTemplate}">
+ </ng-container>
+ </div>
+
+ </div>
+
+ <!-- COLUMN 5 -->
+ <div class="flex-1 p-1">
+ <div class="p-1"><h4 class="font-weight-bold" i18n>Statistics</h4></div>
+
+ <div class="border rounded m-1" *ngIf="displayAttr('copy_tags')">
+ <eg-copy-tags-dialog #copyTagsDialog></eg-copy-tags-dialog>
+ <div class="batch-header font-weight-bold p-2" i18n>Add Item Tags</div>
+ <div class="p-1">
+ <button class="btn btn-outline-dark" (click)="openCopyTags()" i18n>
+ Item Tags
+ </button>
+ </div>
+ </div>
+
+ <div class="border rounded m-1" *ngIf="displayAttr('statcat_filter')">
+ <div class="batch-header font-weight-bold p-2" i18n>Stat Cat Filter</div>
+ <div class="p-1">
+ <eg-org-select
+ domId="statcat_filter-select"
+ placeholder="Stat Cat Filter..." i18n-placeholder
+ [initialOrgId]="statCatFilter"
+ (onChange)="statCatFilter = $event ? $event.id() : null">
+ </eg-org-select>
+ </div>
+ </div>
+
+ <ng-container *ngIf="displayAttr('statcats')">
+ <div *ngFor="let cat of statCats()">
+ <ng-template #statCatTemplate>
+ <eg-combobox domId="stat-cat-input-{{cat.id()}}"
+ (ngModelChange)="statCatValues[cat.id()] = $event ? $event.id : null"
+ [ngModel]="statCatValues[cat.id()]">
+ <eg-combobox-entry *ngFor="let entry of cat.entries()"
+ [entryId]="entry.id()" [entryLabel]="entry.value()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </ng-template>
+ <eg-batch-item-attr label="{{cat.name()}} ({{orgSn(cat.owner())}})" i18n-label
+ name="stat_cat_{{cat.id()}}" editInputDomId="stat-cat-input-{{cat.id()}}"
+ [editTemplate]="statCatTemplate"
+ [labelCounts]="statCatCounts(cat.id())"
+ (valueCleared)="statCatChanged(cat.id(), true)"
+ (changesSaved)="statCatChanged(cat.id())">
+ </eg-batch-item-attr>
+ </div>
+ </ng-container>
+ </div>
+</div>
+
+
--- /dev/null
+import {Component, Input, OnInit, AfterViewInit, ViewChild,
+ EventEmitter, Output, QueryList, ViewChildren} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {SafeUrl} from '@angular/platform-browser';
+import {tap} from 'rxjs/operators';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {OrgService} from '@eg/core/org.service';
+import {StoreService} from '@eg/core/store.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
+import {VolCopyContext} from './volcopy';
+import {VolCopyService} from './volcopy.service';
+import {FormatService} from '@eg/core/format.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {CopyAlertsDialogComponent
+ } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
+import {CopyTagsDialogComponent
+ } from '@eg/staff/share/holdings/copy-tags-dialog.component';
+import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {BatchItemAttrComponent, BatchChangeSelection
+ } from '@eg/staff/share/holdings/batch-item-attr.component';
+import {FileExportService} from '@eg/share/util/file-export.service';
+
+@Component({
+ selector: 'eg-copy-attrs',
+ templateUrl: 'copy-attrs.component.html',
+
+ // Match the header of the batch attrs component
+ styles: [
+ `.batch-header {background-color: #EBF4FA;}`,
+ `.template-row {background-color: #EBF4FA;}`
+ ]
+})
+export class CopyAttrsComponent implements OnInit, AfterViewInit {
+
+ @Input() context: VolCopyContext;
+
+ // Batch values applied from the form.
+ // Some values are scalar, some IdlObjects depending on copy fleshyness.
+ values: {[field: string]: any} = {};
+
+ // Map of stat ID to entry ID.
+ statCatValues: {[statId: number]: number} = {};
+
+ loanDurationLabelMap: {[level: number]: string} = {};
+ fineLevelLabelMap: {[level: number]: string} = {};
+
+ statCatFilter: number;
+
+ @ViewChild('loanDurationShort', {static: false})
+ loanDurationShort: StringComponent;
+ @ViewChild('loanDurationNormal', {static: false})
+ loanDurationNormal: StringComponent;
+ @ViewChild('loanDurationLong', {static: false})
+ loanDurationLong: StringComponent;
+
+ @ViewChild('fineLevelLow', {static: false})
+ fineLevelLow: StringComponent;
+ @ViewChild('fineLevelNormal', {static: false})
+ fineLevelNormal: StringComponent;
+ @ViewChild('fineLevelHigh', {static: false})
+ fineLevelHigh: StringComponent;
+
+ @ViewChild('mintConditionYes', {static: false})
+ mintConditionYes: StringComponent;
+ @ViewChild('mintConditionNo', {static: false})
+ mintConditionNo: StringComponent;
+
+ @ViewChild('copyAlertsDialog', {static: false})
+ private copyAlertsDialog: CopyAlertsDialogComponent;
+
+ @ViewChild('copyTagsDialog', {static: false})
+ private copyTagsDialog: CopyTagsDialogComponent;
+
+ @ViewChild('copyTemplateCbox', {static: false})
+ copyTemplateCbox: ComboboxComponent;
+
+ @ViewChildren(BatchItemAttrComponent)
+ batchAttrs: QueryList<BatchItemAttrComponent>;
+
+ // Emitted when the save-ability of this form changes.
+ @Output() canSaveChange: EventEmitter<boolean> = new EventEmitter<boolean>();
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private evt: EventService,
+ private idl: IdlService,
+ private org: OrgService,
+ private net: NetService,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private holdings: HoldingsService,
+ private format: FormatService,
+ private store: StoreService,
+ private fileExport: FileExportService,
+ public volcopy: VolCopyService
+ ) { }
+
+ ngOnInit() {
+ this.statCatFilter = this.volcopy.defaults.values.statcat_filter;
+ }
+
+ ngAfterViewInit() {
+
+ const tmpl = this.store.getLocalItem('cat.copy.last_template');
+ if (tmpl) {
+ // avoid Express Changed warning w/ timeout
+ setTimeout(() => this.copyTemplateCbox.selectedId = tmpl);
+ }
+
+ this.loanDurationLabelMap[1] = this.loanDurationShort.text;
+ this.loanDurationLabelMap[2] = this.loanDurationNormal.text;
+ this.loanDurationLabelMap[3] = this.loanDurationLong.text;
+
+ this.fineLevelLabelMap[1] = this.fineLevelLow.text;
+ this.fineLevelLabelMap[2] = this.fineLevelNormal.text;
+ this.fineLevelLabelMap[3] = this.fineLevelHigh.text;
+
+ }
+
+ statCats(): IdlObject[] {
+ if (this.statCatFilter) {
+ const orgs = this.org.descendants(this.statCatFilter, true);
+
+ return this.volcopy.commonData.acp_stat_cat.filter(
+ sc => orgs.includes(sc.owner()));
+
+ } else {
+
+ return this.volcopy.commonData.acp_stat_cat;
+ }
+ }
+
+
+ orgSn(orgId: number): string {
+ return orgId ? this.org.get(orgId).shortname() : '';
+ }
+
+ statCatCounts(catId: number): {[value: string]: number} {
+ catId = Number(catId);
+ const counts = {};
+
+ this.context.copyList().forEach(copy => {
+ const entry = copy.stat_cat_entries()
+ .filter(e => e.stat_cat() === catId)[0];
+
+ let value = '';
+ if (entry) {
+ if (this.volcopy.statCatEntryMap[entry.id()]) {
+ value = this.volcopy.statCatEntryMap[entry.id()].value();
+ } else {
+ // Map to a remote stat cat. Ignore.
+ return;
+ }
+ }
+
+ if (counts[value] === undefined) {
+ counts[value] = 0;
+ }
+ counts[value]++;
+ });
+
+ return counts;
+ }
+
+ itemAttrCounts(field: string): {[value: string]: number} {
+
+ const counts = {};
+ this.context.copyList().forEach(copy => {
+ const value = this.getFieldDisplayValue(field, copy);
+
+ if (counts[value] === undefined) {
+ counts[value] = 0;
+ }
+ counts[value]++;
+ });
+
+ return counts;
+ }
+
+ getFieldDisplayValue(field: string, copy: IdlObject): string {
+
+ // Some fields don't live directly on the copy.
+ if (field === 'owning_lib') {
+ return this.org.get(
+ copy.call_number().owning_lib()).shortname() +
+ ' : ' + copy.call_number().label();
+ }
+
+ const value = copy[field]();
+
+ if (!value && value !== 0) { return ''; }
+
+ switch (field) {
+
+ case 'status':
+ return this.volcopy.copyStatuses[value].name();
+
+ case 'location':
+ return value.name() +
+ ' (' + this.org.get(value.owning_lib()).shortname() + ')';
+
+ case 'edit_date':
+ case 'create_date':
+ case 'active_date':
+ return this.format.transform(
+ {datatype: 'timestamp', value: value});
+
+ case 'editor':
+ case 'creator':
+ return value.usrname();
+
+ case 'circ_lib':
+ return this.org.get(value).shortname();
+
+ case 'age_protect':
+ const rule = this.volcopy.commonData.acp_age_protect.filter(
+ r => r.id() === Number(value))[0];
+ return rule ? rule.name() : '';
+
+ case 'floating':
+ const grp = this.volcopy.commonData.acp_floating_group.filter(
+ g => g.id() === Number(value))[0];
+ return grp ? grp.name() : '';
+
+ case 'loan_duration':
+ return this.loanDurationLabelMap[value];
+
+ case 'fine_level':
+ return this.fineLevelLabelMap[value];
+
+ case 'circ_as_type':
+ const map = this.volcopy.commonData.acp_item_type_map.filter(
+ m => m.code() === value)[0];
+ return map ? map.value() : '';
+
+ case 'circ_modifier':
+ const mod = this.volcopy.commonData.acp_circ_modifier.filter(
+ m => m.code() === value)[0];
+ return mod ? mod.name() : '';
+
+ case 'mint_condition':
+ if (!this.mintConditionYes) { return ''; }
+ return value === 't' ?
+ this.mintConditionYes.text : this.mintConditionNo.text;
+ }
+
+ return value;
+ }
+
+ copyWantsChange(copy: IdlObject, field: string,
+ changeSelection: BatchChangeSelection): boolean {
+ const disValue = this.getFieldDisplayValue(field, copy);
+ return changeSelection[disValue] === true;
+ }
+
+ applyCopyValue(field: string, value?: any, changeSelection?: BatchChangeSelection) {
+ if (value === undefined) {
+ value = this.values[field];
+ } else {
+ this.values[field] = value;
+ }
+
+ if (field === 'owning_lib') {
+ this.owningLibChanged(value, changeSelection);
+
+ } else {
+
+ this.context.copyList().forEach(copy => {
+ if (!copy[field] || copy[field]() === value) { return; }
+
+ // Change selection indicates which items should be modified
+ // based on the display value for the selected field at
+ // time of editing.
+ if (changeSelection &&
+ !this.copyWantsChange(copy, field, changeSelection)) {
+ return;
+ }
+
+ copy[field](value);
+ copy.ischanged(true);
+ });
+ }
+
+ this.emitSaveChange();
+ }
+
+ owningLibChanged(orgId: number, changeSelection?: BatchChangeSelection) {
+ if (!orgId) { return; }
+
+ // Map existing vol IDs to their replacments.
+ const newVols: any = {};
+
+ this.context.copyList().forEach(copy => {
+
+ if (changeSelection &&
+ !this.copyWantsChange(copy, 'owning_lib', changeSelection)) {
+ return;
+ }
+
+ // Change the copy circ lib to match the new owning lib
+ // if configured to do so.
+ if (this.volcopy.defaults.values.circ_lib_mod_with_owning_lib) {
+ if (copy.circ_lib() !== orgId) {
+ copy.circ_lib(orgId);
+ copy.ischanged(true);
+
+ this.batchAttrs
+ .filter(ba => ba.name === 'circ_lib')
+ .forEach(attr => attr.hasChanged = true);
+ }
+ }
+
+ const vol = copy.call_number();
+
+ if (vol.owning_lib() === orgId) { return; } // No change needed
+
+ let newVol;
+ if (newVols[vol.id()]) {
+ newVol = newVols[vol.id()];
+
+ } else {
+
+ // The open-ils.cat.asset.volume.fleshed.batch.update API
+ // will use the existing volume when trying to create a
+ // new volume with the same parameters as an existing volume.
+ newVol = this.idl.clone(vol);
+ newVol.owning_lib(orgId);
+ newVol.id(this.volcopy.autoId--);
+ newVol.isnew(true);
+ newVols[vol.id()] = newVol;
+ }
+
+ copy.call_number(newVol);
+ copy.ischanged();
+
+ this.context.removeCopyNode(copy.id());
+ this.context.findOrCreateCopyNode(copy);
+ });
+
+ // If any of the above actions results in an empty volume
+ // remove it from the tree. Note this does not delete the
+ // volume at the server, since other items could be attached
+ // of which this instance of the editor is not aware.
+ Object.keys(newVols).forEach(volId => {
+
+ const volNode = this.context.volNodes().filter(
+ node => node.target.id() === +volId)[0];
+
+ if (volNode && volNode.children.length === 0) {
+ this.context.removeVolNode(+volId);
+ }
+ });
+ }
+
+ // Create or modify a stat cat entry for each copy that does not
+ // already match the new value.
+ statCatChanged(catId: number, clear?: boolean) {
+ catId = Number(catId);
+
+ const entryId = this.statCatValues[catId];
+
+ if (!entryId || !this.volcopy.statCatEntryMap[entryId]) {
+ console.warn(
+ `Attempt to apply stat cat value which does not exist.
+ This is likely the result of a stale copy template.
+ stat_cat=${catId} entry=${entryId}`);
+
+ return;
+ }
+
+ this.context.copyList().forEach(copy => {
+
+ let entry = copy.stat_cat_entries()
+ .filter(e => e.stat_cat() === catId)[0];
+
+ if (entry) {
+ if (entry.id() === entryId) {
+ // Requested mapping already exists.
+ return;
+ }
+ } else {
+
+ // Copy has no entry for this stat cat yet.
+ entry = this.idl.create('asce');
+ entry.stat_cat(catId);
+ copy.stat_cat_entries().push(entry);
+ }
+
+ entry.id(entryId);
+ entry.value(this.volcopy.statCatEntryMap[entryId].value());
+
+ copy.ischanged(true);
+ });
+ }
+
+ openCopyAlerts() {
+ this.copyAlertsDialog.inPlaceMode = true;
+ this.copyAlertsDialog.copyIds = this.context.copyList().map(c => c.id());
+
+ this.copyAlertsDialog.open({size: 'lg'}).subscribe(
+ newAlert => {
+ if (newAlert) {
+ this.context.copyList().forEach(copy => {
+ const a = this.idl.clone(newAlert);
+ a.isnew(true);
+ a.copy(copy.id());
+ if (!copy.copy_alerts()) { copy.copy_alerts([]); }
+ copy.copy_alerts().push(a);
+ copy.ischanged(true);
+ });
+ }
+ }
+ );
+ }
+
+ openCopyTags() {
+ this.copyTagsDialog.inPlaceMode = true;
+ this.copyTagsDialog.copyIds = this.context.copyList().map(c => c.id());
+
+ this.copyTagsDialog.open({size: 'lg'}).subscribe(newTags => {
+ if (!newTags || newTags.length === 0) { return; }
+
+ newTags.forEach(tag => {
+ this.context.copyList().forEach(copy => {
+
+ if (copy.tags().filter(
+ m => m.tag().id() === tag.id()).length > 0) {
+ return; // map already exists
+ }
+
+ const map = this.idl.create('acptcm');
+ map.isnew(true);
+ map.copy(copy.id());
+ map.tag(tag);
+
+ copy.tags().push(map);
+ copy.ischanged(true);
+ });
+ });
+ });
+ }
+
+ applyTemplate() {
+ const entry = this.copyTemplateCbox.selected;
+ if (!entry) { return; }
+
+ this.store.setLocalItem('cat.copy.last_template', entry.id);
+
+ const template = this.volcopy.templates[entry.id];
+
+ Object.keys(template).forEach(field => {
+ const value = template[field];
+
+ if (value === null || value === undefined) { return; }
+
+ if (field === 'statcats') {
+ Object.keys(value).forEach(catId => {
+ this.statCatValues[+catId] = value[+catId];
+ this.statCatChanged(+catId);
+ });
+ return;
+ }
+
+ // In some cases, we may have to fetch the data since
+ // the local code assumes copy field is fleshed.
+ let promise = Promise.resolve(value);
+
+ if (field === 'location') {
+ // May be a 'remote' location. Fetch as needed.
+ promise = this.volcopy.getLocation(value);
+ }
+
+ promise.then(val => {
+ this.applyCopyValue(field, val);
+
+ // Indicate in the form these values have changed
+ this.batchAttrs
+ .filter(ba => ba.name === field)
+ .forEach(attr => attr.hasChanged = true);
+ });
+ });
+ }
+
+ saveTemplate() {
+ const entry: ComboboxEntry = this.copyTemplateCbox.selected;
+ if (!entry) { return; }
+
+ let name;
+ let template;
+
+ if (entry.freetext) {
+ name = entry.label;
+ // freetext entries don't have an ID, but we may need one later.
+ entry.id = entry.label;
+ template = {};
+ } else {
+ name = entry.id;
+ template = this.volcopy.templates[name];
+ }
+
+ this.batchAttrs.forEach(comp => {
+ if (!comp.hasChanged) { return; }
+
+ const field = comp.name;
+ const value = this.values[field];
+
+ if (value === null) {
+ delete template[field];
+ return;
+ }
+
+ if (field.match(/stat_cat_/)) {
+ const statId = field.match(/stat_cat_(\d+)/)[1];
+ if (!template.statcats) { template.statcats = {}; }
+
+ template.statcats[statId] = value;
+
+ } else {
+
+ // Some values are fleshed. this assumes fleshed objects
+ // have an 'id' value, which is true so far.
+ template[field] =
+ typeof value === 'object' ? value.id() : value;
+ }
+ });
+
+ this.volcopy.templates[name] = template;
+ this.volcopy.saveTemplates();
+ }
+
+ exportTemplate($event) {
+ if (this.fileExport.inProgress()) { return; }
+
+ this.fileExport.exportFile(
+ $event, JSON.stringify(this.volcopy.templates), 'text/json');
+ }
+
+ importTemplate($event) {
+ const file: File = $event.target.files[0];
+ if (!file) { return; }
+
+ const reader = new FileReader();
+
+ reader.addEventListener('load', () => {
+
+ try {
+ const template = JSON.parse(reader.result as string);
+ const name = Object.keys(template)[0];
+ this.volcopy.templates[name] = template[name];
+ } catch (E) {
+ console.error('Invalid Item Attribute template', E);
+ return;
+ }
+
+ this.volcopy.saveTemplates();
+ // Adds the new one to the list and re-sorts the labels.
+ this.volcopy.fetchTemplates();
+ });
+
+ reader.readAsText(file);
+ }
+
+ // Returns null when no export is in progress.
+ exportTemplateUrl(): SafeUrl {
+ return this.fileExport.safeUrl;
+ }
+
+ deleteTemplate() {
+ const entry: ComboboxEntry = this.copyTemplateCbox.selected;
+ if (!entry) { return; }
+ delete this.volcopy.templates[entry.id];
+ this.volcopy.saveTemplates();
+ this.copyTemplateCbox.selected = null;
+ }
+
+ displayAttr(field: string): boolean {
+ return this.volcopy.defaults.hidden[field] !== true;
+ }
+
+ copyFieldLabel(field: string): string {
+ const def = this.idl.classes.acp.field_map[field];
+ return def ? def.label : '';
+ }
+
+ // Returns false if any items are in magic statuses
+ statusEditable(): boolean {
+ const copies = this.context.copyList();
+ for (let idx = 0; idx < copies.length; idx++) {
+ if (this.volcopy.copyStatIsMagic(copies[idx].status())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // Called any time a change occurs that could affect the
+ // save-ability of the form.
+ emitSaveChange() {
+ setTimeout(() => {
+ const canSave = this.batchAttrs.filter(
+ attr => attr.warnOnRequired()).length === 0;
+
+ this.canSaveChange.emit(canSave);
+ });
+ }
+}
+
+
+
--- /dev/null
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {VolCopyComponent} from './volcopy.component';
+
+const routes: Routes = [{
+ path: ':tab/:target/:target_id',
+ component: VolCopyComponent
+ /*
+ }, {
+ path: 'templates'
+ component: VolCopyComponent
+ }, {
+ path: 'configure'
+ component: VolCopyComponent
+ */
+}];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+ providers: []
+})
+
+export class VolCopyRoutingModule {}
+
--- /dev/null
+
+input[type="number"] {
+ /* visually accomodates numbers in the hundreds */
+ width: 4.5em;
+}
+
+.vol-row {
+ background-color: rgba(0,0,0,.03);
+ border-top: 1px solid #d9edf7;
+ border-bottom: 1px solid #d9edf7;
+}
+
+.clear-button {
+ border: none;
+ background-color: rgba(0, 0, 0, 0.0);
+ padding-left: .25rem;
+ padding-right: .25rem;
+ line-height: inherit;
+}
+
+.clear-button .material-icons {
+ font-size: 15px;
+ color: grey;
+}
+
--- /dev/null
+<eg-confirm-dialog
+ #confirmDelVol
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="Delete Call Number?"
+ dialogBody="Delete {{deleteVolCount}} Call Number(s) and All Associated Item(s)?">
+</eg-confirm-dialog>
+
+<eg-confirm-dialog
+ #confirmDelCopy
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="Delete Item?"
+ dialogBody="Delete {{deleteCopyCount}} Item(s)?">
+</eg-confirm-dialog>
+
+<div class="row d-flex bg-faint mb-2 pb-1 pt-1 border border-dark rounded">
+ <div class="p-1" [ngStyle]="{flex: flexAt(1)}"> </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(2)}"> </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(3)}" *ngIf="displayColumn('classification')">
+ <div><label class="font-weight-bold" i18n>Classification</label></div>
+ <div>
+ <eg-combobox [smallFormControl]="true" [(ngModel)]="batchVolClass">
+ <eg-combobox-entry *ngFor="let cls of volcopy.commonData.acn_class"
+ [entryId]="cls.id()" [entryLabel]="cls.name()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </div>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(4)}" *ngIf="displayColumn('prefix')">
+ <div><label class="font-weight-bold" i18n>Prefix</label></div>
+ <div>
+ <eg-combobox [smallFormControl]="true" [(ngModel)]="batchVolPrefix">
+ <eg-combobox-entry *ngFor="let pfx of volcopy.commonData.acn_prefix"
+ [entryId]="pfx.id()" [entryLabel]="pfx.label()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </div>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(5)}">
+ <div>
+ <label class="font-weight-bold label-with-material-icon" i18n>
+ Call Number Label
+ </label>
+ </div>
+ <div>
+ <eg-combobox [smallFormControl]="true"
+ [allowFreeText]="true" [(ngModel)]="batchVolLabel">
+ <eg-combobox-entry *ngFor="let label of recordVolLabels"
+ [entryId]="label" [entryLabel]="label">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </div>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(6)}" *ngIf="displayColumn('suffix')">
+ <div><label class="font-weight-bold" i18n>Suffix</label></div>
+ <div>
+ <eg-combobox [smallFormControl]="true" [(ngModel)]="batchVolSuffix">
+ <eg-combobox-entry *ngFor="let sfx of volcopy.commonData.acn_suffix"
+ [entryId]="sfx.id()" [entryLabel]="sfx.label()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </div>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(7)}">
+ <div><label class="font-weight-bold" i18n>Batch</label></div>
+ <div>
+ <button class="btn btn-sm btn-outline-dark label-with-material-icon"
+ (click)="batchVolApply()">
+ <span i18n>Apply</span>
+ <span class="material-icons">arrow_downward</span>
+ </button>
+ </div>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(8)}">
+ <ng-container *ngIf="displayColumn('generate_barcodes')">
+ <div><label class="font-weight-bold" i18n>Generate Barcodes</label></div>
+ <button class="btn btn-sm btn-outline-dark label-with-material-icon"
+ (click)="generateBarcodes()">
+ <span i18n>Generate</span>
+ <span class="material-icons">arrow_downward</span>
+ </button>
+ </ng-container>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexSpan(9, 10)}">
+ <ng-container *ngIf="displayColumn('generate_barcodes')">
+ <div><label class="font-weight-bold" i18n>Checkdigit</label></div>
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ (change)="saveUseCheckdigit()"
+ id="use-checkdigit" [(ngModel)]="useCheckdigit"/>
+ <label class="form-check-label" for="use-checkdigit" i18n>
+ Use Checkdigit
+ </label>
+ </div>
+ </ng-container>
+ </div>
+</div>
+
+
+
+<div class="row d-flex mt-2 mb-2">
+ <div class="p-1" [ngStyle]="{flex: flexAt(1)}">
+ <span class="font-weight-bold" i18n>Owning Library
+ <ng-container *ngIf="expand !== 1">
+ <button title="Expand Column" i18n-title
+ class="material-icon-button" (click)="expand = 1" i18n>
+ ↗
+ </button>
+ </ng-container>
+ <ng-container *ngIf="expand === 1">
+ <button title="Shrink Column" i18n-title
+ class="material-icon-button" (click)="expand = null" i18n>
+ ↙
+ </button>
+ </ng-container>
+ </span>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(2)}">
+ <label class="font-weight-bold" i18n>Call Numbers</label>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(3)}" *ngIf="displayColumn('classification')">
+ <span class="font-weight-bold" i18n>Classification
+ <ng-container *ngIf="expand !== 3">
+ <button title="Expand Column" i18n-title
+ class="material-icon-button" (click)="expand = 3" i18n>
+ ↗
+ </button>
+ </ng-container>
+ <ng-container *ngIf="expand === 3">
+ <button title="Shrink Column" i18n-title
+ class="material-icon-button" (click)="expand = null" i18n>
+ ↙
+ </button>
+ </ng-container>
+ </span>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(4)}" *ngIf="displayColumn('prefix')">
+ <span class="font-weight-bold" i18n>Prefix
+ <ng-container *ngIf="expand !== 4">
+ <button title="Expand Column" i18n-title
+ class="material-icon-button" (click)="expand = 4" i18n>
+ ↗
+ </button>
+ </ng-container>
+ <ng-container *ngIf="expand === 4">
+ <button title="Shrink Column" i18n-title
+ class="material-icon-button" (click)="expand = null" i18n>
+ ↙
+ </button>
+ </ng-container>
+ </span>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(5)}">
+ <span class="font-weight-bold" i18n>Call Number Label
+ <ng-container *ngIf="expand !== 5">
+ <button title="Expand Column" i18n-title
+ class="material-icon-button" (click)="expand = 5" i18n>
+ ↗
+ </button>
+ </ng-container>
+ <ng-container *ngIf="expand === 5">
+ <button title="Shrink Column" i18n-title
+ class="material-icon-button" (click)="expand = null" i18n>
+ ↙
+ </button>
+ </ng-container>
+ </span>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(6)}" *ngIf="displayColumn('suffix')">
+ <span class="font-weight-bold" i18n>Suffix
+ <ng-container *ngIf="expand !== 6">
+ <button title="Expand Column" i18n-title
+ class="material-icon-button" (click)="expand = 6" i18n>
+ ↗
+ </button>
+ </ng-container>
+ <ng-container *ngIf="expand === 6">
+ <button title="Shrink Column" i18n-title
+ class="material-icon-button" (click)="expand = null" i18n>
+ ↙
+ </button>
+ </ng-container>
+ </span>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(7)}">
+ <label class="font-weight-bold" i18n>Items</label>
+ </div>
+ <!--
+ When hiding the copy_number column, absorb its colum width to
+ take advantage of the space and to ensure the main columns still
+ line up with the batch updater row sitting above
+ -->
+ <div class="p-1"
+ [ngStyle]="{flex: displayColumn('copy_number_vc') ? flexAt(8) : flexSpan(8, 9)}">
+ <span class="font-weight-bold" i18n>Barcode
+ <ng-container *ngIf="expand !== 8">
+ <button title="Expand Column" i18n-title
+ class="material-icon-button" (click)="expand = 8" i18n>
+ ↗
+ </button>
+ </ng-container>
+ <ng-container *ngIf="expand === 8">
+ <button title="Shrink Column" i18n-title
+ class="material-icon-button" (click)="expand = null" i18n>
+ ↙
+ </button>
+ </ng-container>
+ </span>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(9)}" *ngIf="displayColumn('copy_number_vc')">
+ <label class="font-weight-bold" i18n>Item #</label>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(10)}">
+ <span class="font-weight-bold" i18n>Part
+ <ng-container *ngIf="expand !== 10">
+ <button title="Expand Column" i18n-title
+ class="material-icon-button" (click)="expand = 10" i18n>
+ ↗
+ </button>
+ </ng-container>
+ <ng-container *ngIf="expand === 10">
+ <button title="Shrink Column" i18n-title
+ class="material-icon-button" (click)="expand = null" i18n>
+ ↙
+ </button>
+ </ng-container>
+ </span>
+ </div>
+</div>
+
+<ng-container *ngFor="let orgNode of context.orgNodes(); let orgIdx = index">
+ <ng-container *ngFor="let volNode of orgNode.children; let volIdx = index">
+ <ng-container *ngFor="let copyNode of volNode.children; let copyIdx = index">
+ <div class="row d-flex mt-1" [ngClass]="{'vol-row': copyIdx == 0}">
+ <div class="p-1" [ngStyle]="{flex: flexAt(1)}">
+ <ng-container *ngIf="copyIdx == 0">
+ <span>{{orgNode.target.shortname()}}</span>
+ {{sessionType}}
+ <ng-container *ngIf="context.sessionType == 'record' || context.sessionType == 'mixed'">
+ <button class="clear-button" (click)="deleteVol(volNode)"
+ title="Delete Call Number {{volNode.target.label()}}" i18n-title>
+ <span class="material-icons">clear</span>
+ </button>
+ </ng-container>
+ </ng-container>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(2)}">
+ <ng-container *ngIf="copyIdx == 0 && volIdx == 0">
+ <input type="number" class="form-control form-control-sm"
+ [disabled]="context.sessionType == 'copy' || context.sessionType == 'vol'"
+ [required]="true" [min]="existingVolCount(orgNode)"
+ [ngModel]="orgNode.children.length"
+ (ngModelChange)="volCountChanged(orgNode, $event)"/>
+ </ng-container>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(3)}" *ngIf="displayColumn('classification')">
+ <ng-container *ngIf="copyIdx == 0">
+ <eg-combobox
+ [selectedId]="volNode.target.label_class()"
+ [smallFormControl]="true"
+ [required]="true"
+ (onChange)="applyVolValue(volNode.target, 'label_class', $event ? $event.id : null)">
+ <eg-combobox-entry *ngFor="let cls of volcopy.commonData.acn_class"
+ [entryId]="cls.id()" [entryLabel]="cls.name()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </ng-container>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(4)}" *ngIf="displayColumn('prefix')">
+ <ng-container *ngIf="copyIdx == 0">
+ <eg-combobox
+ [selectedId]="volNode.target.prefix()"
+ [required]="true"
+ [smallFormControl]="true"
+ (onChange)="applyVolValue(volNode.target, 'prefix', $event ? $event.id : null)">
+ <eg-combobox-entry
+ [entryId]="-1" entryLabel="<None>" i18n-entryLabel>
+ </eg-combobox-entry>
+ <eg-combobox-entry *ngFor="let pfx of volcopy.commonData.acn_prefix"
+ [entryId]="pfx.id()" [entryLabel]="pfx.label()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </ng-container>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(5)}">
+ <ng-container *ngIf="copyIdx == 0">
+ <input class="form-control form-control-sm" type="text"
+ spellcheck="false"
+ [required]="true"
+ [ngClass]="{invalid: !volNode.target.label()}"
+ [ngModel]="volNode.target.label()"
+ (change)="applyVolValue(volNode.target, 'label', $event.target.value)">
+ </ng-container>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(6)}" *ngIf="displayColumn('suffix')">
+ <ng-container *ngIf="copyIdx == 0">
+ <eg-combobox
+ [selectedId]="volNode.target.suffix()"
+ [required]="true"
+ [smallFormControl]="true"
+ (onChange)="applyVolValue(volNode.target, 'suffix', $event ? $event.id : null)">
+ <eg-combobox-entry
+ [entryId]="-1" entryLabel="<None>" i18n-entryLabel>
+ </eg-combobox-entry>
+ <eg-combobox-entry *ngFor="let sfx of volcopy.commonData.acn_suffix"
+ [entryId]="sfx.id()" [entryLabel]="sfx.label()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </ng-container>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(7)}">
+ <ng-container *ngIf="copyIdx == 0">
+ <input type="number" class="form-control form-control-sm"
+ [disabled]="context.sessionType == 'copy'"
+ [required]="true" [min]="existingCopyCount(volNode)"
+ [ngModel]="volNode.children.length"
+ (ngModelChange)="copyCountChanged(volNode, $event)"/>
+ </ng-container>
+ </div>
+ <div class="p-1"
+ [ngStyle]="{flex: displayColumn('copy_number_vc') ? flexAt(8) : flexSpan(8, 9)}">
+ <div class="d-flex">
+ <ng-container *ngIf="context.sessionType != 'copy'">
+ <button class="clear-button" (click)="deleteCopy(copyNode)"
+ [disabled]="volcopy.restrictCopyDelete(copyNode.target.status())"
+ title="Delete Item {{copyNode.target.barcode()}}" i18n-title>
+ <span class="material-icons">clear</span>
+ </button>
+ </ng-container>
+
+ <!--
+ Barcode value is not required for new copies, since those
+ without a barcode will be ignored.
+ -->
+ <input type="text" class="form-control form-control-sm"
+ title="{{copyStatLabel(copyNode.target)}}"
+ id="barcode-input-{{copyNode.target.id()}}"
+ spellcheck="false" [required]="true"
+ placeholder="New Barcode..." i18n-placeholder
+ [disabled]="volcopy.copyStatIsMagic(copyNode.target.status())"
+ [ngClass]="{
+ 'text-danger': copyNode.target._dupe_barcode,
+ 'invalid': !copyNode.target.barcode() && !copyNode.target.isnew()
+ }"
+ (change)="barcodeChanged(copyNode.target, $event.target.value)"
+ (ngModelChange)="copyNode.target.barcode($event)"
+ (keyup.enter)="selectNextBarcode(copyNode.target.id())"
+ (keyup.shift.enter)="selectNextBarcode(copyNode.target.id(), true)"
+ (focus)="$event.target.select()"
+ [ngModel]="copyNode.target.barcode()"
+ (ngModelChange)="applyCopyValue(copyNode.target, 'barcode', $event)"/>
+ </div>
+ <div *ngIf="copyNode.target._dupe_barcode"
+ class="alert alert-danger font-italic p-1" i18n>
+ Duplicate Barcode
+ </div>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(9)}" *ngIf="displayColumn('copy_number_vc')">
+ <input type="number" min="1" class="form-control form-control-sm"
+ [ngModel]="copyNode.target.copy_number()"
+ (ngModelChange)="applyCopyValue(copyNode.target, 'copy_number', $event)"/>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(10)}">
+ <ng-container *ngIf="!recordHasParts(volNode.target.record())">
+ <label i18n>N/A</label>
+ </ng-container>
+ <ng-container *ngIf="recordHasParts(volNode.target.record())">
+ <eg-combobox
+ [selectedId]="copyNode.target.parts()[0] ? copyNode.target.parts()[0].id() : null"
+ [smallFormControl]="true"
+ (onChange)="copyPartChanged(copyNode, $event)">
+ <eg-combobox-entry
+ *ngFor="let part of volcopy.bibParts[volNode.target.record()]"
+ [entryId]="part.id()" [entryLabel]="part.label()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </ng-container>
+ </div>
+ </div>
+ </ng-container>
+ </ng-container>
+</ng-container>
+
+<hr/>
+
+<div class="row d-flex">
+
+ <div class="p-1" [ngStyle]="{flex: flexSpan(1, 2)}">
+ <eg-org-select #newVolOrg [applyDefault]="true"
+ [limitPerms]="['CREATE_VOLUME']" [hideOrgs]="volcopy.hideVolOrgs">
+ </eg-org-select>
+ </div>
+
+ <div class="p-1" [ngStyle]="{flex: flexSpan(3, 4)}">
+ <button class="btn btn-outline-dark ml-2"
+ (click)="addVol(newVolOrg.selectedOrg())" i18n>
+ Add Call Number
+ </button>
+ </div>
+
+ <div class="p-1" [ngStyle]="{flex: flexAt(5)}"></div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(6)}"></div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(7)}"></div>
+
+ <div class="p-1 pl-3" [ngStyle]="{flex: flexAt(8)}">
+ <ng-container *ngIf="displayColumn('generate_barcodes')">
+ <button class="btn btn-sm btn-outline-dark mr-2"
+ (click)="generateBarcodes()" i18n>Generate Barcodes</button>
+ </ng-container>
+ </div>
+
+ <div class="p-1" [ngStyle]="{flex: flexSpan(9, 10)}">
+ <ng-container *ngIf="displayColumn('generate_barcodes')">
+ <div class="form-check form-check-inline mr-2">
+ <input class="form-check-input" type="checkbox"
+ (change)="saveUseCheckdigit()"
+ id="use-checkdigit-2" [(ngModel)]="useCheckdigit"/>
+ <label class="form-check-label" for="use-checkdigit-2" i18n>
+ Use Checkdigit
+ </label>
+ </div>
+ </ng-container>
+ </div>
+</div>
+
--- /dev/null
+import {Component, OnInit, AfterViewInit, ViewChild, Input, Renderer2, Output, EventEmitter} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {tap} from 'rxjs/operators';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {VolCopyContext, HoldingsTreeNode} from './volcopy';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {VolCopyService} from './volcopy.service';
+
+@Component({
+ selector: 'eg-vol-edit',
+ templateUrl: 'vol-edit.component.html',
+ styleUrls: ['vol-edit.component.css']
+})
+
+
+export class VolEditComponent implements OnInit {
+
+ @Input() context: VolCopyContext;
+
+ // There are 10 columns in the editor form. Set the flex values
+ // here so they don't have to be hard-coded and repeated in the
+ // markup. Changing a flex value here will propagate to all
+ // rows in the form. Column numbers are 1-based.
+ flexSettings: {[column: number]: number} = {
+ 1: 1, 2: 1, 3: 2, 4: 1, 5: 2, 6: 1, 7: 1, 8: 2, 9: 1, 10: 1};
+
+ // If a column is specified as the expand field, its flex value
+ // will magically grow.
+ expand: number;
+
+ batchVolClass: ComboboxEntry;
+ batchVolPrefix: ComboboxEntry;
+ batchVolSuffix: ComboboxEntry;
+ batchVolLabel: ComboboxEntry;
+
+ autoBarcodeInProgress = false;
+ useCheckdigit = false;
+
+ deleteVolCount: number = null;
+ deleteCopyCount: number = null;
+
+ recordVolLabels: string[] = [];
+
+ @ViewChild('confirmDelVol', {static: false})
+ confirmDelVol: ConfirmDialogComponent;
+
+ @ViewChild('confirmDelCopy', {static: false})
+ confirmDelCopy: ConfirmDialogComponent;
+
+ // Emitted when the save-ability of this form changes.
+ @Output() canSaveChange: EventEmitter<boolean> = new EventEmitter<boolean>();
+
+ constructor(
+ private renderer: Renderer2,
+ private idl: IdlService,
+ private org: OrgService,
+ private pcrud: PcrudService,
+ private net: NetService,
+ private auth: AuthService,
+ private holdings: HoldingsService,
+ public volcopy: VolCopyService
+ ) {}
+
+ ngOnInit() {
+
+ this.deleteVolCount = null;
+ this.deleteCopyCount = null;
+ this.useCheckdigit = this.volcopy.defaults.values.use_checkdigit;
+
+ this.volcopy.fetchRecordVolLabels(this.context.recordId)
+ .then(labels => this.recordVolLabels = labels)
+ .then(_ => this.volcopy.fetchBibParts(this.context.getRecordIds()))
+ .then(_ => this.addStubCopies());
+ }
+
+ copyStatLabel(copy: IdlObject): string {
+ if (copy) {
+ const statId = copy.status();
+ if (statId in this.volcopy.copyStatuses) {
+ return this.volcopy.copyStatuses[statId].name();
+ }
+ }
+ return '';
+ }
+
+ recordHasParts(bibId: number): boolean {
+ return this.volcopy.bibParts[bibId] &&
+ this.volcopy.bibParts[bibId].length > 0;
+ }
+
+ // Column width (flex:x) for column by column number.
+ flexAt(column: number): number {
+ return this.flexSpan(column, column);
+ }
+
+ // Returns the flex amount occupied by a span of columns.
+ flexSpan(column1: number, column2: number): number {
+ let flex = 0;
+ for (let i = column1; i <= column2; i++) {
+ let value = this.flexSettings[i];
+ if (this.expand === i) { value = value * 3; }
+ flex += value;
+ }
+ return flex;
+ }
+
+ volCountChanged(orgNode: HoldingsTreeNode, count: number) {
+ if (count === null) { return; }
+ const diff = count - orgNode.children.length;
+ if (diff > 0) {
+ this.createVols(orgNode, diff);
+ } else if (diff < 0) {
+ this.deleteVols(orgNode, -diff);
+ }
+ }
+
+
+ addVol(org: IdlObject) {
+ if (!org) { return; }
+ const orgNode = this.context.findOrCreateOrgNode(org.id());
+ this.createVols(orgNode, 1);
+ }
+
+ existingVolCount(orgNode: HoldingsTreeNode): number {
+ return orgNode.children.filter(volNode => !volNode.target.isnew()).length;
+ }
+
+ existingCopyCount(volNode: HoldingsTreeNode): number {
+ return volNode.children.filter(copyNode => !copyNode.target.isnew()).length;
+ }
+
+ copyCountChanged(volNode: HoldingsTreeNode, count: number) {
+ if (count === null) { return; }
+ const diff = count - volNode.children.length;
+ if (diff > 0) {
+ this.createCopies(volNode, diff);
+ } else if (diff < 0) {
+ this.deleteCopies(volNode, -diff);
+ }
+ }
+
+ // This only removes copies that were created during the
+ // current editing session and have not yet been saved in the DB.
+ deleteCopies(volNode: HoldingsTreeNode, count: number) {
+ for (let i = 0; i < count; i++) {
+ const copyNode = volNode.children[volNode.children.length - 1];
+ if (copyNode && copyNode.target.isnew()) {
+ volNode.children.pop();
+ } else {
+ break;
+ }
+ }
+ }
+
+ createCopies(volNode: HoldingsTreeNode, count: number) {
+ for (let i = 0; i < count; i++) {
+
+ // Our context assumes copies are fleshed with volumes
+ const vol = volNode.target;
+ const copy = this.volcopy.createStubCopy(vol);
+ copy.call_number(vol);
+ this.context.findOrCreateCopyNode(copy);
+ }
+ }
+
+
+ createVols(orgNode: HoldingsTreeNode, count: number) {
+ const vols = [];
+ for (let i = 0; i < count; i++) {
+
+ // This will vivify the volNode if needed.
+ const vol = this.volcopy.createStubVol(
+ this.context.recordId, orgNode.target.id());
+
+ vols.push(vol);
+
+ // Our context assumes copies are fleshed with volumes
+ const copy = this.volcopy.createStubCopy(vol);
+ copy.call_number(vol);
+ this.context.findOrCreateCopyNode(copy);
+ }
+
+ this.volcopy.setVolClassLabels(vols);
+ }
+
+ // This only removes vols that were created during the
+ // current editing session and have not yet been saved in the DB.
+ deleteVols(orgNode: HoldingsTreeNode, count: number) {
+ for (let i = 0; i < count; i++) {
+ const volNode = orgNode.children[orgNode.children.length - 1];
+ if (volNode && volNode.target.isnew()) {
+ orgNode.children.pop();
+ } else {
+ break;
+ }
+ }
+ }
+
+ // When editing existing vols, be sure each has at least one copy.
+ addStubCopies(volNode?: HoldingsTreeNode) {
+ const nodes = volNode ? [volNode] : this.context.volNodes();
+
+ nodes.forEach(vNode => {
+ if (vNode.children.length === 0) {
+ const vol = vNode.target;
+ const copy = this.volcopy.createStubCopy(vol);
+ copy.call_number(vol);
+ this.context.findOrCreateCopyNode(copy);
+ }
+ });
+ }
+
+ applyVolValue(vol: IdlObject, key: string, value: any) {
+
+ if (value === null && (key === 'prefix' || key === 'suffix')) {
+ // -1 is the empty prefix/suffix value.
+ value = -1;
+ }
+
+ if (vol[key]() !== value) {
+ vol[key](value);
+ vol.ischanged(true);
+ }
+
+ this.emitSaveChange();
+ }
+
+ applyCopyValue(copy: IdlObject, key: string, value: any) {
+ if (copy[key]() !== value) {
+ copy[key](value);
+ copy.ischanged(true);
+ }
+ }
+
+ copyPartChanged(copyNode: HoldingsTreeNode, entry: ComboboxEntry) {
+ const copy = copyNode.target;
+ const part = copyNode.target.parts()[0];
+
+ if (entry) {
+
+ const newPart =
+ this.volcopy.bibParts[copy.call_number().record()]
+ .filter(p => p.id() === entry.id)[0];
+
+ // Nothing to change?
+ if (part && part.id() === newPart.id()) { return; }
+
+ copy.parts([newPart]);
+ copy.ischanged(true);
+
+ } else if (part) { // Part map no longer needed.
+
+ copy.parts([]);
+ copy.ischanged(true);
+ }
+ }
+
+ batchVolApply() {
+ this.context.volNodes().forEach(volNode => {
+ const vol = volNode.target;
+ if (this.batchVolClass) {
+ this.applyVolValue(vol, 'label_class', this.batchVolClass.id);
+ }
+ if (this.batchVolPrefix) {
+ this.applyVolValue(vol, 'prefix', this.batchVolPrefix.id);
+ }
+ if (this.batchVolSuffix) {
+ this.applyVolValue(vol, 'suffix', this.batchVolSuffix.id);
+ }
+ if (this.batchVolLabel) {
+ // Use label; could be freetext.
+ this.applyVolValue(vol, 'label', this.batchVolLabel.label);
+ }
+ });
+ }
+
+ // Focus and select the next editable barcode.
+ selectNextBarcode(id: number, previous?: boolean) {
+ let found = false;
+ let nextId: number = null;
+ let firstId: number = null;
+
+ let copies = this.context.copyList();
+ if (previous) { copies = copies.reverse(); }
+
+ // Find the ID of the next item. If this is the last item,
+ // loop back to the first item.
+ copies.forEach(copy => {
+ if (nextId !== null) { return; }
+
+ // In case we have to loop back to the first copy.
+ if (firstId === null && this.barcodeCanChange(copy)) {
+ firstId = copy.id();
+ }
+
+ if (found) {
+ if (nextId === null && this.barcodeCanChange(copy)) {
+ nextId = copy.id();
+ }
+ } else if (copy.id() === id) {
+ found = true;
+ }
+ });
+
+ this.renderer.selectRootElement(
+ '#barcode-input-' + (nextId || firstId)).select();
+ }
+
+ barcodeCanChange(copy: IdlObject): boolean {
+ return !this.volcopy.copyStatIsMagic(copy.status());
+ }
+
+ generateBarcodes() {
+ this.autoBarcodeInProgress = true;
+
+ // Autogen only replaces barcodes for items which are in
+ // certain statuses.
+ const copies = this.context.copyList()
+ .filter((copy, idx) => {
+ // During autogen we do not replace the first item,
+ // so it's status is not relevant.
+ return idx === 0 || this.barcodeCanChange(copy);
+ });
+
+ if (copies.length > 1) { // seed barcode will always be present
+ this.proceedWithAutogen(copies)
+ .then(_ => this.autoBarcodeInProgress = false);
+ }
+ }
+
+ proceedWithAutogen(copyList: IdlObject[]): Promise<any> {
+
+ const seedBarcode: string = copyList[0].barcode();
+ copyList.shift(); // Avoid replacing the seed barcode
+
+ const count = copyList.length;
+
+ return this.net.request('open-ils.cat',
+ 'open-ils.cat.item.barcode.autogen',
+ this.auth.token(), seedBarcode, count, {
+ checkdigit: this.useCheckdigit,
+ skip_dupes: true
+ }
+ ).pipe(tap(barcodes => {
+
+ copyList.forEach(copy => {
+ if (copy.barcode() !== barcodes[0]) {
+ copy.barcode(barcodes[0]);
+ copy.ischanged(true);
+ }
+ barcodes.shift();
+ });
+
+ })).toPromise();
+ }
+
+ barcodeChanged(copy: IdlObject, barcode: string) {
+ // note: copy.barcode(barcode) applied via ngModel
+ copy.ischanged(true);
+ copy._dupe_barcode = false;
+
+ if (!barcode) {
+ this.emitSaveChange();
+ return;
+ }
+
+ if (!this.autoBarcodeInProgress) {
+ // Manual barcode entry requires dupe check
+
+ copy._dupe_barcode = false;
+ this.pcrud.search('acp', {
+ deleted: 'f',
+ barcode: barcode,
+ id: {'!=': copy.id()}
+ }).subscribe(
+ resp => {
+ if (resp) { copy._dupe_barcode = true; }
+ },
+ err => {},
+ () => this.emitSaveChange()
+ );
+ }
+ }
+
+ deleteCopy(copyNode: HoldingsTreeNode) {
+
+ if (copyNode.target.isnew()) {
+ // Confirmation not required when deleting brand new copies.
+ this.deleteOneCopy(copyNode);
+ return;
+ }
+
+ this.deleteCopyCount = 1;
+ this.confirmDelCopy.open().toPromise().then(confirmed => {
+ if (confirmed) { this.deleteOneCopy(copyNode); }
+ });
+ }
+
+ deleteOneCopy(copyNode: HoldingsTreeNode) {
+ const targetCopy = copyNode.target;
+
+ const orgNodes = this.context.orgNodes();
+ for (let orgIdx = 0; orgIdx < orgNodes.length; orgIdx++) {
+ const orgNode = orgNodes[orgIdx];
+
+ for (let volIdx = 0; volIdx < orgNode.children.length; volIdx++) {
+ const volNode = orgNode.children[volIdx];
+
+ for (let copyIdx = 0; copyIdx < volNode.children.length; copyIdx++) {
+ const copy = volNode.children[copyIdx].target;
+
+ if (copy.id() === targetCopy.id()) {
+ volNode.children.splice(copyIdx, 1);
+ if (!copy.isnew()) {
+ copy.isdeleted(true);
+ this.context.copiesToDelete.push(copy);
+ }
+
+ if (volNode.children.length === 0) {
+ // When removing the last copy, add a stub copy.
+ this.addStubCopies();
+ }
+
+ return;
+ }
+ }
+ }
+ }
+ }
+
+
+ deleteVol(volNode: HoldingsTreeNode) {
+
+ if (volNode.target.isnew()) {
+ // Confirmation not required when deleting brand new vols.
+ this.deleteOneVol(volNode);
+ return;
+ }
+
+ this.deleteVolCount = 1;
+ this.deleteCopyCount = volNode.children.length;
+
+ this.confirmDelVol.open().toPromise().then(confirmed => {
+ if (confirmed) { this.deleteOneVol(volNode); }
+ });
+ }
+
+ deleteOneVol(volNode: HoldingsTreeNode) {
+
+ let deleteVolIdx = null;
+ const targetVol = volNode.target;
+
+ // FOR loops allow for early exit
+ const orgNodes = this.context.orgNodes();
+ for (let orgIdx = 0; orgIdx < orgNodes.length; orgIdx++) {
+ const orgNode = orgNodes[orgIdx];
+
+ for (let volIdx = 0; volIdx < orgNode.children.length; volIdx++) {
+ const vol = orgNode.children[volIdx].target;
+
+ if (vol.id() === targetVol.id()) {
+ deleteVolIdx = volIdx;
+
+ if (vol.isnew()) {
+ // New volumes, which can only have new copies
+ // may simply be removed from the holdings
+ // tree to delete them.
+ break;
+ }
+
+ // Mark volume and attached copies as deleted
+ // and track for later deletion.
+ targetVol.isdeleted(true);
+ this.context.volsToDelete.push(targetVol);
+
+ // When deleting vols, no need to delete the linked
+ // copies. They'll be force deleted via the API.
+ }
+
+ if (deleteVolIdx !== null) { break; }
+ }
+
+ if (deleteVolIdx !== null) {
+ orgNode.children.splice(deleteVolIdx, 1);
+ break;
+ }
+ }
+ }
+
+ displayColumn(field: string): boolean {
+ return this.volcopy.defaults.hidden[field] !== true;
+ }
+
+ saveUseCheckdigit() {
+ this.volcopy.defaults.values.use_checkdigit = this.useCheckdigit === true;
+ this.volcopy.saveDefaults();
+ }
+
+ canSave(): boolean {
+
+ const copies = this.context.copyList();
+
+ const badCopies = copies.filter(copy => {
+ return copy._dupe_barcode || (!copy.isnew() && !copy.barcode());
+ }).length > 0;
+
+ if (badCopies) { return false; }
+
+ const badVols = this.context.volNodes().filter(volNode => {
+ const vol = volNode.target;
+ return !(
+ vol.prefix() && vol.label() && vol.suffix && vol.label_class()
+ );
+ }).length > 0;
+
+ return !badVols;
+ }
+
+ // Called any time a change occurs that could affect the
+ // save-ability of the form.
+ emitSaveChange() {
+ setTimeout(() => {
+ this.canSaveChange.emit(this.canSave());
+ });
+ }
+}
+
--- /dev/null
+<eg-staff-banner bannerText="Holdings Editor" i18n-bannerText></eg-staff-banner>
+
+<div class="row" *ngIf="sessionExpired">
+ <div class="col-lg-6 mt-4 offset-lg-3 alert alert-danger d-flex justify-content-center" i18n>
+ Holdings Editor Session Expired
+ </div>
+</div>
+
+<ng-container *ngIf="!sessionExpired && !loading">
+
+ <eg-bib-summary *ngIf="context.recordId" [recordId]="context.recordId"></eg-bib-summary>
+
+ <div class="mt-3"> </div>
+
+ <ul ngbNav #holdingsNav="ngbNav" class="nav-tabs"
+ [activeId]="tab" (navChange)="beforeTabChange($event)">
+ <li ngbNavItem="holdings">
+ <a ngbNavLink i18n>Holdings</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-vol-edit [context]="context"
+ (canSaveChange)="volsCanSave = $event"></eg-vol-edit>
+ </div>
+ <ng-container *ngIf="volcopy.defaults.values.unified_display">
+ <div class="mt-2">
+ <eg-copy-attrs [context]="context"
+ (canSaveChange)="attrsCanSave = $event"></eg-copy-attrs>
+ </div>
+ </ng-container>
+ </ng-template>
+ </li>
+ <ng-container *ngIf="!volcopy.defaults.values.unified_display">
+ <li ngbNavItem="attrs">
+ <a ngbNavLink i18n>Item Attributes</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-copy-attrs [context]="context"
+ (canSaveChange)="attrsCanSave = $event"></eg-copy-attrs>
+ </div>
+ </ng-template>
+ </li>
+ </ng-container>
+ <li ngbNavItem="config">
+ <a ngbNavLink i18n>Preferences</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-volcopy-config [context]="context"></eg-volcopy-config>
+ </div>
+ </ng-template>
+ </li>
+ </ul>
+ <div [ngbNavOutlet]="holdingsNav"></div>
+
+ <ng-container *ngIf="tab === 'holdings' || tab === 'attrs'">
+ <hr class="m-2"/>
+ <div class="row m-2 p-2 border border-dark rounded bg-faint">
+ <div class="col-lg-12 d-flex">
+ <div class="form-check form-check-inline ml-2">
+ <input class="form-check-input" id='use-labels-cbox' type="checkbox"
+ [(ngModel)]="printLabels" (change)="savePrintLabels()">
+ <label class="form-check-label" for='use-labels-cbox'
+ i18n>Print Labels?</label>
+ </div>
+ <div class="flex-1"> </div>
+ <button class="btn btn-outline-dark" (click)="save()"
+ [ngClass]="{'border-danger': isNotSaveable()}"
+ [disabled]="isNotSaveable()" i18n>Save</button>
+ <button class="btn btn-outline-dark ml-2" (click)="save(true)"
+ [ngClass]="{'border-danger': isNotSaveable()}"
+ [disabled]="isNotSaveable()" i18n>Save & Exit</button>
+ </div>
+ </div>
+ </ng-container>
+
+</ng-container>
+
+<ng-container *ngIf="loading">
+ <div class="row">
+ <div class="col-lg-6 offset-lg-3">
+ <eg-progress-inline></eg-progress-inline>
+ </div>
+ </div>
+</ng-container>
+
--- /dev/null
+import {Component, OnInit, AfterViewInit, ViewChild, Renderer2} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {tap} from 'rxjs/operators';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {OrgService} from '@eg/core/org.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {HoldingsService, CallNumData} from '@eg/staff/share/holdings/holdings.service';
+import {VolCopyContext} from './volcopy';
+import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
+import {AnonCacheService} from '@eg/share/util/anon-cache.service';
+import {VolCopyService} from './volcopy.service';
+import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+
+const COPY_FLESH = {
+ flesh: 1,
+ flesh_fields: {
+ acp: [
+ 'call_number', 'location', 'parts', 'tags',
+ 'creator', 'editor', 'stat_cat_entries'
+ ],
+ acptcm: ['tag'],
+ acpt: ['tag_type']
+ }
+};
+
+interface EditSession {
+
+ // Unset if editing in multi-record mode
+ record_id: number;
+
+ // list of copy IDs
+ copies: number[];
+
+ // Adding to or creating new call numbers
+ raw: CallNumData[];
+
+ // Hide the volumes editor
+ hide_vols: boolean;
+
+ // Hide the copy attrs editor.
+ hide_copies: boolean;
+}
+
+@Component({
+ templateUrl: 'volcopy.component.html'
+})
+export class VolCopyComponent implements OnInit {
+
+ context: VolCopyContext;
+ loading = true;
+ sessionExpired = false;
+ printLabels = false;
+
+ tab = 'holdings'; // holdings | attrs | config
+ target: string; // item | callnumber | record | session
+ targetId: string; // id value or session string
+
+ volsCanSave = true;
+ attrsCanSave = true;
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private renderer: Renderer2,
+ private evt: EventService,
+ private idl: IdlService,
+ private org: OrgService,
+ private net: NetService,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private cache: AnonCacheService,
+ private holdings: HoldingsService,
+ private volcopy: VolCopyService
+ ) { }
+
+ ngOnInit() {
+ this.route.paramMap.subscribe(
+ (params: ParamMap) => this.negotiateRoute(params));
+ }
+
+ negotiateRoute(params: ParamMap) {
+ this.tab = params.get('tab') || 'holdings';
+ this.target = params.get('target');
+ this.targetId = params.get('target_id');
+
+ if (this.volcopy.currentContext) {
+ // Avoid clobbering the context on route change.
+ this.context = this.volcopy.currentContext;
+ } else {
+ this.context = new VolCopyContext();
+ this.context.org = this.org; // inject;
+ }
+
+ switch (this.target) {
+ case 'item':
+ this.context.copyId = +this.targetId;
+ break;
+ case 'callnumber':
+ this.context.volId = +this.targetId;
+ break;
+ case 'record':
+ this.context.recordId = +this.targetId;
+ break;
+ case 'session':
+ this.context.session = this.targetId;
+ break;
+ }
+
+ if (this.volcopy.currentContext) {
+ this.loading = false;
+
+ } else {
+ // Avoid refetching the data during route changes.
+ this.volcopy.currentContext = this.context;
+ this.load();
+ }
+ }
+
+ load(copyIds?: number[]): Promise<any> {
+ this.sessionExpired = false;
+ this.loading = true;
+ this.context.reset();
+
+ return this.volcopy.load()
+ .then(_ => this.fetchHoldings(copyIds))
+ .then(_ => this.volcopy.applyVolLabels(
+ this.context.volNodes().map(n => n.target)))
+ .then(_ => this.context.sortHoldings())
+ .then(_ => this.context.setRecordId())
+ .then(_ => this.printLabels =
+ this.volcopy.defaults.values.print_labels === true)
+ .then(_ => {
+ // unified display has no 'attrs' tab
+ if (this.volcopy.defaults.values.unified_display
+ && this.tab === 'attrs') {
+ this.tab = 'holdings';
+ this.routeToTab();
+ }
+ })
+ .then(_ => this.loading = false);
+ }
+
+ fetchHoldings(copyIds?: number[]): Promise<any> {
+
+ if (copyIds && copyIds.length > 0) {
+ // Reloading copies that were just edited.
+ return this.fetchCopies(copyIds);
+
+ } else if (this.context.session) {
+ this.context.sessionType = 'mixed';
+ return this.fetchSession(this.context.session);
+
+ } else if (this.context.copyId) {
+ this.context.sessionType = 'copy';
+ return this.fetchCopies(this.context.copyId);
+
+ } else if (this.context.volId) {
+ this.context.sessionType = 'vol';
+ return this.fetchVols(this.context.volId);
+
+ } else if (this.context.recordId) {
+ this.context.sessionType = 'record';
+ return this.fetchRecords(this.context.recordId);
+ }
+ }
+
+ // Changing a tab in the UI means changing the route.
+ // Changing the route ultimately results in changing the tab.
+ beforeTabChange(evt: NgbNavChangeEvent) {
+ evt.preventDefault();
+ this.tab = evt.nextId;
+ this.routeToTab();
+ }
+
+ routeToTab() {
+ const url =
+ `/staff/cat/volcopy/${this.tab}/${this.target}/${this.targetId}`;
+
+ // Retain search parameters
+ this.router.navigate([url], {queryParamsHandling: 'merge'});
+ }
+
+ fetchSession(session: string): Promise<any> {
+
+ return this.cache.getItem(session, 'edit-these-copies')
+ .then((editSession: EditSession) => {
+
+ if (!editSession) {
+ this.loading = false;
+ this.sessionExpired = true;
+ return Promise.reject('Session Expired');
+ }
+
+ console.debug('Edit Session', editSession);
+
+ this.context.recordId = editSession.record_id;
+
+ if (editSession.copies && editSession.copies.length > 0) {
+ return this.fetchCopies(editSession.copies);
+ }
+
+ const volsToFetch = [];
+ const volsToCreate = [];
+ editSession.raw.forEach((volData: CallNumData) => {
+ this.context.fastAdd = volData.fast_add === true;
+
+ if (volData.callnumber > 0) {
+ volsToFetch.push(volData);
+ } else {
+ volsToCreate.push(volData);
+ }
+ });
+
+ let promise = Promise.resolve();
+ if (volsToFetch.length > 0) {
+ promise = promise.then(_ =>
+ this.fetchVolsStubCopies(volsToFetch));
+ }
+
+ if (volsToCreate.length > 0) {
+ promise = promise.then(_ =>
+ this.createVolsStubCopies(volsToCreate));
+ }
+
+ return promise;
+ });
+ }
+
+ // Creating new vols. Each gets a stub copy.
+ createVolsStubCopies(volDataList: CallNumData[]): Promise<any> {
+
+ const vols = [];
+ volDataList.forEach(volData => {
+
+ const vol = this.volcopy.createStubVol(
+ this.context.recordId,
+ volData.owner || this.auth.user().ws_ou()
+ );
+
+ if (volData.label) {vol.label(volData.label); }
+
+ volData.callnumber = vol.id(); // wanted by addStubCopies
+ vols.push(vol);
+ this.context.findOrCreateVolNode(vol);
+ });
+
+ return this.addStubCopies(vols, volDataList)
+ .then(_ => this.volcopy.setVolClassLabels(vols));
+ }
+
+ // Fetch vols by ID, but instead of retrieving their copies
+ // add a stub copy to each.
+ fetchVolsStubCopies(volDataList: CallNumData[]): Promise<any> {
+
+ const volIds = volDataList.map(volData => volData.callnumber);
+ const vols = [];
+
+ return this.pcrud.search('acn', {id: volIds})
+ .pipe(tap((vol: IdlObject) => vols.push(vol))).toPromise()
+ .then(_ => this.addStubCopies(vols, volDataList));
+ }
+
+ // Add a stub copy to each vol using data from the edit session.
+ addStubCopies(vols: IdlObject[], volDataList: CallNumData[]): Promise<any> {
+
+ const copies = [];
+ vols.forEach(vol => {
+ const volData = volDataList.filter(
+ vData => vData.callnumber === vol.id())[0];
+
+ const copy =
+ this.volcopy.createStubCopy(vol, {circLib: volData.owner});
+
+ this.context.findOrCreateCopyNode(copy);
+ copies.push(copy);
+ });
+
+ return this.volcopy.setCopyStatus(copies, this.context.fastAdd);
+ }
+
+ fetchCopies(copyIds: number | number[]): Promise<any> {
+ const ids = [].concat(copyIds);
+ return this.pcrud.search('acp', {id: ids}, COPY_FLESH)
+ .pipe(tap(copy => this.context.findOrCreateCopyNode(copy)))
+ .toPromise();
+ }
+
+ // Fetch call numbers and linked copies by call number ids.
+ fetchVols(volIds?: number | number[]): Promise<any> {
+ const ids = [].concat(volIds);
+
+ return this.pcrud.search('acn', {id: ids})
+ .pipe(tap(vol => this.context.findOrCreateVolNode(vol)))
+ .toPromise().then(_ => {
+ return this.pcrud.search('acp',
+ {call_number: ids, deleted: 'f'}, COPY_FLESH
+ ).pipe(tap(copy => this.context.findOrCreateCopyNode(copy))
+ ).toPromise();
+ });
+ }
+
+ // Fetch call numbers and copies by record ids.
+ fetchRecords(recordIds: number | number[]): Promise<any> {
+ const ids = [].concat(recordIds);
+
+ return this.pcrud.search('acn',
+ {record: ids, deleted: 'f', label: {'!=' : '##URI##'}},
+ {}, {idlist: true, atomic: true}
+ ).toPromise().then(volIds => this.fetchVols(volIds));
+ }
+
+
+ save(close?: boolean): Promise<any> {
+ this.loading = true;
+
+ // Volume update API wants volumes fleshed with copies, instead
+ // of the other way around, which is what we have here.
+ const volumes: IdlObject[] = [];
+
+ this.context.volNodes().forEach(volNode => {
+ const newVol = this.idl.clone(volNode.target);
+ const copies: IdlObject[] = [];
+
+ volNode.children.forEach(copyNode => {
+ const copy = copyNode.target;
+
+ if (copy.isnew() && !copy.barcode()) {
+ // A new copy w/ no barcode is a stub copy sitting
+ // on an empty call number. Ignore it.
+ return;
+ }
+
+ if (copy.ischanged() || copy.isnew() || copy.isdeleted()) {
+ const copyClone = this.idl.clone(copy);
+ // De-flesh call number
+ copyClone.call_number(copy.call_number().id());
+ copies.push(copyClone);
+ }
+ });
+
+ newVol.copies(copies);
+
+ if (newVol.ischanged() || newVol.isnew() || copies.length > 0) {
+ volumes.push(newVol);
+ }
+ });
+
+ this.context.volsToDelete.forEach(vol => {
+ const cloneVol = this.idl.clone(vol);
+ // No need to flesh copies -- they'll be force deleted.
+ cloneVol.copies([]);
+ volumes.push(cloneVol);
+ });
+
+ this.context.copiesToDelete.forEach(copy => {
+ const cloneCopy = this.idl.clone(copy);
+ const copyVol = cloneCopy.call_number();
+ cloneCopy.call_number(copyVol.id()); // de-flesh
+
+ let vol = volumes.filter(v => v.id() === copyVol.id())[0];
+
+ if (vol) {
+ vol.copies().push(cloneCopy);
+ } else {
+ vol = this.idl.clone(copyVol);
+ vol.copies([cloneCopy]);
+ }
+
+ volumes.push(vol);
+ });
+
+ // De-flesh before posting
+ volumes.forEach(vol => {
+ vol.copies().forEach(copy => {
+ ['editor', 'creator', 'location'].forEach(field => {
+ if (typeof copy[field]() === 'object') {
+ copy[field](copy[field]().id());
+ }
+ });
+ });
+ });
+
+ let promise: Promise<number[]> = Promise.resolve([]);
+
+ if (volumes.length > 0) {
+ promise = this.saveApi(volumes, false, close);
+ }
+
+ return promise.then(copyIds => {
+
+ // In addition to the copies edited in this update call,
+ // reload any other copies that were previously loaded.
+ const ids: any = {}; // dedupe
+ this.context.copyList()
+ .map(c => c.id())
+ .filter(id => id > 0) // scrub the new copy IDs
+ .concat(copyIds)
+ .forEach(id => ids[id] = true);
+
+ copyIds = Object.keys(ids).map(id => Number(id));
+
+ if (close) {
+ return this.openPrintLabels(copyIds)
+ .then(_ => setTimeout(() => window.close()));
+ }
+
+ return this.load(Object.keys(ids).map(id => Number(id)));
+
+ }).then(_ => this.loading = false);
+ }
+
+ saveApi(volumes: IdlObject[], override?:
+ boolean, close?: boolean): Promise<number[]> {
+
+ let method = 'open-ils.cat.asset.volume.fleshed.batch.update';
+ if (override) { method += '.override'; }
+
+ return this.net.request('open-ils.cat',
+ method, this.auth.token(), volumes, true,
+ { auto_merge_vols: true,
+ create_parts: true,
+ return_copy_ids: true,
+ force_delete_copies: true
+ }
+
+ ).toPromise().then(copyIds => {
+
+ const evt = this.evt.parse(copyIds);
+
+ if (evt) {
+ // TODO: handle overrides?
+ // return this.saveApi(volumes, true, close);
+ this.loading = false;
+ alert(evt);
+ return Promise.reject();
+ }
+
+ return copyIds;
+ });
+ }
+
+ savePrintLabels() {
+ this.volcopy.defaults.values.print_labels = this.printLabels === true;
+ this.volcopy.saveDefaults();
+ }
+
+ openPrintLabels(copyIds?: number[]): Promise<any> {
+ if (!this.printLabels) { return Promise.resolve(); }
+
+ if (!copyIds || copyIds.length === 0) {
+ copyIds = this.context.copyList()
+ .map(c => c.id()).filter(id => id > 0);
+ }
+
+ return this.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.anon_cache.set_value',
+ null, 'print-labels-these-copies', {copies : copyIds}
+
+ ).toPromise().then(key => {
+
+ const url = '/eg/staff/cat/printlabels/' + key;
+ setTimeout(() => window.open(url, '_blank'));
+ });
+ }
+
+ isNotSaveable(): boolean {
+ return !(this.volsCanSave && this.attrsCanSave);
+ }
+}
+
+
+
--- /dev/null
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {CommonWidgetsModule} from '@eg/share/common-widgets.module';
+import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module';
+import {VolCopyRoutingModule} from './routing.module';
+import {VolCopyComponent} from './volcopy.component';
+import {VolEditComponent} from './vol-edit.component';
+import {VolCopyService} from './volcopy.service';
+import {CopyAttrsComponent} from './copy-attrs.component';
+import {ItemLocationSelectModule} from '@eg/share/item-location-select/item-location-select.module';
+import {VolCopyConfigComponent} from './config.component';
+
+@NgModule({
+ declarations: [
+ VolCopyComponent,
+ VolEditComponent,
+ CopyAttrsComponent,
+ VolCopyConfigComponent
+ ],
+ imports: [
+ StaffCommonModule,
+ CommonWidgetsModule,
+ HoldingsModule,
+ VolCopyRoutingModule,
+ ItemLocationSelectModule
+ ],
+ providers: [
+ VolCopyService
+ ]
+})
+
+export class VolCopyModule {
+}
--- /dev/null
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs';
+import {map, tap, mergeMap} from 'rxjs/operators';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {EventService, EgEvent} from '@eg/core/event.service';
+import {AuthService} from '@eg/core/auth.service';
+import {VolCopyContext} from './volcopy';
+import {HoldingsService, CallNumData} from '@eg/staff/share/holdings/holdings.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {StoreService} from '@eg/core/store.service';
+import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+/* Managing volcopy data */
+
+interface VolCopyDefaults {
+ values: {[field: string]: any};
+ hidden: {[field: string]: boolean};
+}
+
+@Injectable()
+export class VolCopyService {
+
+ autoId = -1;
+
+ localOrgs: number[];
+ defaults: VolCopyDefaults = null;
+ copyStatuses: {[id: number]: IdlObject} = {};
+ bibParts: {[bibId: number]: IdlObject[]} = {};
+
+ // This will be all 'local' copy locations plus any remote
+ // locations that we are required to interact with.
+ copyLocationMap: {[id: number]: IdlObject} = {};
+
+ // Track this here so it can survive route changes.
+ currentContext: VolCopyContext;
+
+ statCatEntryMap: {[id: number]: IdlObject} = {}; // entry id => entry
+
+ templateNames: ComboboxEntry[] = [];
+ templates: any = {};
+
+ commonData: {[key: string]: IdlObject[]} = {};
+ magicCopyStats: number[] = [];
+
+ hideVolOrgs: number[] = [];
+
+ constructor(
+ private evt: EventService,
+ private net: NetService,
+ private idl: IdlService,
+ private org: OrgService,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private holdings: HoldingsService,
+ private store: StoreService,
+ private serverStore: ServerStoreService
+ ) {}
+
+
+ // Fetch the data that is always needed.
+ load(): Promise<any> {
+
+ if (this.commonData.acp_item_type_map) { return Promise.resolve(); }
+
+ this.localOrgs = this.org.fullPath(this.auth.user().ws_ou(), true);
+
+ this.hideVolOrgs = this.org.list()
+ .filter(o => !this.org.canHaveVolumes(o)).map(o => o.id());
+
+ return this.net.request(
+ 'open-ils.cat', 'open-ils.cat.volcopy.data', this.auth.token()
+ ).pipe(tap(dataset => {
+ const key = Object.keys(dataset)[0];
+ this.commonData[key] = dataset[key];
+ })).toPromise()
+ .then(_ => this.ingestCommonData())
+
+ // These will come up later -- prefetch.
+ .then(_ => this.serverStore.getItemBatch([
+ 'cat.copy.templates',
+ 'eg.cat.volcopy.defaults',
+ 'eg.cat.record.summary.collapse'
+ ]))
+
+ .then(_ => this.holdings.getMagicCopyStatuses())
+ .then(stats => this.magicCopyStats = stats)
+ .then(_ => this.fetchDefaults())
+ .then(_ => this.fetchTemplates());
+ }
+
+ ingestCommonData() {
+
+ this.commonData.acp_location.forEach(
+ loc => this.copyLocationMap[loc.id()] = loc);
+
+ // Remove the -1 prefix and suffix so they can be treated
+ // specially in the markup.
+ this.commonData.acn_prefix =
+ this.commonData.acn_prefix.filter(pfx => pfx.id() !== -1);
+
+ this.commonData.acn_suffix =
+ this.commonData.acn_suffix.filter(sfx => sfx.id() !== -1);
+
+ this.commonData.acp_status.forEach(
+ stat => this.copyStatuses[stat.id()] = stat);
+
+ this.commonData.acp_stat_cat.forEach(cat => {
+ cat.entries().forEach(
+ entry => this.statCatEntryMap[entry.id()] = entry);
+ });
+ }
+
+ getLocation(id: number): Promise<IdlObject> {
+ if (this.copyLocationMap[id]) {
+ return Promise.resolve(this.copyLocationMap[id]);
+ }
+
+ return this.pcrud.retrieve('acpl', id)
+ .pipe(tap(loc => this.copyLocationMap[loc.id()] = loc))
+ .toPromise();
+ }
+
+ fetchTemplates(): Promise<any> {
+
+ // First check for local copy templates, since server-side
+ // templates are new w/ this code. Move them to the server.
+ const tmpls = this.store.getLocalItem('cat.copy.templates');
+
+ const promise = tmpls ?
+ this.serverStore.setItem('cat.copy.templates', tmpls) :
+ Promise.resolve();
+
+ return promise
+ .then(_ => this.serverStore.getItem('cat.copy.templates'))
+ .then(templates => {
+
+ if (!templates) { return null; }
+
+ this.templates = templates;
+
+ this.templateNames = Object.keys(templates)
+ .sort((n1, n2) => n1 < n2 ? -1 : 1)
+ .map(name => ({id: name, label: name}));
+
+ this.store.removeLocalItem('cat.copy.templates');
+ });
+ }
+
+
+ saveTemplates(): Promise<any> {
+ this.store.setLocalItem('cat.copy.templates', this.templates);
+ // Re-sort, etc.
+ return this.fetchTemplates();
+ }
+
+ fetchDefaults(): Promise<any> {
+ if (this.defaults) { return Promise.resolve(); }
+
+ return this.serverStore.getItem('eg.cat.volcopy.defaults').then(
+ (defaults: VolCopyDefaults) => {
+ this.defaults = defaults || {values: {}, hidden: {}};
+ }
+ );
+ }
+
+ // Fetch vol labels for a single record based on the defeault
+ // classification scheme
+ fetchRecordVolLabels(id: number): Promise<string[]> {
+ if (!id) { return Promise.resolve([]); }
+
+ // NOTE: see https://bugs.launchpad.net/evergreen/+bug/1874897
+ // for more on MARC call numbers and classification scheme.
+ // If there is no workstation-default value, pass null
+ // to use the org unit default.
+
+ return this.net.request(
+ 'open-ils.cat',
+ 'open-ils.cat.biblio.record.marc_cn.retrieve',
+ id, this.defaults.values.classification || null
+ ).toPromise().then(res => {
+ return Object.values(res)
+ .map(blob => Object.values(blob)[0]).sort();
+ });
+ }
+
+ createStubVol(recordId: number, orgId: number, options?: any): IdlObject {
+ if (!options) { options = {}; }
+
+ const vol = this.idl.create('acn');
+ vol.id(this.autoId--);
+ vol.isnew(true);
+ vol.record(recordId);
+ vol.label(null);
+ vol.owning_lib(Number(orgId));
+ vol.prefix(this.defaults.values.prefix || -1);
+ vol.suffix(this.defaults.values.suffix || -1);
+
+ return vol;
+ }
+
+ createStubCopy(vol: IdlObject, options?: any): IdlObject {
+ if (!options) { options = {}; }
+
+ const copy = this.idl.create('acp');
+ copy.id(this.autoId--);
+ copy.isnew(true);
+ copy.call_number(vol); // fleshed
+ copy.price('0.00');
+ copy.deposit_amount('0.00');
+ copy.fine_level(2); // Normal
+ copy.loan_duration(2); // Normal
+ copy.location(this.commonData.acp_default_location); // fleshed
+ copy.circ_lib(Number(options.circLib || vol.owning_lib()));
+
+ copy.deposit('f');
+ copy.circulate('t');
+ copy.holdable('t');
+ copy.opac_visible('t');
+ copy.ref('f');
+ copy.mint_condition('t');
+
+ copy.parts([]);
+ copy.tags([]);
+ copy.stat_cat_entries([]);
+
+ return copy;
+ }
+
+
+ // Applies label_class values to a batch of volumes, followed by
+ // applying labels to vols that need it.
+ setVolClassLabels(vols: IdlObject[]): Promise<any> {
+
+ return this.applyVolClasses(vols)
+ .then(_ => this.applyVolLabels(vols));
+ }
+
+ // Apply label_class values to any vols that need it based either on
+ // the workstation default value or the org setting for the
+ // owning lib library.
+ applyVolClasses(vols: IdlObject[]): Promise<any> {
+
+ vols = vols.filter(v => !v.label_class());
+
+ const orgIds: any = {};
+ vols.forEach(vol => orgIds[vol.owning_lib()] = true);
+
+ let promise = Promise.resolve(); // Serialization
+
+ if (this.defaults.values.classification) {
+ // Workstation default classification overrides the
+ // classification that might be used at the owning lib.
+
+ vols.forEach(vol =>
+ vol.label_class(this.defaults.values.classification));
+
+ return promise;
+
+ } else {
+
+ // Get the label class default for each owning lib and
+ // apply to the volumes owned by that lib.
+
+ Object.keys(orgIds).map(orgId => Number(orgId))
+ .forEach(orgId => {
+ promise = promise.then(_ => {
+
+ return this.org.settings(
+ 'cat.default_classification_scheme', orgId)
+ .then(sets => {
+
+ const orgVols = vols.filter(v => v.owning_lib() === orgId);
+ orgVols.forEach(vol => {
+ vol.label_class(
+ sets['cat.default_classification_scheme'] || 1
+ );
+ });
+ });
+ });
+ });
+ }
+
+ return promise;
+ }
+
+ // Apply labels to volumes based on the appropriate MARC call number.
+ applyVolLabels(vols: IdlObject[]): Promise<any> {
+
+ vols = vols.filter(v => !v.label());
+
+ // Serialize
+ let promise = Promise.resolve();
+
+ vols.forEach(vol => {
+
+ // Avoid unnecessary lookups.
+ // Note the label may have been applied to this volume
+ // in a previous iteration of this loop.
+ if (vol.label()) { return; }
+
+ promise = promise.then(_ => {
+ return this.net.request(
+ 'open-ils.cat',
+ 'open-ils.cat.biblio.record.marc_cn.retrieve',
+ vol.record(), vol.label_class()).toPromise()
+
+ .then(cnList => {
+ // Use '_' as a placeholder to indicate when a
+ // vol has already been addressed.
+ let label = '_';
+
+ if (cnList.length > 0) {
+ const field = Object.keys(cnList[0])[0];
+ label = cnList[0][field];
+ }
+
+ // Avoid making duplicate marc_cn calls by applying
+ // the label to all vols that apply.
+ vols.forEach(vol2 => {
+ if (vol2.record() === vol.record() &&
+ vol2.label_class() === vol.label_class()) {
+ vol.label(label);
+ }
+ });
+ });
+ });
+ });
+
+ return promise.then(_ => {
+ // Remove the placeholder label
+ vols.forEach(vol => {
+ if (vol.label() === '_') { vol.label(''); }
+ });
+ });
+ }
+
+ // Sets the default copy status for a batch of copies.
+ setCopyStatus(copies: IdlObject[], fastAdd: boolean): Promise<any> {
+
+ const setting = fastAdd ?
+ 'cat.default_copy_status_fast' :
+ 'cat.default_copy_status_normal';
+
+ let promise = Promise.resolve(); // Seralize
+
+ copies.forEach(copy => {
+
+ // Avoid unnecessary lookups. Copy may have been modified
+ // during a previous iteration of this loop.
+ if (!isNaN(copy.status())) { return; }
+
+ promise = promise.then(_ =>
+ this.org.settings(setting, copy.circ_lib())
+
+ ).then(sets => {
+
+ // 0 == Available; 5 == In Process
+ const stat = sets[setting] || (fastAdd ? 0 : 5);
+
+ copies.forEach(copy2 => {
+ if (copy2.circ_lib() === copy.circ_lib()) {
+ copy2.status(stat);
+ }
+ });
+ });
+ });
+
+ return promise;
+ }
+
+
+ saveDefaults(): Promise<any> {
+
+ // Scrub unnecessary content before storing.
+
+ Object.keys(this.defaults.values).forEach(field => {
+ if (this.defaults.values[field] === null) {
+ delete this.defaults.values[field];
+ }
+ });
+
+ Object.keys(this.defaults.hidden).forEach(field => {
+ if (this.defaults.hidden[field] !== true) {
+ delete this.defaults.hidden[field];
+ }
+ });
+
+ return this.serverStore.setItem(
+ 'eg.cat.volcopy.defaults', this.defaults);
+ }
+
+ fetchBibParts(recordIds: number[]) {
+
+ if (recordIds.length === 0) { return; }
+
+ // Avoid doubling up
+ if (this.bibParts[recordIds[0]]) { return; }
+
+ this.pcrud.search('bmp',
+ {record: recordIds, deleted: 'f'})
+ .subscribe(
+ part => {
+ if (!this.bibParts[part.record()]) {
+ this.bibParts[part.record()] = [];
+ }
+ this.bibParts[part.record()].push(part);
+ },
+ err => {},
+ () => {
+ recordIds.forEach(bibId => {
+ if (this.bibParts[bibId]) {
+ this.bibParts[bibId] = this.bibParts[bibId]
+ .sort((p1, p2) =>
+ p1.label_sortkey() < p2.label_sortkey() ? -1 : 1);
+ }
+ });
+ }
+ );
+ }
+
+
+ copyStatIsMagic(statId: number): boolean {
+ return this.magicCopyStats.includes(statId);
+ }
+
+ restrictCopyDelete(statId: number): boolean {
+ return this.copyStatuses[statId] &&
+ this.copyStatuses[statId].restrict_copy_delete() === 't';
+ }
+}
+
--- /dev/null
+import {IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+
+/* Models the holdings tree and manages related data shared
+ * volcopy across components. */
+
+export class HoldingsTreeNode {
+ children: HoldingsTreeNode[];
+ nodeType: 'org' | 'vol' | 'copy';
+ target: any;
+ parentNode: HoldingsTreeNode;
+ constructor() {
+ this.children = [];
+ }
+}
+
+class HoldingsTree {
+ root: HoldingsTreeNode;
+ constructor() {
+ this.root = new HoldingsTreeNode();
+ }
+}
+
+export class VolCopyContext {
+
+ holdings: HoldingsTree = new HoldingsTree();
+ org: OrgService; // injected
+
+ sessionType: 'copy' | 'vol' | 'record' | 'mixed';
+
+ // Edit content comes from a cached session
+ session: string;
+
+ // Note in multi-record mode this value will be unset.
+ recordId: number;
+
+ // Load specific call number by ID.
+ volId: number;
+
+ // Load specific copy by ID.
+ copyId: number;
+
+ fastAdd: boolean;
+
+ volsToDelete: IdlObject[] = [];
+ copiesToDelete: IdlObject[] = [];
+
+ reset() {
+ this.holdings = new HoldingsTree();
+ this.volsToDelete = [];
+ this.copiesToDelete = [];
+ }
+
+ orgNodes(): HoldingsTreeNode[] {
+ return this.holdings.root.children;
+ }
+
+ volNodes(): HoldingsTreeNode[] {
+ let vols = [];
+ this.orgNodes().forEach(orgNode =>
+ vols = vols.concat(orgNode.children));
+ return vols;
+ }
+
+ copyList(): IdlObject[] {
+ let copies = [];
+ this.volNodes().forEach(volNode => {
+ copies = copies.concat(volNode.children.map(c => c.target));
+ });
+ return copies;
+ }
+
+ // Returns IDs for all bib records represented in our holdings tree.
+ getRecordIds(): number[] {
+ const idHash: {[id: number]: boolean} = {};
+
+ this.volNodes().forEach(volNode =>
+ idHash[volNode.target.record()] = true);
+
+ return Object.keys(idHash).map(id => Number(id));
+ }
+
+ // When working on exactly one record, set our recordId value.
+ setRecordId() {
+ if (!this.recordId) {
+ const ids = this.getRecordIds();
+ if (ids.length === 1) {
+ this.recordId = ids[0];
+ }
+ }
+ }
+
+ // Adds an org unit node; unsorted.
+ findOrCreateOrgNode(orgId: number): HoldingsTreeNode {
+
+ const existing: HoldingsTreeNode =
+ this.orgNodes().filter(n => n.target.id() === orgId)[0];
+
+ if (existing) { return existing; }
+
+ const node: HoldingsTreeNode = new HoldingsTreeNode();
+ node.nodeType = 'org';
+ node.target = this.org.get(orgId);
+ node.parentNode = this.holdings.root;
+
+ this.orgNodes().push(node);
+
+ return node;
+ }
+
+ findOrCreateVolNode(vol: IdlObject): HoldingsTreeNode {
+ const orgId = vol.owning_lib();
+ const orgNode = this.findOrCreateOrgNode(orgId);
+
+ const existing = orgNode.children.filter(
+ n => n.target.id() === vol.id())[0];
+
+ if (existing) { return existing; }
+
+ const node: HoldingsTreeNode = new HoldingsTreeNode();
+ node.nodeType = 'vol';
+ node.target = vol;
+ node.parentNode = orgNode;
+
+ orgNode.children.push(node);
+
+ return node;
+ }
+
+
+ findOrCreateCopyNode(copy: IdlObject): HoldingsTreeNode {
+ const volNode = this.findOrCreateVolNode(copy.call_number());
+
+ const existing = volNode.children.filter(
+ c => c.target.id() === copy.id())[0];
+
+ if (existing) { return existing; }
+
+ const node: HoldingsTreeNode = new HoldingsTreeNode();
+ node.nodeType = 'copy';
+ node.target = copy;
+ node.parentNode = volNode;
+
+ volNode.children.push(node);
+
+ return node;
+ }
+
+ removeVolNode(volId: number) {
+ this.orgNodes().forEach(orgNode => {
+ for (let idx = 0; idx < orgNode.children.length; idx++) {
+ if (orgNode.children[idx].target.id() === volId) {
+ orgNode.children.splice(idx, 1);
+ break;
+ }
+ }
+ });
+ }
+
+ removeCopyNode(copyId: number) {
+ this.volNodes().forEach(volNode => {
+ for (let idx = 0; idx < volNode.children.length; idx++) {
+ if (volNode.children[idx].target.id() === copyId) {
+ volNode.children.splice(idx, 1);
+ break;
+ }
+ }
+ });
+ }
+
+ sortHoldings() {
+
+ this.orgNodes().forEach(orgNode => {
+ orgNode.children.forEach(volNode => {
+
+ // Sort copys by barcode code
+ volNode.children = volNode.children.sort((c1, c2) =>
+ c1.target.barcode() < c2.target.barcode() ? -1 : 1);
+
+ });
+
+ // Sort call numbers by label
+ orgNode.children = orgNode.children.sort((c1, c2) =>
+ c1.target.label() < c2.target.label() ? -1 : 1);
+ });
+
+ // sort org units by shortname
+ this.holdings.root.children = this.orgNodes().sort((o1, o2) =>
+ o1.target.shortname() < o2.target.shortname() ? -1 : 1);
+ }
+
+ changesPending(): boolean {
+ const modified = (o: IdlObject): boolean => {
+ return o.isnew() || o.ischanged() || o.isdeleted();
+ };
+
+ if (this.volNodes().filter(n => modified(n.target)).length > 0) {
+ return true;
+ }
+
+ if (this.copyList().filter(c => modified(c)).length > 0) {
+ return true;
+ }
+
+ return false;
+ }
+}
<eg-mark-damaged-dialog #markDamagedDialog></eg-mark-damaged-dialog>
<eg-mark-missing-dialog #markMissingDialog></eg-mark-missing-dialog>
<eg-copy-alerts-dialog #copyAlertsDialog></eg-copy-alerts-dialog>
+<eg-copy-tags-dialog #copyTagsDialog></eg-copy-tags-dialog>
<eg-replace-barcode-dialog #replaceBarcode></eg-replace-barcode-dialog>
<eg-delete-holding-dialog #deleteHolding></eg-delete-holding-dialog>
<eg-bucket-dialog #bucketDialog></eg-bucket-dialog>
</eg-grid-toolbar-action>
<eg-grid-toolbar-action
+ i18n-group group="Add" i18n-label label="Add/Manage Item Tags"
+ (onClick)="openItemTags($event)">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
i18n-group group="Add" i18n-label label="Add Items To Bucket"
(onClick)="openBucketDialog($event)">
</eg-grid-toolbar-action>
import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
import {CopyAlertsDialogComponent
} from '@eg/staff/share/holdings/copy-alerts-dialog.component';
+import {CopyTagsDialogComponent
+ } from '@eg/staff/share/holdings/copy-tags-dialog.component';
import {ReplaceBarcodeDialogComponent
} from '@eg/staff/share/holdings/replace-barcode-dialog.component';
import {DeleteHoldingDialogComponent
private markMissingDialog: MarkMissingDialogComponent;
@ViewChild('copyAlertsDialog', { static: true })
private copyAlertsDialog: CopyAlertsDialogComponent;
+ @ViewChild('copyTagsDialog', {static: false})
+ private copyTagsDialog: CopyTagsDialogComponent;
@ViewChild('replaceBarcode', { static: true })
private replaceBarcode: ReplaceBarcodeDialogComponent;
@ViewChild('deleteHolding', { static: true })
);
}
+ openItemTags(rows: HoldingsEntry[]) {
+ const copyIds = this.selectedCopyIds(rows);
+ if (copyIds.length === 0) { return; }
+
+ this.copyTagsDialog.copyIds = copyIds;
+ this.copyTagsDialog.open({size: 'lg'}).subscribe(
+ modified => {
+ if (modified) {
+ this.hardRefresh();
+ }
+ }
+ );
+ }
+
openReplaceBarcodeDialog(rows: HoldingsEntry[]) {
const ids = this.selectedCopyIds(rows);
if (ids.length === 0) { return; }
import {PatronBarcodeValidatorDirective} from '@eg/share/validators/patron_barcode_validator.directive';
import {BroadcastService} from '@eg/share/util/broadcast.service';
import {CourseService} from './share/course.service';
+import {FileExportService} from '@eg/share/util/file-export.service';
/**
* Imports the EG common modules and adds modules common to all staff UI's.
AccessKeyService,
AudioService,
BroadcastService,
- CourseService
+ CourseService,
+ FileExportService
]
};
}
const query: any = new Array();
query.push(base);
+ console.log(JSON.stringify(this.eventsDataSource.filters));
+
Object.keys(this.eventsDataSource.filters).forEach(key => {
Object.keys(this.eventsDataSource.filters[key]).forEach(key2 => {
query.push(this.eventsDataSource.filters[key][key2]);
<div class="d-flex">
<div class="flex-1 font-weight-bold" i18n>Title:</div>
<div class="flex-3">
- <eg-bib-display-field [summary]="summary" field="title">
+ <eg-bib-display-field [summary]="summary" field="title"
+ routerLink="/staff/catalog/record/{{summary.id}}">
</eg-bib-display-field>
</div>
<div class="flex-1 font-weight-bold pl-1" i18n>Edition:</div>
ngOnInit() {
- if (this.summary) {
- this.summary.getBibCallNumber();
- this.loadCourseInformation(this.summary.record.id());
- } else {
- if (this.recordId) {
- this.loadSummary();
- }
- }
-
this.store.getItem('eg.cat.record.summary.collapse')
.then(value => this.expand = !value)
- .then(() => this.initDone = true);
+ .then(_ => this.cat.fetchCcvms())
+ .then(_ => {
+ if (this.summary) {
+ return this.loadCourseInformation(this.summary.record.id())
+ .then(_ => this.summary.getBibCallNumber());
+ } else {
+ if (this.recordId) {
+ return this.loadSummary();
+ }
+ }
+ }).then(_ => this.initDone = true);
}
saveExpandState() {
this.store.setItem('eg.cat.record.summary.collapse', !this.expand);
}
- loadSummary(): void {
- this.loadCourseInformation(this.recordId);
- this.bib.getBibSummary(this.recordId).toPromise()
- .then(summary => {
- summary.getBibCallNumber();
- this.summary = summary;
+ loadSummary(): Promise<any> {
+ return this.loadCourseInformation(this.recordId)
+ .then(_ => {
+ return this.bib.getBibSummary(this.recordId).toPromise()
+ .then(summary => {
+ this.summary = summary;
+ return summary.getBibCallNumber();
+ });
});
}
- loadCourseInformation(recordId) {
- this.org.settings('circ.course_materials_opt_in').then(setting => {
+ loadCourseInformation(recordId): Promise<any> {
+ return this.org.settings('circ.course_materials_opt_in')
+ .then(setting => {
if (setting['circ.course_materials_opt_in']) {
this.course.fetchCoursesForRecord(recordId).then(courseList => {
if (courseList) {
--- /dev/null
+
+<div class="border rounded m-1">
+ <div class="font-weight-bold header p-2 d-flex" i18n>
+ {{label}} <span *ngIf="hasChanged" class="text-danger">*</span>
+ <ng-container *ngIf="bulky()">
+ <div class="flex-1"></div>
+ <a href='javascript:;' (click)="expanded = true" *ngIf="!expanded">
+ <span class="material-icons">unfold_more</span>
+ </a>
+ <a href='javascript:;' (click)="expanded = false" *ngIf="expanded">
+ <span class="material-icons">unfold_less</span>
+ </a>
+ </ng-container>
+ </div>
+ <div tabindex="0" class="p-2" *ngIf="!editing || multiValue()"
+ (click)="enterEditMode()" (keyup.enter)="enterEditMode()"
+ [ngClass]="{'has-changes': hasChanged, 'bg-warning': warnOnRequired()}">
+ <div class="d-flex"
+ *ngFor="let count of labelCounts | keyvalue; let idx = index">
+ <ng-container *ngIf="!expanded && !editing && idx === defaultDisplayCount">
+ <span class="text-info" i18n>...</span>
+ </ng-container>
+ <ng-container *ngIf="expanded || editing || idx < defaultDisplayCount">
+ <ng-container *ngIf="editing">
+ <div class="ml-4 mr-2">
+ <input type="checkbox" class="form-check-input"
+ [(ngModel)]="editValues[count.key]"/>
+ </div>
+ </ng-container>
+ <div class="flex-1">
+ <ng-container *ngIf="displayAs == 'bool'">
+ <ng-container *ngIf="valueIsUnset(count.key); else defaultBool">
+ <span i18n><Unset></span>
+ </ng-container>
+ <ng-template #defaultBool>
+ <span *ngIf="count.key == 't'" i18n>Yes</span>
+ <span *ngIf="count.key == 'f'" i18n>No</span>
+ </ng-template>
+ </ng-container>
+ <ng-container *ngIf="displayAs == 'currency'">
+ <ng-container
+ *ngIf="valueIsUnset(count.key); else defaultCurrency">
+ <span i18n><Unset></span>
+ </ng-container>
+ <ng-template #defaultCurrency>{{count.key | currency}}</ng-template>
+ </ng-container>
+ <ng-container *ngIf="displayAs != 'bool' && displayAs != 'currency'">
+ <ng-container
+ *ngIf="valueIsUnset(count.key); else default">
+ <span i18n><Unset></span>
+ </ng-container>
+ <ng-template #default>{{count.key}}</ng-template>
+ </ng-container>
+ </div>
+ <div i18n>{{count.value}} copies</div>
+ </ng-container>
+ </div>
+ </div>
+ <ng-container *ngIf="editing">
+ <ng-container *ngTemplateOutlet="editTemplate"></ng-container>
+ <div class="mt-1">
+ <button class="btn btn-outline-dark" (click)="save()" i18n>Apply</button>
+ <button class="btn btn-outline-dark ml-1" (click)="cancel()" i18n>Cancel</button>
+ <button class="btn btn-outline-dark ml-1" (click)="clear()" i18n>Clear</button>
+ </div>
+ </ng-container>
+</div>
+
--- /dev/null
+import {Component, OnInit, Input, Output, ViewChild, TemplateRef,
+ EventEmitter} from '@angular/core';
+import {StringComponent} from '@eg/share/string/string.component';
+
+/**
+ * Displays attribute values and associated copy counts for managing
+ * updates to batches of items.
+ */
+
+
+// Map of display value to boolean indicating whether a given item
+// should be modified.
+export interface BatchChangeSelection {
+ [value: string]: boolean;
+}
+
+@Component({
+ selector: 'eg-batch-item-attr',
+ templateUrl: 'batch-item-attr.component.html',
+ styles: [
+ `.header { background-color: #d9edf7; }`,
+ `.has-changes { background-color: #dff0d8; }`
+ ]
+})
+
+export class BatchItemAttrComponent {
+
+ // Main display label, e.g. "Circulation Modifier"
+ @Input() label: string;
+
+ // Optional. Useful for exracting information (i.e. hasChanges)
+ // on a specific field from a set of batch attr components.
+ @Input() name: string;
+
+ // Maps display labels to the number of items that have the label.
+ // e.g. {"Stacks": 4, "Display": 12}
+ @Input() labelCounts: {[label: string]: number} = {};
+
+ // Ref to some type of edit widget for modifying the value.
+ // Note this component simply displays the template, it does not
+ // interact with the template in any way.
+ @Input() editTemplate: TemplateRef<any>;
+
+ @Input() editInputDomId = '';
+
+ // In some cases, we can map display labels to something more
+ // human friendly.
+ @Input() displayAs: 'bool' | 'currency' = null;
+
+ // Display only
+ @Input() readOnly = false;
+
+ // Warn the user when a required field has an empty value
+ @Input() valueRequired = false;
+
+ // If true, a value of '' is considered unset for display and
+ // valueRequired purposes.
+ @Input() emptyStringIsUnset = true;
+
+ // Lists larger than this will be partially hidden behind
+ // and expandy.
+ @Input() defaultDisplayCount = 7;
+
+ @Output() changesSaved: EventEmitter<BatchChangeSelection> =
+ new EventEmitter<BatchChangeSelection>();
+
+ @Output() changesCanceled: EventEmitter<void> = new EventEmitter<void>();
+ @Output() valueCleared: EventEmitter<void> = new EventEmitter<void>();
+
+ // Is the editTtemplate visible?
+ editing = false;
+
+ hasChanged = false;
+
+ // Showing all entries?
+ expanded = false;
+
+ // Indicate which display values the user wants to modify.
+ editValues: BatchChangeSelection = {};
+
+ constructor() {}
+
+ save() {
+ this.hasChanged = true;
+ this.editing = false;
+ this.changesSaved.emit(this.editValues);
+ }
+
+ cancel() {
+ this.editing = false;
+ this.changesCanceled.emit();
+ }
+
+ clear() {
+ this.hasChanged = true;
+ this.editing = false;
+ this.valueCleared.emit();
+ }
+
+ bulky(): boolean {
+ return Object.keys(this.labelCounts).length > this.defaultDisplayCount;
+ }
+
+ multiValue(): boolean {
+ return Object.keys(this.labelCounts).length > 1;
+ }
+
+ // True if a value is required and any value exists that's unset.
+ warnOnRequired(): boolean {
+ if (!this.valueRequired) { return false; }
+
+ return Object.keys(this.labelCounts)
+ .filter(key => this.valueIsUnset(key)).length > 0;
+ }
+
+ valueIsUnset(value: any): boolean {
+ return (
+ value === null ||
+ value === undefined ||
+ (this.emptyStringIsUnset && value === '')
+ );
+ }
+
+ enterEditMode() {
+ if (this.readOnly || this.editing) { return; }
+ this.editing = true;
+
+ // Assume all values should be edited by default
+ Object.keys(this.labelCounts).forEach(
+ key => this.editValues[key] = true);
+
+ if (this.editInputDomId) {
+ setTimeout(() => {
+ // Avoid using selectRootElement to focus.
+ // https://stackoverflow.com/a/36059595
+ const node = document.getElementById(this.editInputDomId);
+ if (node) { node.focus(); }
+ });
+ }
+ }
+}
+
+
+
<div class="modal-header">
<h4 class="modal-title">
<ng-container *ngIf="mode == 'create'">
- <span i18n>Adding alerts for {{copies.length}} item(s).</span>
+ <span i18n>Adding alerts for {{copyIds.length}} item(s).</span>
</ng-container>
<ng-container *ngIf="mode == 'manage'">
<span i18n>Managing alerts for item {{copies[0].barcode()}}</span>
export class CopyAlertsDialogComponent
extends DialogComponent implements OnInit {
- _copyIds: number[];
- @Input() set copyIds(ids: number[]) {
- this._copyIds = [].concat(ids);
- }
- get copyIds(): number[] {
- return this._copyIds;
- }
+ // If there are multiple copyIds, only new alerts may be applied.
+ // If there is only one copyId, then tags may be applied or removed.
+ @Input() copyIds: number[] = [];
- _mode: string; // create | manage
- @Input() set mode(m: string) {
- this._mode = m;
- }
- get mode(): string {
- return this._mode;
- }
+ mode: string; // create | manage
+
+ // If true, no attempt is made to save the new alerts to the
+ // database. It's assumed this takes place in the calling code.
+ // This is useful for creating alerts for new copies.
+ @Input() inPlaceMode = false;
// In 'create' mode, we may be adding notes to multiple copies.
copies: IdlObject[];
// In 'manage' mode we only handle a single copy.
copy: IdlObject;
+
alertTypes: ComboboxEntry[];
newAlert: IdlObject;
changesMade: boolean;
this.newAlert = this.idl.create('aca');
this.newAlert.create_staff(this.auth.user().id());
- if (this.copyIds.length === 0) {
+ if (this.copyIds.length === 0 && !this.inPlaceMode) {
return throwError('copy ID required');
}
// In manage mode, we can only manage a single copy.
- // But in create mode, we can add alerts to multiple copies.
-
- if (this.mode === 'manage') {
- if (this.copyIds.length > 1) {
- console.warn('Attempt to manage alerts for multiple copies.');
- this.copyIds = [this.copyIds[0]];
- }
+ // But in create mode, we can add tags to multiple copies.
+ if (this.copyIds.length === 1 && !this.inPlaceMode) {
+ this.mode = 'manage';
+ } else {
+ this.mode = 'create';
}
// Observerify data loading
}
getAlertTypes(): Promise<any> {
- if (this.alertTypes) {
- return Promise.resolve();
- }
+ if (this.alertTypes) { return Promise.resolve(); }
+
return this.pcrud.retrieveAll('ccat',
{ active: true,
scope_org: this.org.ancestors(this.auth.user().ws_ou(), true)
}
getCopies(): Promise<any> {
+ if (this.inPlaceMode) { return Promise.resolve(); }
+
return this.pcrud.search('acp', {id: this.copyIds}, {}, {atomic: true})
.toPromise().then(copies => {
this.copies = copies;
addNew() {
if (!this.newAlert.alert_type()) { return; }
+ if (this.inPlaceMode) {
+ this.close(this.newAlert);
+ return;
+ }
+
const alerts: IdlObject[] = [];
this.copies.forEach(c => {
const a = this.idl.clone(this.newAlert);
--- /dev/null
+<eg-string #successMsg text="Successfully Modified Item Tags" i18n-text></eg-string>
+<eg-string #errorMsg text="Failed To Modify Item Tags" i18n-text></eg-string>
+
+<ng-template #dialogContent>
+ <div class="modal-header">
+ <h4 class="modal-title">
+ <ng-container *ngIf="mode == 'create'">
+ <span i18n>Adding tags for {{copyIds.length}} item(s).</span>
+ </ng-container>
+ <ng-container *ngIf="mode == 'manage'">
+ <span i18n>Managing tags for item {{copy.barcode()}}</span>
+ </ng-container>
+ <span i18n></span>
+ </h4>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="close()">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body p-4 form-validated">
+
+ <ng-container *ngIf="mode == 'manage' && copy.tags().length">
+ <h4 i18n>Existing Tags</h4>
+ <div class="row mt-2 p-2" *ngFor="let map of copy.tags()">
+ <div class="col-lg-4">{{map.tag().tag_type().label()}}</div>
+ <div class="col-lg-5">{{map.tag().label()}}</div>
+ <div class="col-lg-3">
+ <button class="btn btn-outline-danger" (click)="removeTag(map.tag())" i18n>
+ Remove
+ </button>
+ </div>
+ </div>
+ <hr/>
+ </ng-container>
+
+ <h4 i18n>New Tags</h4>
+ <div class="row mt-2 p-2" *ngFor="let tag of newTags">
+ <ng-container *ngIf="!tag.isdeleted()">
+ <div class="col-lg-4">{{tagTypeMap[tag.tag_type()].label()}}</div>
+ <div class="col-lg-5">{{tag.label()}}</div>
+ <div class="col-lg-3">
+ <button class="btn btn-outline-danger" (click)="removeTag(tag)" i18n>
+ Remove
+ </button>
+ </div>
+ </ng-container>
+ </div>
+
+ <div class="row mt-2 p-2 rounded border border-success">
+ <div class="col-lg-4">
+ <eg-combobox [entries]="tagTypes" [(ngModel)]="curTagType"
+ i18n-placeholder placeholder="Select Tag Type...">
+ </eg-combobox>
+ </div>
+ <div class="col-lg-5">
+ <eg-combobox [asyncDataSource]="tagDataSource" [(ngModel)]="curTag"
+ [allowFreeText]="true"
+ i18n-placeholder placeholder="Select Tag Type...">
+ </eg-combobox>
+ </div>
+ <div class="col-lg-3">
+ <div class="pt-2">
+ <button class="btn btn-success" (click)="addNew()" i18n>Add Tag</button>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary" (click)="close()" i18n>Cancel</button>
+ <button class="btn btn-success mr-2" (click)="applyChanges()" i18n>Apply Changes</button>
+ </div>
+</ng-template>
--- /dev/null
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Observable, throwError, from, empty} from 'rxjs';
+import {tap, map, switchMap} from 'rxjs/operators';
+import {NetService} from '@eg/core/net.service';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+/**
+ * Dialog for managing copy tags.
+ */
+
+@Component({
+ selector: 'eg-copy-tags-dialog',
+ templateUrl: 'copy-tags-dialog.component.html'
+})
+
+export class CopyTagsDialogComponent
+ extends DialogComponent implements OnInit {
+
+ // If there are multiple copyIds, only new tags may be applied.
+ // If there is only one copyId, then tags may be applied or removed.
+ @Input() copyIds: number[] = [];
+
+ mode: string; // create | manage
+
+ // If true, no attempt is made to save the new tags to the
+ // database. It's assumed this takes place in the calling code.
+ // This is useful for creating tags for new copies.
+ @Input() inPlaceMode = false;
+
+ // In 'create' mode, we may be adding notes to multiple copies.
+ copies: IdlObject[] = [];
+
+ // In 'manage' mode we only handle a single copy.
+ copy: IdlObject;
+
+ tagTypes: ComboboxEntry[];
+
+ curTag: ComboboxEntry = null;
+ curTagType: ComboboxEntry = null;
+ newTags: IdlObject[] = [];
+ deletedMaps: IdlObject[] = [];
+ tagMap: {[id: number]: IdlObject} = {};
+ tagTypeMap: {[id: number]: IdlObject} = {};
+
+ tagDataSource: (term: string) => Observable<ComboboxEntry>;
+
+ @ViewChild('successMsg', { static: true }) private successMsg: StringComponent;
+ @ViewChild('errorMsg', { static: true }) private errorMsg: StringComponent;
+
+ constructor(
+ private modal: NgbModal, // required for passing to parent
+ private toast: ToastService,
+ private net: NetService,
+ private idl: IdlService,
+ private pcrud: PcrudService,
+ private org: OrgService,
+ private auth: AuthService) {
+ super(modal); // required for subclassing
+ }
+
+ ngOnInit() {
+
+ this.tagDataSource = term => {
+ if (!this.curTagType) { return empty(); }
+
+ return this.pcrud.search(
+ 'acpt', {
+ tag_type: this.curTagType.id,
+ '-or': [
+ {value: {'ilike': `%${term}%`}},
+ {label: {'ilike': `%${term}%`}}
+ ]
+ },
+ {order_by: {acpt: 'label'}}
+ ).pipe(map(copyTag => {
+ this.tagMap[copyTag.id()] = copyTag;
+ return {id: copyTag.id(), label: copyTag.label()};
+ }));
+ };
+ }
+
+ /**
+ */
+ open(args: NgbModalOptions): Observable<IdlObject[]> {
+ this.copy = null;
+ this.copies = [];
+ this.newTags = [];
+ this.deletedMaps = [];
+
+ if (this.copyIds.length === 0 && !this.inPlaceMode) {
+ return throwError('copy ID required');
+ }
+
+ // In manage mode, we can only manage a single copy.
+ // But in create mode, we can add tags to multiple copies.
+
+ if (this.copyIds.length === 1 && !this.inPlaceMode) {
+ this.mode = 'manage';
+ } else {
+ this.mode = 'create';
+ }
+
+ // Observify data loading
+ const obs = from(this.getTagTypes().then(_ => this.getCopies()));
+
+ // Return open() observable to caller
+ return obs.pipe(switchMap(_ => super.open(args)));
+ }
+
+ getTagTypes(): Promise<any> {
+ if (this.tagTypes) { return Promise.resolve(); }
+
+ this.tagTypes = [];
+ return this.pcrud.search('cctt',
+ {owner: this.org.ancestors(this.auth.user().ws_ou(), true)},
+ {order_by: {cctt: 'label'}}
+ ).pipe(tap(tag => {
+ this.tagTypeMap[tag.code()] = tag;
+ this.tagTypes.push({id: tag.code(), label: tag.label()});
+ })).toPromise();
+ }
+
+ getCopies(): Promise<any> {
+ if (this.inPlaceMode) { return Promise.resolve(); }
+
+ return this.pcrud.search('acp', {id: this.copyIds},
+ {flesh: 3, flesh_fields: {
+ acp: ['tags'], acptcm: ['tag'], acpt: ['tag_type']}},
+ {atomic: true}
+ )
+ .toPromise().then(copies => {
+ this.copies = copies;
+ if (copies.length === 1) {
+ this.copy = copies[0];
+ }
+ });
+ }
+
+ removeTag(tag: IdlObject) {
+ this.newTags = this.newTags.filter(t => t.id() !== tag.id());
+
+ if (tag.isnew() || this.mode === 'create') { return; }
+
+ const existing = this.copy.tags().filter(m => m.tag().id() === tag.id())[0];
+ if (!existing) { return; }
+
+ existing.isdeleted(true);
+ this.deletedMaps.push(existing);
+ this.copy.tags(this.copy.tags().filter(m => m.tag().id() !== tag.id()));
+ this.copy.ischanged(true);
+ }
+
+ addNew() {
+ if (!this.curTagType || !this.curTag) { return; }
+
+ let tag;
+
+ if (this.curTag.freetext) {
+ // Create a new tag w/ the provided tag text.
+ tag = this.idl.create('acpt');
+ tag.isnew(true);
+ tag.tag_type(this.curTagType.id);
+ tag.label(this.curTag.label);
+ tag.owner(this.auth.user().ws_ou());
+ tag.pub('t');
+ } else {
+ tag = this.tagMap[this.curTag.id];
+ }
+
+ this.newTags.push(tag);
+ }
+
+ createNewTags(): Promise<any> {
+ let promise = Promise.resolve();
+
+ this.newTags.forEach(tag => {
+ if (!tag.isnew()) { return; }
+
+ promise = promise.then(_ => {
+ return this.pcrud.create(tag).toPromise().then(id => {
+ console.log('create returned ', id);
+ tag.id(id);
+ });
+ });
+ });
+
+ return promise;
+ }
+
+ deleteMaps(): Promise<any> {
+ if (this.deletedMaps.length === 0) { return Promise.resolve(); }
+ return this.pcrud.remove(this.deletedMaps).toPromise();
+ }
+
+ applyChanges() {
+
+ if (this.inPlaceMode) {
+ this.close(this.newTags);
+ return;
+ }
+
+ let promise = this.deleteMaps().then(_ => this.createNewTags());
+
+ this.newTags.forEach(tag => {
+ this.copies.forEach(copy => {
+
+ if (copy.tags() && copy.tags().filter(
+ m => m.tag().id() === tag.id()).length > 0) {
+ return; // map already exists
+ }
+
+ promise = promise.then(_ => {
+ const tagMap = this.idl.create('acptcm');
+ tagMap.isnew(true);
+ tagMap.copy(copy.id());
+ tagMap.tag(tag.id());
+ return this.pcrud.create(tagMap).toPromise();
+ });
+ });
+ });
+
+ promise.then(_ => {
+ this.successMsg.current().then(msg => this.toast.success(msg));
+ this.close(this.newTags.length > 0);
+ });
+ }
+}
+
import {MarkDamagedDialogComponent} from './mark-damaged-dialog.component';
import {MarkMissingDialogComponent} from './mark-missing-dialog.component';
import {CopyAlertsDialogComponent} from './copy-alerts-dialog.component';
+import {CopyTagsDialogComponent} from './copy-tags-dialog.component';
import {ReplaceBarcodeDialogComponent} from './replace-barcode-dialog.component';
import {DeleteHoldingDialogComponent} from './delete-volcopy-dialog.component';
import {ConjoinedItemsDialogComponent} from './conjoined-items-dialog.component';
import {TransferItemsComponent} from './transfer-items.component';
import {TransferHoldingsComponent} from './transfer-holdings.component';
+import {BatchItemAttrComponent} from './batch-item-attr.component';
@NgModule({
declarations: [
MarkDamagedDialogComponent,
MarkMissingDialogComponent,
CopyAlertsDialogComponent,
+ CopyTagsDialogComponent,
ReplaceBarcodeDialogComponent,
DeleteHoldingDialogComponent,
ConjoinedItemsDialogComponent,
TransferItemsComponent,
- TransferHoldingsComponent
+ TransferHoldingsComponent,
+ BatchItemAttrComponent
],
imports: [
StaffCommonModule
MarkDamagedDialogComponent,
MarkMissingDialogComponent,
CopyAlertsDialogComponent,
+ CopyTagsDialogComponent,
ReplaceBarcodeDialogComponent,
DeleteHoldingDialogComponent,
ConjoinedItemsDialogComponent,
TransferItemsComponent,
- TransferHoldingsComponent
+ TransferHoldingsComponent,
+ BatchItemAttrComponent
],
providers: [
HoldingsService
* Common code for mananging holdings
*/
import {Injectable, EventEmitter} from '@angular/core';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
import {NetService} from '@eg/core/net.service';
import {AnonCacheService} from '@eg/share/util/anon-cache.service';
import {PcrudService} from '@eg/core/pcrud.service';
import {AuthService} from '@eg/core/auth.service';
import {IdlObject} from '@eg/core/idl.service';
import {EventService} from '@eg/core/event.service';
+import {PcrudService} from '@eg/core/pcrud.service';
-interface NewCallNumData {
+export interface CallNumData {
owner?: number;
label?: string;
fast_add?: boolean;
barcode?: string;
+ callnumber?: number;
}
@Injectable()
spawnAddHoldingsUi(
recordId: number, // Bib record ID
editExistingCallNums?: number[], // Add copies to / modify existing CNs
- newCallNumData?: NewCallNumData[], // Creating new call numbers
+ newCallNumData?: CallNumData[], // Creating new call numbers
editCopyIds?: number[], // Edit existing items
hideCopies?: boolean, // Hide the copy edit pane
hideVols?: boolean) {
return;
}
setTimeout(() => {
- const url = `/eg/staff/cat/volcopy/${key}`;
+ const tab = hideVols ? 'attrs' : 'holdings';
+ const url = `/eg2/staff/cat/volcopy/${tab}/session/${key}`;
window.open(url, '_blank');
});
});
}
});
}
+
+ /* TODO: make these more configurable per lp1616170 */
+ getMagicCopyStatuses(): Promise<number[]> {
+ return Promise.resolve([
+ 1, // Checked out
+ 3, // Lost
+ 6, // In transit
+ 8, // On holds shelf
+ 16, // Long overdue
+ 18 // Canceled Transit
+ ]);
+ }
}
border-left: 5px solid #FA787E;
}
+.invalid {
+ border-left: 5px solid #FA787E;
+}
+
/* Typical form CSS.
* Brings font size down 5% to squeeze a bit more in.
* Bold labels
color: black;
}
+/* Washed out version of the Bootstrap 'info' background.
+ * Useful for blocking out sections of a page/form without it
+ * being so intensely colorful */
+.bg-faint {
+ /*background-color: rgb(204, 229, 255, 0.3);*/
+
+ /* d9edf7 */
+ /*background-color: rgb(217, 237, 247, 0.5);*/
+
+ background-color: rgba(0,0,0,.03);
+}
+
/* Allow for larger XL dialogs */
@media (min-width: 1300px) { .modal-xl { max-width: 1200px; } }
@media (min-width: 1600px) { .modal-xl { max-width: 1500px; } }
}
}
+__PACKAGE__->register_method(
+ method => "volcopy_data",
+ api_name => "open-ils.cat.volcopy.data",
+ stream => 1,
+ argc => 3,
+ signature => {
+ desc => q|Returns a batch of org-scoped data types needed by the
+ volume/copy editor|,
+ params => [
+ {desc => 'Authtoken', type => 'string'}
+ ]
+ },
+ return => {desc => 'Stream of various object type lists', type => 'array'}
+);
+
+sub volcopy_data {
+ my ($self, $client, $auth) = @_;
+ my $e = new_editor(authtoken => $auth);
+
+ $e->checkauth or return $e->event;
+ my $org_ids = $U->get_org_ancestors($e->requestor->ws_ou);
+
+ $client->respond({
+ acp_location => $e->search_asset_copy_location([
+ {deleted => 'f', owning_lib => $org_ids},
+ {order_by => {acpl => 'name'}}
+ ])
+ });
+
+ # Provide a reasonable default copy location. Typically "Stacks"
+ $client->respond({
+ acp_default_location => $e->search_asset_copy_location([
+ {deleted => 'f', owning_lib => $org_ids},
+ {order_by => {acpl => 'id'}, limit => 1}
+ ])->[0]
+ });
+
+ $client->respond({
+ acp_status => $e->search_config_copy_status([
+ {id => {'!=' => undef}},
+ {order_by => {ccs => 'name'}}
+ ])
+ });
+
+ $client->respond({
+ acp_age_protect => $e->search_config_rules_age_hold_protect([
+ {id => {'!=' => undef}},
+ {order_by => {crahp => 'name'}}
+ ])
+ });
+
+ $client->respond({
+ acp_floating_group => $e->search_config_floating_group([
+ {id => {'!=' => undef}},
+ {order_by => {cfg => 'name'}}
+ ])
+ });
+
+ $client->respond({
+ acp_circ_modifier => $e->search_config_circ_modifier([
+ {code => {'!=' => undef}},
+ {order_by => {ccm => 'name'}}
+ ])
+ });
+
+ $client->respond({
+ acp_item_type_map => $e->search_config_item_type_map([
+ {code => {'!=' => undef}},
+ {order_by => {ccm => 'value'}}
+ ])
+ });
+
+ $client->respond({
+ acn_class => $e->search_asset_call_number_class([
+ {id => {'!=' => undef}},
+ {order_by => {acnc => 'name'}}
+ ])
+ });
+
+ $client->respond({
+ acn_prefix => $e->search_asset_call_number_prefix([
+ {owning_lib => $org_ids},
+ {order_by => {acnp => 'label_sortkey'}}
+ ])
+ });
+
+ $client->respond({
+ acn_suffix => $e->search_asset_call_number_suffix([
+ {owning_lib => $org_ids},
+ {order_by => {acns => 'label_sortkey'}}
+ ])
+ });
+
+ # Some object types require more complex sorting, etc.
+
+ my $cats = $e->search_asset_stat_cat([
+ {owner => $org_ids},
+ { flesh => 2,
+ flesh_fields => {asc => ['owner', 'entries'], aou => ['ou_type']}
+ }
+ ]);
+
+ # Sort stat cats by depth then by name within each depth group.
+ $cats = [
+ sort {
+ my $d1 = $a->owner->ou_type->depth;
+ my $d2 = $b->owner->ou_type->depth;
+ return $a->name cmp $b->name if $d1 == $d2;
+
+ # Sort cats closer to the workstation org unit to the front.
+ return $d1 > $d2 ? -1 : 1;
+ }
+ @$cats
+ ];
+
+ for my $cat (@$cats) {
+ # de-flesh org data
+ $cat->owner($cat->owner->id);
+
+ # sort entries
+ $cat->entries([sort {$a->value cmp $b->value} @{$cat->entries}]);
+ }
+
+ $client->respond({acp_stat_cat => $cats});
+
+ return undef;
+}
+
+
1;
# vi:et:ts=4:sw=4
--- /dev/null
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+ 'eg.cat.volcopy.defaults', 'cat', 'object',
+ oils_i18n_gettext(
+ 'eg.cat.volcopy.defaults',
+ 'Holdings Editor Default Values and Visibility',
+ 'cwst', 'label'
+ )
+), (
+ 'cat.copy.templates', 'cat', 'object',
+ oils_i18n_gettext(
+ 'cat.copy.templates',
+ 'Holdings Editor Copy Templates',
+ 'cwst', 'label'
+ )
+);
+
+
}
).then(function(key) {
if (key) {
- var url = egCore.env.basePath + 'cat/volcopy/' + key;
+ //var url = egCore.env.basePath + 'cat/volcopy/' + key;
+ var url = '/eg2/staff/cat/volcopy/session/' + key;
$timeout(function() { $window.open(url, '_blank') });
} else {
alert('Could not create anonymous cache key!');