<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)="openCopyAlerts()" i18n>
+ <button class="btn btn-outline-dark" (click)="openCopyTags()" i18n>
Item Tags
</button>
</div>
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} from '@eg/staff/share/holdings/batch-item-attr.component';
import {FileExportService} from '@eg/share/util/file-export.service';
@ViewChild('copyAlertsDialog', {static: false})
private copyAlertsDialog: CopyAlertsDialogComponent;
+ @ViewChild('copyTagsDialog', {static: false})
+ private copyTagsDialog: CopyTagsDialogComponent;
+
@ViewChild('copyTemplateCbox', {static: false})
copyTemplateCbox: ComboboxComponent;
);
}
+ openCopyTags() {
+ this.copyTagsDialog.mode = 'create';
+ this.copyTagsDialog.inPlaceMode = true;
+
+ this.copyTagsDialog.open({size: 'lg'}).subscribe(newTags => {
+ if (!newTags || newTags.length === 0) { return; }
+
+ newTags.forEach(tag => {
+ this.context.copyList().forEach(copy => {
+ console.log('ADDING TAG ', tag);
+ /*
+ const a = this.idl.clone(newTag);
+ 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; }
<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[], mode: string) {
+ 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; }
--- /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 {{copies.length}} item(s).</span>
+ </ng-container>
+ <ng-container *ngIf="mode == 'manage'">
+ <span i18n>Managing tags for item {{copies[0].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">
+ <div class="row mt-2 p-2" *ngFor="let tag of newTags">
+ <div class="col-lg-4">
+ {{tag.tag_type()}}
+ </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>
+ </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>
+ <ng-container *ngIf="mode == 'manage'">
+ <!-- in manage mode list all of the tags linked to the copy -->
+ <!--
+ <div class="row mt-2"
+ *ngFor="let tag of copy.copy_tags()">
+ <div class="col-lg-12 pb-2"><hr/></div>
+ <div class="col-lg-4">
+ <eg-combobox [entries]="tagTypes" [startId]="tag.tag_type()"
+ i18n-placeholder placeholder="Tag Type..."
+ [required]="true"
+ (onChange)="tag.tag_type($event ? $event.id : null); tag.ischanged(true)">
+ </eg-combobox>
+ <div class="pl-2 pt-2" i18n>
+ Added: {{tag.create_time() | date:'shortDate'}}
+ </div>
+ </div>
+ <div class="col-lg-5">
+ <textarea class="form-control" rows="2"
+ i18n-placeholder placeholder="Tag Note..."
+ (ngModelChange)="tag.note($event); tag.ischanged(true)"
+ [ngModel]="tag.note()">
+ </textarea>
+ </div>
+ <div class="col-lg-3">
+ <div class="d-flex flex-column">
+ <div class="form-check">
+ <input class="form-check-input" type="checkbox"
+ [ngModel]="tag.temp() == 't'"
+ (ngModelChange)="tag.temp($event ? 't' : 'f'); tag.ischanged(true)"
+ id="tag-temporary-{{tag.id()}}">
+ <label class="form-check-label" for="tag-temporary-{{tag.id()}}" i18n>
+ Temporary?
+ </label>
+ </div>
+ <div class="form-check pt-2">
+ <input class="form-check-input" type="checkbox"
+ [ngModel]="tag.ack_time() != null"
+ (ngModelChange)="tag.ack_time($event ? 'now' : null); tag.ischanged(true)"
+ id="tag-temporary-{{tag.id()}}">
+ <label class="form-check-label" for="tag-temporary-{{tag.id()}}" i18n>
+ Clear?
+ </label>
+ </div>
+ </div>
+ </div>
+ </div>
+ -->
+ </ng-container>
+ </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 {
+
+ static autoId = -1;
+
+ @Input() copyIds: number[] = [];
+
+ // If there are multiple copyIds, only new tags may be applied.
+ // If there is only one copyId, then tags may be applied or removed.
+ 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[] = [];
+ tagMap: {[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 = [];
+
+ 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.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.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: 1, flesh_fields: {acp: ['tags']}}, {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());
+
+ // TODO: delete existing maps where needed.
+ }
+
+ 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.id(CopyTagsDialogComponent.autoId--);
+ 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;
+ }
+
+ applyChanges() {
+
+ if (this.inPlaceMode) {
+ this.close(this.newTags);
+ return;
+ }
+
+ // Create the tags then map them to our copies
+
+ let promise = this.createNewTags();
+
+ this.newTags.forEach(tag => {
+ this.copies.forEach(copy => {
+
+ if (copy.tags() && copy.tags().filter(
+ t => t.tag_type() === tag.id()).length > 0) {
+ return; // map already exists
+ }
+
+ promise = promise.then(_ => {
+ const map = this.idl.create('acptcm');
+ map.isnew(true);
+ map.copy(copy.id());
+ map.tag(tag.id());
+ return this.pcrud.create(map).toPromise();
+ });
+ })
+ });
+
+ promise.then(_ => {
+ this.successMsg.current().then(msg => this.toast.success(msg));
+ this.close(this.newTags.length > 0);
+ });
+ }
+
+ /*
+ applyChanges() {
+ const tags = this.copy.copy_tags().filter(a => a.ischanged());
+ if (tags.length === 0) { return; }
+ this.pcrud.update(tags).toPromise().then(
+ ok => this.successMsg.current().then(msg => this.toast.success(msg)),
+ err => this.errorMsg.current().then(msg => this.toast.danger(msg))
+ );
+ }
+ */
+}
+
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';
MarkDamagedDialogComponent,
MarkMissingDialogComponent,
CopyAlertsDialogComponent,
+ CopyTagsDialogComponent,
ReplaceBarcodeDialogComponent,
DeleteHoldingDialogComponent,
ConjoinedItemsDialogComponent,
MarkDamagedDialogComponent,
MarkMissingDialogComponent,
CopyAlertsDialogComponent,
+ CopyTagsDialogComponent,
ReplaceBarcodeDialogComponent,
DeleteHoldingDialogComponent,
ConjoinedItemsDialogComponent,