LPXXX Angular Volcopy
authorBill Erickson <berickxx@gmail.com>
Wed, 17 Jun 2020 20:36:02 +0000 (16:36 -0400)
committerBill Erickson <berickxx@gmail.com>
Thu, 25 Jun 2020 14:36:18 +0000 (10:36 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
19 files changed:
Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html
Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts
Open-ILS/src/eg2/src/app/share/item-location-select/item-location-select.component.html
Open-ILS/src/eg2/src/app/share/item-location-select/item-location-select.component.ts
Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts
Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.html
Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.ts
Open-ILS/src/eg2/src/app/staff/cat/volcopy/routing.module.ts
Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.css
Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.html
Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.ts
Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.component.html
Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.component.ts
Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.service.ts
Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.ts
Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.ts
Open-ILS/src/eg2/src/app/staff/share/holdings/copy-alerts-dialog.component.ts
Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.service.ts
Open-ILS/src/eg2/src/styles.css

index c456f80..88e636f 100644 (file)
@@ -7,6 +7,7 @@
 <div class="d-flex">
   <input type="text" 
     class="form-control"
+    [id]="domId"
     [ngClass]="{
       'text-success font-italic font-weight-bold': selected && selected.freetext,
       'form-control-sm': smallFormControl
index b1ba655..d4dd6f1 100644 (file)
@@ -35,6 +35,7 @@ export interface ComboboxEntry {
   }]
 })
 export class ComboboxComponent implements ControlValueAccessor, OnInit {
+    static domIdAuto = 0;
 
     selected: ComboboxEntry;
     click$: Subject<string>;
@@ -42,6 +43,8 @@ export class ComboboxComponent implements ControlValueAccessor, OnInit {
 
     @ViewChild('instance', { static: true }) instance: NgbTypeahead;
 
+    @Input() domId = 'eg-combobox-' + ComboboxComponent.domIdAuto++;
+
     // Applies a name attribute to the input.
     // Useful in forms.
     @Input() name: string;
index 99bd390..5f7e388 100644 (file)
@@ -7,6 +7,7 @@
 <eg-string #unsetString text="<Unset>" i18n-text></eg-string>
 
 <eg-combobox #comboBox
+  [domId]="domId"
   [startId]="startId"
   [displayTemplate]="displayTemplate"
   (onChange)="cboxChanged($event)"
index 2f41a10..d5a3baf 100644 (file)
@@ -30,6 +30,7 @@ import {StringComponent} from '@eg/share/string/string.component';
 })
 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.
@@ -45,6 +46,9 @@ export class ItemLocationSelectComponent
 
     @Input() required: boolean;
 
+    @Input() domId = 'eg-item-location-select-' +
+        ItemLocationSelectComponent.domIdAuto++;
+
     @ViewChild('comboBox', {static: false}) comboBox: ComboboxComponent;
     @ViewChild('unsetString', {static: false}) unsetString: StringComponent;
 
index 16f1cbd..431ac60 100644 (file)
@@ -24,6 +24,7 @@ interface OrgDisplay {
   templateUrl: './org-select.component.html'
 })
 export class OrgSelectComponent implements OnInit {
+    static domIdAuto = 0;
 
     selected: OrgDisplay;
     hidden: number[] = [];
@@ -40,7 +41,7 @@ export class OrgSelectComponent implements OnInit {
     @Input() stickySetting: string;
 
     // ID to display in the DOM for this selector
-    @Input() domId = '';
+    @Input() domId = 'eg-org-select-' + OrgSelectComponent.domIdAuto++;
 
     // Org unit field displayed in the selector
     @Input() displayField = 'shortname';
index 9b0bc01..3086135 100644 (file)
@@ -1,7 +1,34 @@
 
 
+<!-- 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>
+
+
 <!-- Copy Templates -->
-<div class="row">
+<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 domId="template-select" #copyTemplateCbox></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>
+    <button class="btn btn-outline-dark mr-2" (click)="importTemplate()" i18n>Import</button>
+    <button class="btn btn-outline-dark mr-2" (click)="exportTemplate()" i18n>Export</button>
+    <div class="flex-1"> </div>
+    <button class="btn btn-outline-dark mr-2" 
+      (click)="copyTemplateCbox.selectedId = null" i18n>Clear</button>
+    <button class="btn btn-outline-danger mr-2" (click)="deleteTemplate()" i18n>Delete Template</button>
+  </div>
 </div>
 
 
     <div>
       <ng-template #locationTemplate>
         <eg-item-location-select (valueChange)="values['location'] = $event"
-          [required]="true" permFilter="UPDATE_COPY">
+          domId='location-input' [required]="true" permFilter="UPDATE_COPY">
         </eg-item-location-select>
       </ng-template>
-
       <eg-batch-item-attr label="Location / Collection" i18n-label
+        editInputDomId="location-input"
         [editTemplate]="locationTemplate"
         [labelCounts]="itemAttrCounts('location')"
         (changesSaved)="applyCopyValue('location')">
     <div>
       <ng-template #circLibTemplate>
         <eg-org-select 
+          domId="circ-lib-input"
           (onChange)="values['circ_lib'] = $event ? $event.id() : null"
           [limitPerms]="['UPDATE_COPY']">
         </eg-org-select>
       </ng-template>
-
       <eg-batch-item-attr label="Circulating Library" i18n-label
+        editInputDomId="circ-lib-input"
         [editTemplate]="circLibTemplate"
         [labelCounts]="itemAttrCounts('circ_lib')"
         (changesSaved)="circLibChanged()">
     <div>
       <ng-template #owningLibTemplate>
         <eg-org-select 
+          domId="owning-lib-input"
           (onChange)="values['owning_lib'] = $event ? $event.id() : null"
           [limitPerms]="['UPDATE_COPY']">
         </eg-org-select>
       </ng-template>
-
       <eg-batch-item-attr label="Owning Library" i18n-label
+        editInputDomId="owning-lib-input"
         [editTemplate]="owningLibTemplate"
         [labelCounts]="itemAttrCounts('owning_lib')"
         (changesSaved)="owningLibChanged()">
     <div>
       <ng-template #copyNumberTemplate>
         <input type="number" class="form-control"
-          [(ngModel)]="values['copy_number']"/>
+          id="copy-number-input" [(ngModel)]="values['copy_number']"/>
       </ng-template>
-
       <eg-batch-item-attr label="Copy Number" i18n-label
+        editInputDomId="copy-number-input"
         [emptyIsUnset]="true"
         [editTemplate]="copyNumberTemplate"
         [labelCounts]="itemAttrCounts('copy_number')"
     <div class="p-1"><h4 class="font-weight-bold" i18n>Circulation</h4></div>
 
     <div>
-                       <ng-template #circulateTemplate>
-                               <select class="form-control" [(ngModel)]="values['circulate']">
-                                       <option value="yes" i18n>Yes</option>
-                                       <option value="no" i18n>No</option>
-                               </select>
-                       </ng-template>
+      <ng-template #circulateTemplate>
+        <ng-container *ngTemplateOutlet="yesNoSelect;context:{field:'circulate'}">
+        </ng-container>
+      </ng-template>
       <eg-batch-item-attr label="Circulate" i18n-label
         displayAs="bool"
+        editInputDomId="circulate-input"
         [editTemplate]="circulateTemplate"
         [labelCounts]="itemAttrCounts('circulate')"
-        (changesSaved)="applyCopyValue('circulate',
-          values['circulate'] === 'yes' ? 't': 'f')">
+        (changesSaved)="applyCopyValue('circulate')">
       </eg-batch-item-attr>
     </div>
 
     <div>
-                       <ng-template #holdableTemplate>
-                               <select class="form-control" [(ngModel)]="values['holdable']">
-                                       <option value="yes" i18n>Yes</option>
-                                       <option value="no" i18n>No</option>
-                               </select>
-                       </ng-template>
+      <ng-template #holdableTemplate>
+        <ng-container *ngTemplateOutlet="yesNoSelect;context:{field:'holdable'}">
+        </ng-container>
+      </ng-template>
       <eg-batch-item-attr label="Holdable" i18n-label
         displayAs="bool"
+        editInputDomId="holdable-input"
         [editTemplate]="holdableTemplate"
         [labelCounts]="itemAttrCounts('holdable')"
-        (changesSaved)="applyCopyValue('holdable',
-          values['holdable'] === 'yes' ? 't': 'f')">
+        (changesSaved)="applyCopyValue('holdable')">
       </eg-batch-item-attr>
     </div>
 
     <div>
-                       <ng-template #ageProtectTemplate>
-                               <select class="form-control" [(ngModel)]="values['age_protect']">
+      <ng-template #ageProtectTemplate>
+        <select class="form-control" 
+          id="age-protect-input" [(ngModel)]="values['age_protect']">
           <option [value]="null" i18n>&lt;Unset&gt;</option>
-                                       <option *ngFor="let rule of ageProtectRules" 
+          <option *ngFor="let rule of ageProtectRules" 
             value="{{rule.id()}}">{{rule.name()}}</option>
-                               </select>
-                       </ng-template>
+        </select>
+      </ng-template>
       <eg-batch-item-attr label="Aged-Based Hold Protection" i18n-label
+        editInputDomId="age-protect-input"
         [emptyIsUnset]="true"
         [editTemplate]="ageProtectTemplate"
         [labelCounts]="itemAttrCounts('age_protect')"
     </div>
 
     <div>
-                       <ng-template #floatingTemplate>
-                               <select class="form-control" [(ngModel)]="values['floating']">
+      <ng-template #floatingTemplate>
+        <select class="form-control" 
+          id="floating-input" [(ngModel)]="values['floating']">
           <option [value]="null" i18n>&lt;Unset&gt;</option>
-                                       <option *ngFor="let grp of floatingGroups" 
+          <option *ngFor="let grp of floatingGroups" 
             value="{{grp.id()}}">{{grp.name()}}</option>
-                               </select>
-                       </ng-template>
+        </select>
+      </ng-template>
       <eg-batch-item-attr label="Floating" i18n-label
+        editInputDomId="floating-input"
         [emptyIsUnset]="true"
         [editTemplate]="floatingTemplate"
         [labelCounts]="itemAttrCounts('floating')"
       <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" [(ngModel)]="values['loan_duration']">
+      <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>
+        </select>
+      </ng-template>
       <eg-batch-item-attr label="Loan Duration" i18n-label
+        editInputDomId="loan-duration-input"
         [emptyIsUnset]="true"
         [editTemplate]="loanDurationTemplate"
         [labelCounts]="itemAttrCounts('loan_duration')"
       <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" [(ngModel)]="values['fine_level']">
+      <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>
+        </select>
+      </ng-template>
       <eg-batch-item-attr label="Fine Level" i18n-label
+        editInputDomId="fine-level-input"
         [emptyIsUnset]="true"
         [editTemplate]="fineLevelTemplate"
         [labelCounts]="itemAttrCounts('fine_level')"
     </div>
 
     <div>
-                       <ng-template #ageProtectTemplate>
-                               <select class="form-control" [(ngModel)]="values['age_protect']">
+      <ng-template #ageProtectTemplate>
+        <select class="form-control" [(ngModel)]="values['age_protect']">
           <option [value]="null" i18n>&lt;Unset&gt;</option>
-                                       <option *ngFor="let rule of ageProtectRules" 
+          <option *ngFor="let rule of ageProtectRules" 
             value="{{rule.id()}}">{{rule.name()}}</option>
-                               </select>
-                       </ng-template>
+        </select>
+      </ng-template>
       <eg-batch-item-attr label="Aged-Based Hold Protection" i18n-label
         [emptyIsUnset]="true"
         [editTemplate]="ageProtectTemplate"
     </div>
 
     <div>
-                       <ng-template #circAsTypeTemplate>
-                               <select class="form-control" [(ngModel)]="values['circ_as_type']">
+      <ng-template #circAsTypeTemplate>
+        <select class="form-control" [(ngModel)]="values['circ_as_type']">
           <option [value]="null" i18n>&lt;Unset&gt;</option>
-                                       <option *ngFor="let map of itemTypeMaps"
+          <option *ngFor="let map of itemTypeMaps"
             value="{{map.code()}}">{{map.value()}}</option>
-                               </select>
-                       </ng-template>
+        </select>
+      </ng-template>
       <eg-batch-item-attr label="Circulate as Type" i18n-label
         [emptyIsUnset]="true"
         [editTemplate]="circAsTypeTemplate"
     </div>
 
     <div>
-                       <ng-template #circModifierTemplate>
-                               <select class="form-control" [(ngModel)]="values['circ_modifier']">
+      <ng-template #circModifierTemplate>
+        <select class="form-control" [(ngModel)]="values['circ_modifier']">
           <option [value]="null" i18n>&lt;Unset&gt;</option>
-                                       <option *ngFor="let mod of circModifiers"
+          <option *ngFor="let mod of circModifiers"
             value="{{mod.code()}}">{{mod.name()}}</option>
-                               </select>
-                       </ng-template>
+        </select>
+      </ng-template>
       <eg-batch-item-attr label="Circulion Modifier" i18n-label
         [emptyIsUnset]="true"
         [editTemplate]="circModifierTemplate"
   <!-- 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" 
+      <ng-template #alertMessageTemplate>
+        <textarea rows="3" class="form-control" id="alert-message-input"
           [(ngModel)]="values['alert_message']">
         </textarea>
-                       </ng-template>
+      </ng-template>
       <eg-batch-item-attr label="Alert Message" i18n-label
+        editInputDomId="alert-message-input"
         [emptyIsUnset]="true"
         [editTemplate]="alertMessageTemplate"
         [labelCounts]="itemAttrCounts('alert_message')"
         (changesSaved)="applyCopyValue('alert_message')">
       </eg-batch-item-attr>
     </div>
+    -->
+
+    <div class="border rounded m-1">
+      <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>
-                       <ng-template #depositTemplate>
-                               <select class="form-control" [(ngModel)]="values['deposit']">
-                                       <option value="yes" i18n>Yes</option>
-                                       <option value="no" i18n>No</option>
-                               </select>
-                       </ng-template>
+      <ng-template #depositTemplate>
+        <ng-container *ngTemplateOutlet="yesNoSelect;context:{field:'deposit'}">
+        </ng-container>
+      </ng-template>
       <eg-batch-item-attr label="Deposit" i18n-label
         displayAs="bool"
+        editInputDomId="deposit-input"
         [editTemplate]="depositTemplate"
         [labelCounts]="itemAttrCounts('deposit')"
-        (changesSaved)="applyCopyValue('deposit',
-          values['deposit'] === 'yes' ? 't': 'f')">
+        (changesSaved)="applyCopyValue('deposit')">
       </eg-batch-item-attr>
     </div>
 
     <div>
-                       <ng-template #depositAmountTemplate>
-                               <input type="number" class="form-control" 
-          [(ngModel)]="values['deposit_amount']"/>
-                       </ng-template>
+      <ng-template #depositAmountTemplate>
+        <input type="number" class="form-control" 
+          id="deposit-amount-input" [(ngModel)]="values['deposit_amount']"/>
+      </ng-template>
       <eg-batch-item-attr label="Deposit Amount" i18n-label
         displayAs="currency"
+        editInputDomId="deposit-amount-input"
         [editTemplate]="depositAmountTemplate"
         [labelCounts]="itemAttrCounts('deposit_amount')"
         (changesSaved)="applyCopyValue('deposit_amount')">
     </div>
 
     <div>
-                       <ng-template #priceTemplate>
-                               <input type="number" class="form-control" 
-          [(ngModel)]="values['price']"/>
-                       </ng-template>
+      <ng-template #priceTemplate>
+        <input type="number" class="form-control" 
+          id="price-input" [(ngModel)]="values['price']"/>
+      </ng-template>
       <eg-batch-item-attr label="Price" i18n-label
         displayAs="currency"
+        editInputDomId="price-input"
         [editTemplate]="priceTemplate"
         [labelCounts]="itemAttrCounts('price')"
         (changesSaved)="applyCopyValue('price')">
     </div>
 
     <div>
-                       <ng-template #opacVisibleTemplate>
-                               <select class="form-control" [(ngModel)]="values['opac_visible']">
-                                       <option value="yes" i18n>Yes</option>
-                                       <option value="no" i18n>No</option>
-                               </select>
-                       </ng-template>
+      <ng-template #opacVisibleTemplate>
+        <ng-container *ngTemplateOutlet="yesNoSelect;context:{field:'opac_visible'}">
+        </ng-container>
+      </ng-template>
       <eg-batch-item-attr label="OPAC Visible" i18n-label
         displayAs="bool"
+        editInputDomId="opac_visible-input"
         [editTemplate]="opacVisibleTemplate"
         [labelCounts]="itemAttrCounts('opac_visible')"
-        (changesSaved)="applyCopyValue('opac_visible',
-          values['opac_visible'] === 'yes' ? 't': 'f')">
+        (changesSaved)="applyCopyValue('opac_visible')">
       </eg-batch-item-attr>
     </div>
 
     <div>
-                       <ng-template #refTemplate>
-                               <select class="form-control" [(ngModel)]="values['ref']">
-                                       <option value="yes" i18n>Yes</option>
-                                       <option value="no" i18n>No</option>
-                               </select>
-                       </ng-template>
+      <ng-template #refTemplate>
+        <ng-container *ngTemplateOutlet="yesNoSelect;context:{field:'ref'}">
+        </ng-container>
+      </ng-template>
       <eg-batch-item-attr label="Reference" i18n-label
         displayAs="bool"
+        editInputDomId="ref-input"
         [editTemplate]="refTemplate"
         [labelCounts]="itemAttrCounts('ref')"
-        (changesSaved)="applyCopyValue(
-          'ref', values['ref'] === 'yes' ? 't': 'f')">
+        (changesSaved)="applyCopyValue('ref')">
       </eg-batch-item-attr>
     </div>
 
     <div>
-                       <ng-template #costTemplate>
-                               <input type="number" class="form-control" 
-          [(ngModel)]="values['cost']"/>
-                       </ng-template>
+      <ng-template #costTemplate>
+        <input type="number" class="form-control" 
+          id="cost-input" [(ngModel)]="values['cost']"/>
+      </ng-template>
       <eg-batch-item-attr label="Acquisition Cost" i18n-label
-        [emptyIsUnset]="true"
         displayAs="currency"
+        editInputDomId="cost-input"
+        [emptyIsUnset]="true"
         [editTemplate]="costTemplate"
         [labelCounts]="itemAttrCounts('cost')"
         (changesSaved)="applyCopyValue('cost')">
       <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" [(ngModel)]="values['mint_condition']">
+      <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>
+        </select>
+      </ng-template>
       <eg-batch-item-attr label="Quality" i18n-label
+        editInputDomId="mint-condition-input"
         [emptyIsUnset]="true"
         [editTemplate]="mintConditionTemplate"
         [labelCounts]="itemAttrCounts('mint_condition')"
   <!-- COLUMN 5 -->
   <div class="flex-1 p-1">
     <div class="p-1"><h4 class="font-weight-bold" i18n>Statistics</h4></div>
+
+    <div *ngFor="let cat of statCats(); let idx = index">
+      <ng-template #statCatTemplate>
+        <eg-combobox domId="stat-cat-input-{{idx}}"
+          (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
+        editInputDomId="stat-cat-input-{{idx}}"
+        [emptyIsUnset]="true"
+        [editTemplate]="statCatTemplate"
+        [labelCounts]="statCatCounts(cat.id())"
+        (changesSaved)="statCatChanged(cat.id())">
+      </eg-batch-item-attr>
+    </div>
+
   </div>
 </div>
 
index ccfe284..723638c 100644 (file)
@@ -4,6 +4,7 @@ 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';
@@ -12,10 +13,19 @@ 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 {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
 
 @Component({
   selector: 'eg-copy-attrs',
-  templateUrl: 'copy-attrs.component.html'
+  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 {
 
@@ -25,10 +35,8 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
     // Some values are scalar, some IdlObjects depending on copy fleshyness.
     values: {[field: string]: any} = {};
 
-    ageProtectRules: IdlObject[] = [];
-    floatingGroups: IdlObject[] = [];
-    itemTypeMaps: IdlObject[] = [];
-    circModifiers: IdlObject[] = [];
+    // Map of stat ID to entry ID.
+    statCatValues: {[statId: number]: number} = {};
 
     loanDurationLabelMap: {[level: number]: string} = {};
     fineLevelLabelMap: {[level: number]: string} = {};
@@ -52,6 +60,12 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
     @ViewChild('mintConditionNo', {static: false})
         mintConditionNo: StringComponent;
 
+    @ViewChild('copyAlertsDialog', {static: false})
+        private copyAlertsDialog: CopyAlertsDialogComponent;
+
+    @ViewChild('copyTemplateCbox', {static: false})
+        copyTemplateCbox: ComboboxComponent;
+
     constructor(
         private router: Router,
         private route: ActivatedRoute,
@@ -64,15 +78,18 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
         private pcrud: PcrudService,
         private holdings: HoldingsService,
         private volcopy: VolCopyService,
-        private format: FormatService
+        private format: FormatService,
+        private store: StoreService
     ) { }
 
     ngOnInit() {
-        this.load();
     }
 
     ngAfterViewInit() {
 
+        const tmpl = this.store.getLocalItem('cat.copy.last_template');
+        if (tmpl) { this.copyTemplateCbox.selectedId = tmpl; }
+
         this.loanDurationLabelMap[1] = this.loanDurationShort.text;
         this.loanDurationLabelMap[2] = this.loanDurationNormal.text;
         this.loanDurationLabelMap[3] = this.loanDurationLong.text;
@@ -82,45 +99,40 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
         this.fineLevelLabelMap[3] = this.fineLevelHigh.text;
     }
 
-    load() {
-
-        this.pcrud.retrieveAll('crahp')
-        .pipe(tap(rule => this.ageProtectRules.push(rule))).toPromise()
-        .then(_ => {
-
-            this.ageProtectRules = this.ageProtectRules.sort(
-                (a, b) => a.name() < b.name() ? -1 : 1);
-
-        }).then(_ => {
-
-            return this.pcrud.retrieveAll('cfg')
-            .pipe(tap(rule => this.floatingGroups.push(rule))).toPromise()
-
-        }).then(_ => {
-
-            this.floatingGroups = this.floatingGroups.sort(
-                (a, b) => a.name() < b.name() ? -1 : 1);
-
-        }).then(_ => {
-
-            return this.pcrud.retrieveAll('ccm')
-            .pipe(tap(rule => this.circModifiers.push(rule))).toPromise()
-
-        }).then(_ => {
+    statCats(): IdlObject[] {
+        return this.volcopy.statCats;
+    }
 
-            this.circModifiers = this.circModifiers.sort(
-                (a, b) => a.name() < b.name() ? -1 : 1);
 
-        }).then(_ => {
+    orgSn(orgId: number): string {
+        return orgId ? this.org.get(orgId).shortname() : '';
+    }
 
-            return this.pcrud.retrieveAll('citm')
-            .pipe(tap(itemType => this.itemTypeMaps.push(itemType))).toPromise()
+    statCatCounts(catId: number): {[value: string]: number} {
+        catId = Number(catId);
+        const counts = {};
 
-        }).then(_ => {
+        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;
+                }
+            }
 
-            this.itemTypeMaps = this.itemTypeMaps.sort(
-                (a, b) => a.value() < b.value() ? -1 : 1);
+            if (counts[value] === undefined) {
+                counts[value] = 0;
+            }
+            counts[value]++;
         });
+
+        return counts;
     }
 
     itemAttrCounts(field: string): {[value: string]: number} {
@@ -131,7 +143,7 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
 
             if (counts[value] === undefined) {
                 counts[value] = 0;
-            };
+            }
             counts[value]++;
         });
 
@@ -147,11 +159,11 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
                 ' : ' + copy.call_number().label();
         }
 
-        let value = copy[field]();
+        const value = copy[field]();
 
         if (!value && value !== 0) { return ''; }
 
-        switch(field) {
+        switch (field) {
 
             case 'status':
                 return this.volcopy.copyStatuses[value].name();
@@ -174,12 +186,12 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
                 return this.org.get(value).shortname();
 
             case 'age_protect':
-                const rule = this.ageProtectRules.filter(
+                const rule = this.volcopy.ageProtectRules.filter(
                     r => r.id() === Number(value))[0];
                 return rule ? rule.name() : '';
 
             case 'floating':
-                const grp = this.floatingGroups.filter(
+                const grp = this.volcopy.floatingGroups.filter(
                     g => g.id() === Number(value))[0];
                 return grp ? grp.name() : '';
 
@@ -190,12 +202,12 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
                 return this.fineLevelLabelMap[value];
 
             case 'circ_as_type':
-                const map = this.itemTypeMaps.filter(
+                const map = this.volcopy.itemTypeMaps.filter(
                     m => m.code() === value)[0];
                 return map ? map.value() : '';
 
             case 'circ_modifier':
-                const mod = this.circModifiers.filter(
+                const mod = this.volcopy.circModifiers.filter(
                     m => m.code() === value)[0];
                 return mod ? mod.name() : '';
 
@@ -228,8 +240,77 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
 
     owningLibChanged() {
         // TODO
+        // copies.ischanged(true);
         console.log('OWNING LIB ', this.values['owning_lib']);
     }
+
+
+    // Create or modify a stat cat entry for each copy that does not
+    // already match the new value.
+    statCatChanged(catId: number) {
+        catId = Number(catId);
+
+        const entryId = this.statCatValues[catId];
+        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.mode = 'create';
+        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);
+                    });
+                }
+            }
+        );
+    }
+
+    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; }
+
+            this.applyCopyValue(field, value);
+        });
+    }
 }
 
 
index fb4f44d..c5d3f67 100644 (file)
@@ -3,16 +3,7 @@ import {RouterModule, Routes} from '@angular/router';
 import {VolCopyComponent} from './volcopy.component';
 
 const routes: Routes = [{
-    path: 'edit/item/:copy_id',
-    component: VolCopyComponent
-  }, {
-    path: 'edit/callnumber/:vol_id',
-    component: VolCopyComponent
-  }, {
-    path: 'edit/record/:record_id',
-    component: VolCopyComponent
-  }, {
-    path: 'edit/session/:session',
+    path: ':tab/:target/:target_id',
     component: VolCopyComponent
   /*
   }, {
index fabadae..de37f46 100644 (file)
@@ -6,11 +6,10 @@ input[type="number"] {
 
 .vol-row {
   background-color: rgba(0,0,0,.03);
-  border-top: 1px solid rgba(0,0,0,.125);
-  border-bottom: 1px solid rgba(0,0,0,.125);
+  border-top: 1px solid #d9edf7;
+  border-bottom: 1px solid #d9edf7;
 }
 
-
 .clear-button {
   border: none;
   background-color: rgba(0, 0, 0, 0.0);
index a29c3a7..9edeb21 100644 (file)
@@ -12,7 +12,7 @@
   dialogBody="Delete {{deleteCopyCount}} Item(s)?">
 </eg-confirm-dialog>
 
-<div class="row d-flex vol-row border border-info mb-2">
+<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)}">
     </ng-container>
   </ng-container>
 </ng-container>
+
+<hr/>
+
+<div class="form-inline">
+  <eg-org-select #newVolOrg [applyDefault]="true">
+  </eg-org-select>
+  <button class="btn btn-outline-dark ml-2" 
+    (click)="addVol(newVolOrg.selectedOrg())" i18n>
+    Add Call Number
+  </button>
+</div>
+  
index a7975ab..4dc6574 100644 (file)
@@ -105,6 +105,8 @@ export class VolEditComponent implements OnInit {
             );
         });
 
+        if (Object.keys(this.bibParts).length === 0) { return; }
+
         this.pcrud.search('bmp',
             {record: Object.keys(this.bibParts), deleted: 'f'})
         .subscribe(
@@ -162,6 +164,13 @@ export class VolEditComponent implements OnInit {
         }
     }
 
+
+    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;
     }
@@ -372,7 +381,7 @@ export class VolEditComponent implements OnInit {
     }
 
     barcodeChanged(copy: IdlObject, barcode: string) {
-        copy.barcode(barcode);
+        // note: copy.barcode(barcode) applied via ngModel
         copy.ischanged(true);
         copy._dupe_barcode = false;
 
@@ -405,7 +414,6 @@ export class VolEditComponent implements OnInit {
     }
 
     deleteOneCopy(copyNode: HoldingsTreeNode) {
-
         const targetCopy = copyNode.target;
 
         const orgNodes = this.context.orgNodes();
@@ -455,6 +463,7 @@ export class VolEditComponent implements OnInit {
     }
 
     deleteOneVol(volNode: HoldingsTreeNode) {
+
         let deleteVolIdx = null;
         const targetVol = volNode.target;
 
index 156540f..ff1473f 100644 (file)
@@ -1,11 +1,5 @@
 <eg-staff-banner bannerText="Holdings Editor" i18n-bannerText></eg-staff-banner>
 
-<div class="row" [hidden]="!loading">
-  <div class="col-lg-6 offset-lg-3">
-    <eg-progress-inline #loadingProgress></eg-progress-inline>
-  </div>
-</div>
-
 <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
 <ng-container *ngIf="!loading && !sessionExpired">
 
   <eg-bib-summary *ngIf="context.recordId" [recordId]="context.recordId"></eg-bib-summary>
-  
-  <div class="mt-3" *ngIf="!context.hideVols">
-    <eg-vol-edit [context]="context"></eg-vol-edit>
-  </div>
-
-  <ng-container *ngIf="!context.hideVols && !context.hideCopies">
-    <hr class="m-2"/>
-  </ng-container>
-
-  <div class="mt-3" *ngIf="!context.hideCopies">
-    <eg-copy-attrs [context]="context"></eg-copy-attrs>
-  </div>
 
-  <div class="row m-2 p-2 border border-info">
-    <div class="col-lg-12 d-flex">
-      <div class="flex-1"> </div>
-      <button class="btn btn-outline-dark" 
-        [disabled]="!context.isSaveable()" (click)="save()" i18n>Save</button>
-      <button class="btn btn-outline-dark ml-2" 
-        [disabled]="!context.isSaveable()"
-        (click)="save(true)" i18n>Save &amp; Exit</button>
+  <div class="m-2"> </div>
+
+  <ngb-tabset [activeId]="tab" (tabChange)="beforeTabChange($event)">
+
+    <ngb-tab title="Holdings" i18n-title id="holdings">
+      <ng-template ngbTabContent>
+        <div class="mt-2">
+          <div class="row" [hidden]="!loading">
+            <div class="col-lg-6 offset-lg-3">
+              <eg-progress-inline #loadingProgress></eg-progress-inline>
+            </div>
+          </div>
+          <eg-vol-edit [context]="context"></eg-vol-edit>
+        </div>
+      </ng-template>
+    </ngb-tab>
+
+    <ngb-tab title="Item Attributes" i18n-title id="attrs">
+      <ng-template ngbTabContent>
+        <div class="mt-2">
+          <div class="row" [hidden]="!loading">
+            <div class="col-lg-6 offset-lg-3">
+              <eg-progress-inline #loadingProgress></eg-progress-inline>
+            </div>
+          </div>
+        </div>
+        <eg-copy-attrs [context]="context"></eg-copy-attrs>
+      </ng-template>
+    </ngb-tab>
+
+  </ngb-tabset>
+
+  <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="flex-1"> </div>
+        <button class="btn btn-outline-dark" (click)="save()" i18n>Save</button>
+        <button class="btn btn-outline-dark ml-2" 
+          (click)="save(true)" i18n>Save &amp; Exit</button>
+      </div>
     </div>
-  </div>
+  </ng-container>
 
 </ng-container>
 
index 5932ffb..b737779 100644 (file)
@@ -12,11 +12,13 @@ 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 {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
 
 const COPY_FLESH = {
     flesh: 1,
     flesh_fields: {
-        acp: ['call_number', 'location', 'parts', 'creator', 'editor']
+        acp: ['call_number', 'location', 'parts',
+                'creator', 'editor', 'stat_cat_entries']
     }
 };
 
@@ -47,6 +49,10 @@ export class VolCopyComponent implements OnInit {
     loading = true;
     sessionExpired = false;
 
+    tab = 'holdings'; // holdings | attrs | config
+    target: string;   // item | callnumber | record | session
+    targetId: string; // id value or session string
+
     @ViewChild('loadingProgress', {static: false})
     loadingProgress: ProgressInlineComponent;
 
@@ -66,35 +72,54 @@ export class VolCopyComponent implements OnInit {
     ) { }
 
     ngOnInit() {
-        this.context = new VolCopyContext();
-        this.context.org = this.org; // inject;
-
         this.route.paramMap.subscribe(
             (params: ParamMap) => this.negotiateRoute(params));
     }
 
     negotiateRoute(params: ParamMap) {
-        this.context.recordId = +params.get('record_id') || null;
-        this.context.volId    = +params.get('vol_id')    || null;
-        this.context.copyId   = +params.get('copy_id')   || null;
-        this.context.session  =  params.get('session')   || null;
-        this.load();
+        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) {
+            // Avoid refetching the data during route changes.
+            this.volcopy.currentContext = this.context;
+            this.load();
+        }
     }
 
     load(copyIds?: number[]) {
-
         this.sessionExpired = false;
         this.loading = true;
         this.context.reset();
 
-        this.volcopy.fetchDefaults()
-        .then(_ => this.volcopy.fetchCopyStats())
+        this.volcopy.load()
         .then(_ => this.fetchHoldings(copyIds))
         .then(_ => this.volcopy.applyVolLabels(
             this.context.volNodes().map(n => n.target)))
-        .then(_ => this.holdings.fetchCallNumberClasses())
-        .then(_ => this.holdings.fetchCallNumberPrefixes())
-        .then(_ => this.holdings.fetchCallNumberSuffixes())
         .then(_ => this.context.sortHoldings())
         .then(_ => this.context.setRecordId())
         .then(_ => this.loading = false);
@@ -110,20 +135,36 @@ export class VolCopyComponent implements OnInit {
             this.context.sessionType = 'mixed';
             return this.fetchSession(this.context.session);
 
-        } else if (this.context.recordId) {
-            this.context.sessionType = 'record';
-            return this.fetchRecords(this.context.recordId);
+        } 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.copyId) {
-            this.context.sessionType = 'copy';
-            return this.fetchCopies(this.context.copyId);
+        } 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: NgbTabChangeEvent) {
+        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')
@@ -317,6 +358,17 @@ export class VolCopyComponent implements OnInit {
             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());
+                    }
+                });
+            });
+        });
+
         if (volumes.length > 0) {
             this.saveApi(volumes);
         } else {
index abaa8b8..07e009e 100644 (file)
@@ -10,6 +10,8 @@ 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 */
 
@@ -21,6 +23,19 @@ export class VolCopyService {
     defaultValues: any = null;
     copyStatuses: {[id: number]: IdlObject} = null;
 
+    // Track this here so it can survive route changes.
+    currentContext: VolCopyContext;
+
+    ageProtectRules: IdlObject[] = [];
+    floatingGroups: IdlObject[] = [];
+    itemTypeMaps: IdlObject[] = [];
+    circModifiers: IdlObject[] = [];
+    statCats: IdlObject[] = [];
+    statCatEntryMap: {[id: number]: IdlObject} = {}; // entry id => entry
+
+    templateNames: ComboboxEntry[] = [];
+    templates: any = {};
+
     constructor(
         private evt: EventService,
         private net: NetService,
@@ -29,13 +44,112 @@ export class VolCopyService {
         private auth: AuthService,
         private pcrud: PcrudService,
         private holdings: HoldingsService,
-        private store: ServerStoreService
+        private store: StoreService,
+        private serverStore: ServerStoreService
     ) {}
 
+
+    // Fetch the data that is always needed.
+    load(): Promise<any> {
+
+        if (this.itemTypeMaps.length > 0) {
+            return Promise.resolve();
+        }
+
+        return this.fetchDefaults()
+        .then(_ => this.holdings.fetchCallNumberClasses())
+        .then(_ => this.holdings.fetchCallNumberPrefixes())
+        .then(_ => this.holdings.fetchCallNumberSuffixes())
+        .then(_ => this.fetchCopyStats())
+        .then(_ => this.fetchTemplates())
+        .then(_ => {
+
+            return this.pcrud.retrieveAll('crahp')
+            .pipe(tap(rule => this.ageProtectRules.push(rule))).toPromise()
+
+        }).then(_ => {
+
+            this.ageProtectRules = this.ageProtectRules.sort(
+                (a, b) => a.name() < b.name() ? -1 : 1);
+
+        }).then(_ => {
+
+            return this.pcrud.retrieveAll('cfg')
+            .pipe(tap(rule => this.floatingGroups.push(rule))).toPromise();
+
+        }).then(_ => {
+
+            this.floatingGroups = this.floatingGroups.sort(
+                (a, b) => a.name() < b.name() ? -1 : 1);
+
+        }).then(_ => {
+
+            return this.pcrud.retrieveAll('ccm')
+            .pipe(tap(rule => this.circModifiers.push(rule))).toPromise();
+
+        }).then(_ => {
+
+            this.circModifiers = this.circModifiers.sort(
+                (a, b) => a.name() < b.name() ? -1 : 1);
+
+        }).then(_ => {
+
+            return this.pcrud.retrieveAll('citm')
+            .pipe(tap(itemType => this.itemTypeMaps.push(itemType))).toPromise();
+
+        }).then(_ => {
+
+            this.itemTypeMaps = this.itemTypeMaps.sort(
+                (a, b) => a.value() < b.value() ? -1 : 1);
+
+        }).then(_ => {
+
+            return this.net.request('open-ils.circ',
+                'open-ils.circ.stat_cat.asset.retrieve.all',
+                this.auth.token(), this.auth.user().ws_ou()
+            ).toPromise().then(stats => this.statCats = stats);
+
+        }).then(_ => {
+
+            // Sort most local to the front of the list.
+            this.statCats = this.statCats.sort((s1, s2) => {
+                const d1 = this.org.get(s1.owner()).ou_type().depth();
+                const d2 = this.org.get(s2.owner()).ou_type().depth();
+
+                if (d1 > d2) {
+                    return -1;
+                } else if (d1 < d2) {
+                    return 1;
+                } else {
+                    return s1.name() < s2.name() ? -1 : 1;
+                }
+            });
+
+            this.statCats.forEach(cat => {
+                cat.entries().forEach(
+                    entry => this.statCatEntryMap[entry.id()] = entry);
+            });
+        });
+    }
+
+    fetchTemplates(): Promise<any> {
+
+        // TODO: copy templates should be server settings
+        const tmpls = this.store.getLocalItem('cat.copy.templates');
+        if (!tmpls) { return Promise.resolve(); }
+
+        this.templates = tmpls;
+        this.templateNames = Object.keys(tmpls)
+        .sort((n1, n2) => n1 < n2 ? -1 : 1)
+        .map(name => ({id: name, label: name}));
+
+        return Promise.resolve();
+    }
+
     fetchDefaults(): Promise<any> {
         if (this.defaultValues) { return Promise.resolve(); }
 
-        return this.store.getItem('cat.copy.defaults').then(
+        return this.serverStore.getItem('cat.copy.defaults').then(
             defaults => {
                 this.defaultValues = defaults || {};
             }
@@ -111,6 +225,7 @@ export class VolCopyService {
         copy.ref('f');
         copy.mint_condition('t');
         copy.parts([]);
+        copy.stat_cat_entries([]);
 
         return copy;
     }
@@ -254,5 +369,8 @@ export class VolCopyService {
 
         return promise;
     }
+
+
+
 }
 
index 5d745fb..d751c7e 100644 (file)
@@ -170,11 +170,24 @@ export class VolCopyContext {
             o1.target.shortname() < o2.target.shortname() ? -1 : 1);
     }
 
+    // Changes pending and no unresolved issues.
     isSaveable(): boolean {
         const dupeBc = this.copyList().filter(c => c._dupe_barcode).length;
 
         if (dupeBc) { return false; }
 
-        return true;
+        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;
     }
 }
index 52e89be..687066f 100644 (file)
@@ -98,7 +98,8 @@ export class RecordActionsComponent implements OnInit {
     // TODO: Support adding like call numbers by getting selected
     // call numbers from the holdings grid.
     addHoldings() {
-        this.holdings.spawnAddHoldingsUi(this.recId);
+        // -1 == create new call number
+        this.holdings.spawnAddHoldingsUi(this.recId, null, [{callnumber: -1}]);
     }
 
 }
index 439c546..06951b8 100644 (file)
@@ -41,6 +41,11 @@ export class CopyAlertsDialogComponent
         return this._mode;
     }
 
+    // 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.
@@ -78,7 +83,7 @@ export class CopyAlertsDialogComponent
         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');
         }
 
@@ -117,6 +122,9 @@ export class CopyAlertsDialogComponent
     }
 
     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;
@@ -149,6 +157,11 @@ export class CopyAlertsDialogComponent
     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);
index ccfd79b..f907b3b 100644 (file)
@@ -62,7 +62,8 @@ export class HoldingsService {
                 return;
             }
             setTimeout(() => {
-                const url = `/eg2/staff/cat/volcopy/edit/session/${key}`;
+                const tab = hideVols ? 'attrs' : 'holdings';
+                const url = `/eg2/staff/cat/volcopy/${tab}/session/${key}`;
                 window.open(url, '_blank');
             });
         });
index d1144fd..7780680 100644 (file)
@@ -225,6 +225,18 @@ body>.dropdown-menu {z-index: 2100;}
   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; } }