*/
// consider moving these to core...
-import {FormatService} from '@eg/core/format.service';
+import {FormatService, FormatValuePipe} from '@eg/core/format.service';
import {PrintService} from '@eg/share/print/print.service';
// Globally available components
ConfirmDialogComponent,
PromptDialogComponent,
ProgressInlineComponent,
- ProgressDialogComponent
+ ProgressDialogComponent,
+ FormatValuePipe
],
imports: [
CommonModule,
ConfirmDialogComponent,
PromptDialogComponent,
ProgressInlineComponent,
- ProgressDialogComponent
+ ProgressDialogComponent,
+ FormatValuePipe
]
})
-import {Injectable} from '@angular/core';
+import {Injectable, Pipe, PipeTransform} from '@angular/core';
import {DatePipe, CurrencyPipe} from '@angular/common';
import {IdlService, IdlObject} from '@eg/core/idl.service';
import {OrgService} from '@eg/core/org.service';
}
}
+
+// Pipe-ify the above formating logic for use in templates
+@Pipe({name: 'formatValue'})
+export class FormatValuePipe implements PipeTransform {
+ constructor(private formatter: FormatService) {}
+ // Add other filter params as needed to fill in the FormatParams
+ transform(value: string, datatype: string): string {
+ return this.formatter.transform({value: value, datatype: datatype});
+ }
+}
+
<div class="input-group">
- <input
- class="form-control"
+ <input
+ class="form-control"
ngbDatepicker
#datePicker="ngbDatepicker"
[attr.id]="domId.length ? domId : null"
[disabled]="_disabled"
[required]="required"
[(ngModel)]="current"
- (dateSelect)="onDateSelect($event)">
+ (dateSelect)="onDateSelect($event)"/>
<div class="input-group-append">
<button class="btn btn-outline-secondary" [disabled]="_disabled"
(click)="datePicker.toggle()" type="button">
- <span title="Select Date" i18n-title
+ <span title="Select Date" i18n-title
class="material-icons mat-icon-in-button">calendar_today</span>
</button>
</div>
this.onOpen$ = new EventEmitter<any>();
}
- open(options?: NgbModalOptions): Promise<any> {
+ async open(options?: NgbModalOptions): Promise<any> {
if (this.modalRef !== null) {
console.warn('Dismissing existing dialog');
-import {Component, Input, OnInit, Host, TemplateRef} from '@angular/core';
+import {Component, Input, Output, OnInit, Host, TemplateRef, EventEmitter} from '@angular/core';
import {GridToolbarAction} from './grid';
import {GridComponent} from './grid.component';
// Note most input fields should match class fields for GridColumn
@Input() label: string;
+
+ // Register to click events
+ @Output() onClick: EventEmitter<any []>;
+
+ // DEPRECATED: Pass a reference to a function that is called on click.
@Input() action: (rows: any[]) => any;
+ // When present, actions will be grouped by the provided label.
+ @Input() group: string;
+
// Optional: add a function that returns true or false.
// If true, this action will be disabled; if false
// (default behavior), the action will be enabled.
@Input() disableOnRows: (rows: any[]) => boolean;
+
// get a reference to our container grid.
- constructor(@Host() private grid: GridComponent) {}
+ constructor(@Host() private grid: GridComponent) {
+ this.onClick = new EventEmitter<any []>();
+ }
ngOnInit() {
const action = new GridToolbarAction();
action.label = this.label;
action.action = this.action;
+ action.onClick = this.onClick;
+ action.group = this.group;
action.disableOnRows = this.disableOnRows;
-
this.grid.context.toolbarActions.push(action);
}
}
-import {Component, Input, OnInit, Host, TemplateRef} from '@angular/core';
+import {Component, Input, Output, OnInit, Host, TemplateRef, EventEmitter} from '@angular/core';
import {GridToolbarButton} from './grid';
import {GridComponent} from './grid.component';
// Note most input fields should match class fields for GridColumn
@Input() label: string;
+
+ // Register to click events
+ @Output() onClick: EventEmitter<any>;
+
+ // DEPRECATED: Pass a reference to a function that is called on click.
@Input() action: () => any;
+
@Input() set disabled(d: boolean) {
// Support asynchronous disabled values by appling directly
// to our button object as values arrive.
// get a reference to our container grid.
constructor(@Host() private grid: GridComponent) {
+ this.onClick = new EventEmitter<any>();
this.button = new GridToolbarButton();
+ this.button.onClick = this.onClick;
}
ngOnInit() {
<div class="btn-grp" *ngIf="gridContext.toolbarButtons.length">
<button *ngFor="let btn of gridContext.toolbarButtons"
[disabled]="btn.disabled"
- class="btn btn-outline-dark mr-1" (click)="btn.action()">
+ class="btn btn-outline-dark mr-1" (click)="performButtonAction(btn)">
{{btn.label}}
</button>
</div>
<button class="dropdown-item" (click)="performAction(action)"
*ngFor="let action of gridContext.toolbarActions"
[disabled]="shouldDisableAction(action)">
- <span class="ml-2">{{action.label}}</span>
+ <ng-container *ngIf="action.isGroup">
+ <span class="ml-2 font-weight-bold font-italic">{{action.label}}</span>
+ </ng-container>
+ <ng-container *ngIf="action.group && !action.isGroup">
+ <!-- grouped entries are indented -->
+ <span class="ml-4">{{action.label}}</span>
+ </ng-container>
+ <ng-container *ngIf="!action.group && !action.isGroup">
+ <span class="ml-2">{{action.label}}</span>
+ </ng-container>
</button>
</div>
</div>
@Input() colWidthConfig: GridColumnWidthComponent;
@Input() gridPrinter: GridPrintComponent;
+ renderedGroups: {[group: string]: boolean};
+
csvExportInProgress: boolean;
csvExportUrl: SafeUrl;
csvExportFileName: string;
- constructor(private sanitizer: DomSanitizer) {}
+ constructor(private sanitizer: DomSanitizer) {
+ this.renderedGroups = {};
+ }
+
+ ngOnInit() {
+ this.sortActions();
+ }
+
+ sortActions() {
+ const actions = this.gridContext.toolbarActions;
+
+ const unGrouped = actions.filter(a => !a.group)
+ .sort((a, b) => {
+ return a.label < b.label ? -1 : 1;
+ });
+
+ const grouped = actions.filter(a => Boolean(a.group))
+ .sort((a, b) => {
+ if (a.group === b.group) {
+ return a.label < b.label ? -1 : 1;
+ } else {
+ return a.group < b.group ? -1 : 1;
+ }
+ });
- ngOnInit() {}
+ // Insert group markers for rendering
+ const seen: any = {};
+ const grouped2: any[] = [];
+ grouped.forEach(action => {
+ if (!seen[action.group]) {
+ seen[action.group] = true;
+ const act = new GridToolbarAction();
+ act.label = action.group;
+ act.isGroup = true;
+ grouped2.push(act);
+ }
+ grouped2.push(action);
+ });
+
+ this.gridContext.toolbarActions = unGrouped.concat(grouped2);
+ }
saveGridConfig() {
// TODO: when server-side settings are supported, this operation
}
performAction(action: GridToolbarAction) {
- action.action(this.gridContext.getSelectedRows());
+ const rows = this.gridContext.getSelectedRows();
+ action.onClick.emit(rows);
+ if (action.action) { action.action(rows); }
+ }
+
+ performButtonAction(button: GridToolbarButton) {
+ const rows = this.gridContext.getSelectedRows();
+ button.onClick.emit();
+ if (button.action) { button.action(); }
}
shouldDisableAction(action: GridToolbarAction) {
/**
* Collection of grid related classses and interfaces.
*/
-import {TemplateRef} from '@angular/core';
+import {TemplateRef, EventEmitter} from '@angular/core';
import {Observable, Subscription} from 'rxjs';
import {IdlService, IdlObject} from '@eg/core/idl.service';
import {OrgService} from '@eg/core/org.service';
// Actions apply to specific rows
export class GridToolbarAction {
label: string;
- action: (rows: any[]) => any;
+ onClick: EventEmitter<any []>;
+ action: (rows: any[]) => any; // DEPRECATED
+ group: string;
+ isGroup: boolean; // used for group placeholder entries
disableOnRows: (rows: any[]) => boolean;
}
// Buttons are global actions
export class GridToolbarButton {
label: string;
- action: () => any;
+ onClick: EventEmitter<any []>;
+ action: () => any; // DEPRECATED
disabled: boolean;
}
class="form-control"
[attr.id]="domId.length ? domId : null"
[placeholder]="placeholder"
+ [disabled]="disabled"
[(ngModel)]="selected"
[ngbTypeahead]="filter"
[resultTemplate]="displayTemplate"
selected: OrgDisplay;
hidden: number[] = [];
- disabled: number[] = [];
click$ = new Subject<string>();
startOrg: IdlObject;
+ // Disable the entire input
+ @Input() disabled: boolean;
+
@ViewChild('instance') instance: NgbTypeahead;
// Placeholder text for selector input
}
// List of org unit IDs to disable in the selector
+ _disabledOrgs: number[] = [];
@Input() set disableOrgs(ids: number[]) {
- if (ids) { this.disabled = ids; }
+ if (ids) { this._disabledOrgs = ids; }
}
// Apply an org unit value at load time.
selector: 'eg-string',
template: `
<span style='display:none'>
- <ng-container *ngTemplateOutlet="template; context:ctx"></ng-container>
+ <ng-container *ngIf="template">
+ <ng-container *ngTemplateOutlet="template; context:ctx"></ng-container>
+ </ng-container>
+ <ng-container *ngIf="!template">
+ <span>{{text}}</span>
+ </ng-container>
</span>
`
})
// NOTE: talking to the native DOM element is not so great, but
// hopefully we can retire the String* code entirely once
// in-code translations are supported (Ang6?)
- current(ctx?: any): Promise<string> {
+ async current(ctx?: any): Promise<string> {
if (ctx) { this.ctx = ctx; }
- return new Promise(resolve => {
- setTimeout(() => resolve(this.elm.nativeElement.textContent));
- });
+ return new Promise<string>(resolve =>
+ setTimeout(() => resolve(this.elm.nativeElement.textContent))
+ );
}
}
import {StaffCommonModule} from '@eg/staff/common.module';
import {CatalogCommonModule} from '@eg/share/catalog/catalog-common.module';
import {CatalogRoutingModule} from './routing.module';
+import {HoldsModule} from '@eg/staff/share/holds/holds.module';
+import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module';
import {CatalogComponent} from './catalog.component';
import {SearchFormComponent} from './search-form.component';
import {ResultsComponent} from './result/results.component';
import {StaffCatalogService} from './catalog.service';
import {RecordPaginationComponent} from './record/pagination.component';
import {RecordActionsComponent} from './record/actions.component';
-import {HoldingsService} from '@eg/staff/share/holdings.service';
import {BasketActionsComponent} from './basket-actions.component';
import {HoldComponent} from './hold/hold.component';
-import {HoldService} from '@eg/staff/share/hold.service';
import {PartsComponent} from './record/parts.component';
import {PartMergeDialogComponent} from './record/part-merge-dialog.component';
import {BrowseComponent} from './browse.component';
PartsComponent,
PartMergeDialogComponent,
BrowseComponent,
- BrowseResultsComponent
+ BrowseResultsComponent,
],
imports: [
StaffCommonModule,
CatalogCommonModule,
- CatalogRoutingModule
+ CatalogRoutingModule,
+ HoldsModule
],
providers: [
- StaffCatalogService,
- HoldingsService,
- HoldService
+ StaffCatalogService
]
})
<div class="row">
- <div class="col-lg-3">
+ <div class="col-lg-4">
<h3 i18n>Place Hold
<small *ngIf="user">
({{user.family_name()}}, {{user.first_given_name()}})
</small>
</h3>
</div>
- <div class="col-lg-3 text-right">
- <button class="btn btn-outline-dark btn-sm"
- [disabled]="true" i18n>
- <span class="material-icons mat-icon-in-button align-middle" title="Search for Patron">search</span>
- <span class="align-middle">Search for Patron</span>
+ <div class="col-lg-2 text-right">
+ <button class="btn btn-outline-dark btn-sm" [disabled]="true">
+ <span class="material-icons mat-icon-in-button align-middle"
+ i18n-title title="Search for Patron">search</span>
+ <span class="align-middle" i18n>Search for Patron</span>
</button>
</div>
</div>
import {CatalogSearchContext, CatalogSearchState} from '@eg/share/catalog/search-context';
import {CatalogService} from '@eg/share/catalog/catalog.service';
import {StaffCatalogService} from '../catalog.service';
-import {HoldService, HoldRequest,
- HoldRequestTarget} from '@eg/staff/share/hold.service';
+import {HoldsService, HoldRequest,
+ HoldRequestTarget} from '@eg/staff/share/holds/holds.service';
import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
class HoldContext {
private bib: BibRecordService,
private cat: CatalogService,
private staffCat: StaffCatalogService,
- private holds: HoldService,
+ private holds: HoldsService,
private perm: PermService
) {
this.holdContexts = [];
<button class="btn btn-info ml-1" i18n>View in Catalog</button>
</a>
+ <a routerLink="/staff/catalog/hold/T" [queryParams]="{target: recId}">
+ <button class="btn btn-info ml-1" i18n>Place Hold</button>
+ </a>
+
<button class="btn btn-info ml-1" (click)="addVolumes()" i18n>
Add Holdings
</button>
import {StaffCatalogService} from '../catalog.service';
import {StringService} from '@eg/share/string/string.service';
import {ToastService} from '@eg/share/toast/toast.service';
-import {HoldingsService} from '@eg/staff/share/holdings.service';
+import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
@Component({
selector: 'eg-catalog-record-actions',
<div id='staff-catalog-bib-tabs-container' class='mt-3'>
<div class="row">
<div class="col-lg-12 text-right">
- <button class="btn btn-secondary btn-sm"
+ <button class="btn btn-secondary btn-sm"
[disabled]="recordTab == defaultTab"
(click)="setDefaultTab()" i18n>Set Default View</button>
</div>
</ngb-tab>
<ngb-tab title="View Holds" i18n-title id="holds">
<ng-template ngbTabContent>
- <div class="alert alert-info mt-3" i18n>
- Holds tab not yet implemented. See the
- <a target="_blank"
- href="/eg/staff/cat/catalog/record/{{recordId}}/holds">
- AngularJS Holds Tab.
- </a>
- </div>
+ <eg-holds-grid [recordId]="recordId"
+ persistKey="cat.catalog.wide_holds"
+ [defaultSort]="[{name:'request_time',dir:'asc'}]"
+ [initialPickupLib]="currentSearchOrg()"></eg-holds-grid>
</ng-template>
</ngb-tab>
<ngb-tab title="Monograph Parts" i18n-title id="monoparts">
this.bib.fleshBibUsers([summary.record]);
});
}
+
+ currentSearchOrg(): IdlObject {
+ if (this.staffCat && this.staffCat.searchContext) {
+ return this.staffCat.searchContext.searchOrg;
+ }
+ return null;
+ }
}
selector = '#first-query-input';
}
- this.renderer.selectRootElement(selector).focus();
+ try {
+ // TODO: sometime the selector is not available in the DOM
+ // until even later (even with setTimeouts). Need to fix this.
+ // Note the error is thrown from selectRootElement(), not the
+ // call to .focus() on a null reference.
+ this.renderer.selectRootElement(selector).focus();
+ } catch (E) {}
}
/**
+++ /dev/null
-/**
- * Common code for mananging holdings
- */
-import {Injectable, EventEmitter} from '@angular/core';
-import {Observable} from 'rxjs';
-import {map, mergeMap} from 'rxjs/operators';
-import {IdlObject} from '@eg/core/idl.service';
-import {NetService} from '@eg/core/net.service';
-import {PcrudService} from '@eg/core/pcrud.service';
-import {EventService, EgEvent} from '@eg/core/event.service';
-import {AuthService} from '@eg/core/auth.service';
-import {BibRecordService,
- BibRecordSummary} from '@eg/share/catalog/bib-record.service';
-
-// Response from a place-holds API call.
-export interface HoldRequestResult {
- success: boolean;
- holdId?: number;
- evt?: EgEvent;
-}
-
-// Values passed to the place-holds API call.
-export interface HoldRequest {
- holdType: string;
- holdTarget: number;
- recipient: number;
- requestor: number;
- pickupLib: number;
- override?: boolean;
- notifyEmail?: boolean;
- notifyPhone?: string;
- notifySms?: string;
- smsCarrier?: string;
- thawDate?: string; // ISO date
- frozen?: boolean;
- holdableFormats?: {[target: number]: string};
- result?: HoldRequestResult;
-}
-
-// A fleshed hold request target object containing whatever data is
-// available for each hold type / target. E.g. a TITLE hold will
-// not have a value for 'volume', but a COPY hold will, since all
-// copies have volumes. Every HoldRequestTarget will have a bibId and
-// bibSummary. Some values come directly from the API call, others
-// applied locally.
-export interface HoldRequestTarget {
- target: number;
- metarecord?: IdlObject;
- bibrecord?: IdlObject;
- bibId?: number;
- bibSummary?: BibRecordSummary;
- part?: IdlObject;
- volume?: IdlObject;
- copy?: IdlObject;
- issuance?: IdlObject;
- metarecord_filters?: any;
-}
-
-@Injectable()
-export class HoldService {
-
- constructor(
- private evt: EventService,
- private net: NetService,
- private pcrud: PcrudService,
- private auth: AuthService,
- private bib: BibRecordService,
- ) {}
-
- placeHold(request: HoldRequest): Observable<HoldRequest> {
-
- let method = 'open-ils.circ.holds.test_and_create.batch';
- if (request.override) { method = method + '.override'; }
-
- return this.net.request(
- 'open-ils.circ', method, this.auth.token(), {
- patronid: request.recipient,
- pickup_lib: request.pickupLib,
- hold_type: request.holdType,
- email_notify: request.notifyEmail,
- phone_notify: request.notifyPhone,
- thaw_date: request.thawDate,
- frozen: request.frozen,
- sms_notify: request.notifySms,
- sms_carrier: request.smsCarrier,
- holdable_formats_map: request.holdableFormats
- },
- [request.holdTarget]
- ).pipe(map(
- resp => {
- let result = resp.result;
- const holdResult: HoldRequestResult = {success: true};
-
- // API can return an ID, an array of events, or a hash
- // of info.
-
- if (Number(result) > 0) {
- // On success, the API returns the hold ID.
- holdResult.holdId = result;
- console.debug(`Hold successfully placed ${result}`);
-
- } else {
- holdResult.success = false;
- console.info('Hold request failed: ', result);
-
- if (Array.isArray(result)) { result = result[0]; }
-
- if (this.evt.parse(result)) {
- holdResult.evt = this.evt.parse(result);
- } else {
- holdResult.evt = this.evt.parse(result.last_event);
- }
- }
-
- request.result = holdResult;
- return request;
- }
- ));
- }
-
- getHoldTargetMeta(holdType: string, holdTarget: number | number[],
- orgId?: number): Observable<HoldRequestTarget> {
-
- const targetIds = [].concat(holdTarget);
-
- return this.net.request(
- 'open-ils.circ',
- 'open-ils.circ.hold.get_metadata',
- holdType, targetIds, orgId
- ).pipe(mergeMap(meta => {
- const target: HoldRequestTarget = meta;
- target.bibId = target.bibrecord.id();
-
- return this.bib.getBibSummary(target.bibId)
- .pipe(map(sum => {
- target.bibSummary = sum;
- return target;
- }));
- }));
- }
-}
-
+++ /dev/null
-/**
- * Common code for mananging holdings
- */
-import {Injectable, EventEmitter} from '@angular/core';
-import {NetService} from '@eg/core/net.service';
-import {AnonCacheService} from '@eg/share/util/anon-cache.service';
-
-interface NewVolumeData {
- owner: number;
- label?: string;
-}
-
-@Injectable()
-export class HoldingsService {
-
- constructor(
- private net: NetService,
- private anonCache: AnonCacheService
- ) {}
-
- // Open the holdings editor UI in a new browser window/tab.
- spawnAddHoldingsUi(
- recordId: number, // Bib record ID
- addToVols: number[] = [], // Add copies to existing volumes
- volumeData: NewVolumeData[] = []) { // Creating new volumes
-
- const raw: any[] = [];
-
- if (addToVols) {
- addToVols.forEach(volId => raw.push({callnumber: volId}));
- } else if (volumeData) {
- volumeData.forEach(data => raw.push(data));
- }
-
- if (raw.length === 0) { raw.push({}); }
-
- this.anonCache.setItem(null, 'edit-these-copies', {
- record_id: recordId,
- raw: raw,
- hide_vols : false,
- hide_copies : false
- }).then(key => {
- if (!key) {
- console.error('Could not create holds cache key!');
- return;
- }
- setTimeout(() => {
- const url = `/eg/staff/cat/volcopy/${key}`;
- window.open(url, '_blank');
- });
- });
- }
-}
-
--- /dev/null
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {HoldingsService} from './holdings.service';
+import {MarkDamagedDialogComponent} from './mark-damaged-dialog.component';
+import {MarkMissingDialogComponent} from './mark-missing-dialog.component';
+
+@NgModule({
+ declarations: [
+ MarkDamagedDialogComponent,
+ MarkMissingDialogComponent
+ ],
+ imports: [
+ StaffCommonModule
+ ],
+ exports: [
+ MarkDamagedDialogComponent,
+ MarkMissingDialogComponent
+ ],
+ providers: [
+ HoldingsService
+ ]
+})
+
+export class HoldingsModule {}
+
--- /dev/null
+/**
+ * Common code for mananging holdings
+ */
+import {Injectable, EventEmitter} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {AnonCacheService} from '@eg/share/util/anon-cache.service';
+import {AuthService} from '@eg/core/auth.service';
+import {EventService} from '@eg/core/event.service';
+
+interface NewVolumeData {
+ owner: number;
+ label?: string;
+}
+
+@Injectable()
+export class HoldingsService {
+
+ constructor(
+ private net: NetService,
+ private auth: AuthService,
+ private evt: EventService,
+ private anonCache: AnonCacheService
+ ) {}
+
+ // Open the holdings editor UI in a new browser window/tab.
+ spawnAddHoldingsUi(
+ recordId: number, // Bib record ID
+ addToVols: number[] = [], // Add copies to existing volumes
+ volumeData: NewVolumeData[] = []) { // Creating new volumes
+
+ const raw: any[] = [];
+
+ if (addToVols) {
+ addToVols.forEach(volId => raw.push({callnumber: volId}));
+ } else if (volumeData) {
+ volumeData.forEach(data => raw.push(data));
+ }
+
+ if (raw.length === 0) { raw.push({}); }
+
+ this.anonCache.setItem(null, 'edit-these-copies', {
+ record_id: recordId,
+ raw: raw,
+ hide_vols : false,
+ hide_copies : false
+ }).then(key => {
+ if (!key) {
+ console.error('Could not create holds cache key!');
+ return;
+ }
+ setTimeout(() => {
+ const url = `/eg/staff/cat/volcopy/${key}`;
+ window.open(url, '_blank');
+ });
+ });
+ }
+}
+
--- /dev/null
+<eg-string #successMsg text="Successfully Marked Item Damaged" i18n-text></eg-string>
+<eg-string #errorMsg text="Failed To Mark Item Damaged" i18n-text></eg-string>
+
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h4 class="modal-title">
+ <span i18n>Mark Item Damaged</span>
+ </h4>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="dismiss('cross_click')">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <div class="row">
+ <div class="col-lg-1">Barcode:</div>
+ <div class="col-lg-11 font-weight-bold">{{copy.barcode()}}</div>
+ </div>
+ <div class="row">
+ <div class="col-lg-1">Title:</div>
+ <div class="col-lg-11">{{bibSummary.display.title}}</div>
+ </div>
+ <div class="row">
+ <div class="col-lg-1">Author:</div>
+ <div class="col-lg-11">{{bibSummary.display.author}}</div>
+ </div>
+ <div class="card mt-3" *ngIf="chargeResponse">
+ <div class="card-header" i18n>
+ Item was previously checked out
+ </div>
+ <div class="card-body">
+ <ul class="list-group list-group-flush">
+ <li class="list-group-item" i18n>
+ Item was last checked out by patron
+ <a href="/eg/staff/circ/patron/{{chargeResponse.circ.usr().id()}}/checkout">
+ {{chargeResponse.circ.usr().family_name()}},
+ {{chargeResponse.circ.usr().first_given_name()}}
+ ({{chargeResponse.circ.usr().usrname()}})
+ </a>.
+ </li>
+ <li class="list-group-item" i18n>
+ Item was due
+ {{chargeResponse.circ.due_date() | formatValue:'timestamp'}}
+ and returned
+ {{chargeResponse.circ.checkin_time() | date:'MM/dd/yy H:mm a'}}.
+ </li>
+ <li class="list-group-item">
+ <span i18n>
+ Calucated fine amount is
+ <span class="font-weight-bold text-danger">
+ {{chargeResponse.charge | currency}}
+ </span>
+ </span>
+ </li>
+ <ng-container *ngIf="amountChangeRequested">
+ <li class="list-group-item">
+ <div class="row">
+ <div class="col-lg-3" i8n>Billing Type</div>
+ <div class="col-lg-6">
+ <eg-combobox
+ placeholder="Billing Type..." i18n-placeholder
+ (onChange)="newBtype = $event.id"
+ [entries]="billingTypes"></eg-combobox>
+ </div>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="row">
+ <div class="col-lg-3" i8n>Charge Amount</div>
+ <div class="col-lg-6">
+ <input class="form-control" type="number" step="0.01" min="0"
+ [(ngModel)]="newCharge"/>
+ </div>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="row">
+ <div class="col-lg-3" i8n>Note</div>
+ <div class="col-lg-6">
+ <textarea class="form-control" rows="3"
+ [(ngModel)]="newNote"></textarea>
+ </div>
+ </div>
+ </li>
+ </ng-container><!-- amount change requested -->
+ </ul>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <ng-container *ngIf="!chargeResponse">
+ <button type="button" class="btn btn-warning"
+ (click)="dismiss('canceled')" i18n>Cancel</button>
+ <button type="button" class="btn btn-success"
+ (click)="markDamaged()" i18n>Mark Damaged</button>
+ </ng-container>
+ <ng-container *ngIf="chargeResponse">
+ <button type="button" class="btn btn-warning"
+ (click)="dismiss('canceled')" i18n>Cancel</button>
+ <button class="btn btn-info mr-2"
+ (click)="amountChangeRequested = true" i18n>Change Amount</button>
+ <button class="btn btn-secondary mr-2"
+ (click)="markDamaged({apply_fines:'noapply'})" i18n>No Charge</button>
+ <button class="btn btn-success mr-2"
+ (click)="markDamaged({apply_fines:'apply'})" i18n>OK</button>
+ </ng-container>
+ </div>
+</ng-template>
--- /dev/null
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {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 {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+/**
+ * Dialog for marking items damaged and asessing related bills.
+ */
+
+@Component({
+ selector: 'eg-mark-damaged-dialog',
+ templateUrl: 'mark-damaged-dialog.component.html'
+})
+
+export class MarkDamagedDialogComponent
+ extends DialogComponent implements OnInit {
+
+ @Input() copyId: number;
+ copy: IdlObject;
+ bibSummary: BibRecordSummary;
+ billingTypes: ComboboxEntry[];
+
+ // Overide the API suggested charge amount
+ amountChangeRequested: boolean;
+ newCharge: number;
+ newNote: string;
+ newBtype: number;
+
+ @ViewChild('successMsg') private successMsg: StringComponent;
+ @ViewChild('errorMsg') private errorMsg: StringComponent;
+
+
+ // Charge data returned from the server requesting additional charge info.
+ chargeResponse: any;
+
+ constructor(
+ private modal: NgbModal, // required for passing to parent
+ private toast: ToastService,
+ private net: NetService,
+ private evt: EventService,
+ private pcrud: PcrudService,
+ private org: OrgService,
+ private bib: BibRecordService,
+ private auth: AuthService) {
+ super(modal); // required for subclassing
+ this.billingTypes = [];
+ }
+
+ ngOnInit() {}
+
+ /**
+ * Fetch the item/record, then open the dialog.
+ * Dialog promise resolves with true/false indicating whether
+ * the mark-damanged action occured or was dismissed.
+ */
+ async open(args: NgbModalOptions): Promise<boolean> {
+ this.reset();
+
+ if (!this.copyId) {
+ return Promise.reject('copy ID required');
+ }
+
+ await this.getBillingTypes();
+ await this.getData();
+ return super.open(args);
+ }
+
+ // Fetch-cache billing types
+ async getBillingTypes(): Promise<any> {
+ if (this.billingTypes.length > 1) {
+ return Promise.resolve();
+ }
+ return this.pcrud.search('cbt',
+ {owner: this.org.fullPath(this.auth.user().ws_ou(), true)},
+ {}, {atomic: true}
+ ).toPromise().then(bts => {
+ this.billingTypes = bts
+ .sort((a, b) => a.name() < b.name() ? -1 : 1)
+ .map(bt => ({id: bt.id(), label: bt.name()}));
+ });
+ }
+
+ async getData(): Promise<any> {
+ return this.pcrud.retrieve('acp', this.copyId,
+ {flesh: 1, flesh_fields: {acp: ['call_number']}}).toPromise()
+ .then(copy => {
+ this.copy = copy;
+ return this.bib.getBibSummary(
+ copy.call_number().record()).toPromise();
+ }).then(summary => {
+ this.bibSummary = summary;
+ });
+ }
+
+ reset() {
+ this.copy = null;
+ this.bibSummary = null;
+ this.chargeResponse = null;
+ this.newCharge = null;
+ this.newNote = null;
+ this.amountChangeRequested = false;
+ }
+
+ bTypeChange(entry: ComboboxEntry) {
+ this.newBtype = entry.id;
+ }
+
+ markDamaged(args: any) {
+ this.chargeResponse = null;
+
+ if (args && args.apply_fines === 'apply') {
+ args.override_amount = this.newCharge;
+ args.override_btype = this.newBtype;
+ args.override_note = this.newNote;
+ }
+
+ this.net.request(
+ 'open-ils.circ', 'open-ils.circ.mark_item_damaged',
+ this.auth.token(), this.copyId, args
+ ).subscribe(
+ result => {
+ console.debug('Mark damaged returned', result);
+
+ if (Number(result) === 1) {
+ this.successMsg.current().then(msg => this.toast.success(msg));
+ this.close(true);
+ return;
+ }
+
+ const evt = this.evt.parse(result);
+
+ if (evt.textcode === 'DAMAGE_CHARGE') {
+ // More info needed from staff on how to hangle charges.
+ this.chargeResponse = evt.payload;
+ this.newCharge = this.chargeResponse.charge;
+ }
+ },
+ err => {
+ this.errorMsg.current().then(m => this.toast.danger(m));
+ console.error(err);
+ }
+ );
+ }
+}
+
--- /dev/null
+
+
+<eg-string #successMsg
+ text="Successfully Marked Item Missing" i18n-text></eg-string>
+<eg-string #errorMsg
+ text="Failed To Mark Item Missing" i18n-text></eg-string>
+
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h4 class="modal-title">
+ <span i18n>Mark Item Missing</span>
+ </h4>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="dismiss('cross_click')">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <div class="row d-flex justify-content-center">
+ <h5>Mark {{copyIds.length}} Item(s) Missing?</h5>
+ </div>
+ <div class="row" *ngIf="numSucceeded > 0">
+ <div class="col-lg-12" i18n>
+ {{numSucceeded}} Items(s) Successfully Marked Missing
+ </div>
+ </div>
+ <div class="row" *ngIf="numFailed > 0">
+ <div class="col-lg-12">
+ <div class="alert alert-warning">
+ {{numFailed}} Items(s) Failed to be Marked Missing
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <ng-container *ngIf="!chargeResponse">
+ <button type="button" class="btn btn-warning"
+ (click)="dismiss('canceled')" i18n>Cancel</button>
+ <button type="button" class="btn btn-success"
+ (click)="markItemsMissing()" i18n>Mark Missing</button>
+ </ng-container>
+ </div>
+ </ng-template>
+
\ No newline at end of file
--- /dev/null
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {AuthService} from '@eg/core/auth.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {StringComponent} from '@eg/share/string/string.component';
+
+
+/**
+ * Dialog for marking items missing.
+ */
+
+@Component({
+ selector: 'eg-mark-missing-dialog',
+ templateUrl: 'mark-missing-dialog.component.html'
+})
+
+export class MarkMissingDialogComponent
+ extends DialogComponent implements OnInit {
+
+ @Input() copyIds: number[];
+
+ numSucceeded: number;
+ numFailed: number;
+
+ @ViewChild('successMsg')
+ private successMsg: StringComponent;
+
+ @ViewChild('errorMsg')
+ private errorMsg: StringComponent;
+
+ constructor(
+ private modal: NgbModal, // required for passing to parent
+ private toast: ToastService,
+ private net: NetService,
+ private evt: EventService,
+ private auth: AuthService) {
+ super(modal); // required for subclassing
+ }
+
+ ngOnInit() {}
+
+ async markOneItemMissing(ids: number[]): Promise<any> {
+ if (ids.length === 0) {
+ return Promise.resolve();
+ }
+
+ const id = ids.pop();
+
+ return this.net.request(
+ 'open-ils.circ',
+ 'open-ils.circ.mark_item_missing',
+ this.auth.token(), id
+ ).toPromise().then(async(result) => {
+ if (Number(result) === 1) {
+ this.numSucceeded++;
+ this.toast.success(await this.successMsg.current());
+ } else {
+ this.numFailed++;
+ console.error('Mark missing failed ', this.evt.parse(result));
+ this.toast.warning(await this.errorMsg.current());
+ }
+ return this.markOneItemMissing(ids);
+ });
+ }
+
+ async markItemsMissing(): Promise<any> {
+ this.numSucceeded = 0;
+ this.numFailed = 0;
+ const ids = [].concat(this.copyIds);
+ await this.markOneItemMissing(ids);
+ this.close(this.numSucceeded > 0);
+ }
+}
+
+
+
--- /dev/null
+<eg-string #successMsg
+ text="Successfully Canceled Hold" i18n-text></eg-string>
+<eg-string #errorMsg
+ text="Failed To Cancel Hold" i18n-text></eg-string>
+
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h4 class="modal-title">
+ <span i18n>Cancel Hold</span>
+ </h4>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="dismiss('cross_click')">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <div class="row d-flex justify-content-center">
+ <h5>Cancel {{holdIds.length}} Holds?</h5>
+ </div>
+ <div class="row mt-2">
+ <div class="col-lg-4">
+ <label for="cance-reasons" i18n>Cancel Reason</label>
+ </div>
+ <div class="col-lg-8">
+ <eg-combobox id='cancel-reasons' [entries]="cancelReasons"
+ [startId]="5" (onChange)="cancelReason = $event ? $event.id : null">
+ </eg-combobox>
+ </div>
+ </div>
+ <div class="row mt-2">
+ <div class="col-lg-4">
+ <label for="cance-note" i18n>Cancel Note</label>
+ </div>
+ <div class="col-lg-8">
+ <textarea id='cancel-note' class="form-control"
+ [(ngModel)]="cancelNote"></textarea>
+ </div>
+ </div>
+ <div class="row mt-2" *ngIf="numSucceeded > 0">
+ <div class="col-lg-12" i18n>
+ {{numSucceeded}} Hold(s) Successfully Canceled
+ </div>
+ <div class="row" *ngIf="numFailed > 0">
+ <div class="col-lg-12">
+ <div class="alert alert-warning">
+ {{numFailed}} Hold(s) Failed to Cancel.
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <ng-container *ngIf="!chargeResponse">
+ <button type="button" class="btn btn-warning"
+ (click)="dismiss('canceled')" i18n>Cancel</button>
+ <button type="button" class="btn btn-success"
+ (click)="cancelBatch()" i18n>Cancel Hold</button>
+ </ng-container>
+ </div>
+ </ng-template>
\ No newline at end of file
--- /dev/null
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+/**
+ * Dialog for canceling hold requests.
+ */
+
+@Component({
+ selector: 'eg-hold-cancel-dialog',
+ templateUrl: 'cancel-dialog.component.html'
+})
+
+export class HoldCancelDialogComponent
+ extends DialogComponent implements OnInit {
+
+ @Input() holdIds: number[];
+ @ViewChild('successMsg') private successMsg: StringComponent;
+ @ViewChild('errorMsg') private errorMsg: StringComponent;
+
+ changesApplied: boolean;
+ numSucceeded: number;
+ numFailed: number;
+ cancelReason: number;
+ cancelReasons: ComboboxEntry[];
+ cancelNote: string;
+
+ constructor(
+ private modal: NgbModal, // required for passing to parent
+ private toast: ToastService,
+ private net: NetService,
+ private evt: EventService,
+ private pcrud: PcrudService,
+ private auth: AuthService) {
+ super(modal); // required for subclassing
+ this.cancelReasons = [];
+ }
+
+ ngOnInit() {
+ // Avoid fetching cancel reasons in ngOnInit becaues that causes
+ // them to load regardless of whether the dialog is ever used.
+ }
+
+ open(args: NgbModalOptions): Promise<boolean> {
+
+ if (this.cancelReasons.length === 0) {
+ this.pcrud.retrieveAll('ahrcc', {}, {atomic: true}).toPromise()
+ .then(reasons => {
+ this.cancelReasons =
+ reasons.map(r => ({id: r.id(), label: r.label()}));
+ });
+ }
+
+ return super.open(args);
+ }
+
+ async cancelNext(ids: number[]): Promise<any> {
+ if (ids.length === 0) {
+ return Promise.resolve();
+ }
+
+ return this.net.request(
+ 'open-ils.circ', 'open-ils.circ.hold.cancel',
+ this.auth.token(), ids.pop(),
+ this.cancelReason, this.cancelNote
+ ).toPromise().then(
+ async(result) => {
+ if (Number(result) === 1) {
+ this.numSucceeded++;
+ this.toast.success(await this.successMsg.current());
+ } else {
+ this.numFailed++;
+ console.error(this.evt.parse(result));
+ this.toast.warning(await this.errorMsg.current());
+ }
+ this.cancelNext(ids);
+ }
+ );
+ }
+
+ async cancelBatch(): Promise<any> {
+ this.numSucceeded = 0;
+ this.numFailed = 0;
+ const ids = [].concat(this.holdIds);
+ await this.cancelNext(ids);
+ this.close(this.numSucceeded > 0);
+ }
+}
+
+
+
--- /dev/null
+
+<eg-staff-banner bannerText="Hold Details (#{{hold.id}})" i18n-bannerText>
+</eg-staff-banner>
+
+<div class="row">
+ <div class="col-lg-3">
+ <button (click)="showListView()" class="btn btn-info" i18n>List View</button>
+ </div>
+</div>
+
+<div class="well-table">
+ <div class="well-row">
+ <div class="well-label" i18n>Request Date</div>
+ <div class="well-value">{{hold.request_time | formatValue:'timestamp'}}</div>
+ <div class="well-label" i18n>Capture Date</div>
+ <div class="well-value">{{hold.capture_time | formatValue:'timestamp'}}</div>
+ <div class="well-label" i18n>Available On</div>
+ <div class="well-value">{{hold.shelf_time | formatValue:'timestamp'}}</div>
+ </div>
+ <div class="well-row">
+ <div class="well-label" i18n>hold Type</div>
+ <div class="well-value">
+ {{hold.hold_type}}
+ <!-- TODO: add part data to wide holds
+ <span *ngIf="hold.hold_type == 'P'"> - {{hold.part_label}}</span>
+ -->
+ </div>
+ <div class="well-label" i18n>Current Item</div>
+ <div class="well-value">
+ <a href="/eg/staff/cat/item/{{hold.cp_id}}">{{hold.cp_barcode}}</a>
+ </div>
+ <div class="well-label" i18n>Call Number</div>
+ <div class="well-value">{{hold.cn_full_label}}</div>
+ </div>
+ <div class="well-row">
+ <div class="well-label" i18n>Pickup Lib</div>
+ <div class="well-value">{{hold.pl_shortname}}</div>
+ <div class="well-label" i18n>Status</div>
+ <div class="well-value">
+ <ng-container [ngSwitch]="hold.hold_status">
+ <div *ngSwitchCase="-1" i18n>Unknown Error</div>
+ <div *ngSwitchCase="1" i18n>Waiting for Item</div>
+ <div *ngSwitchCase="2" i18n>Waiting for Capture</div>
+ <div *ngSwitchCase="3" i18n>In Transit</div>
+ <div *ngSwitchCase="4" i18n>Ready for Pickup</div>
+ <div *ngSwitchCase="5" i18n>Hold Shelf Delay</div>
+ <div *ngSwitchCase="6" i18n>Canceled</div>
+ <div *ngSwitchCase="7" i18n>Suspended</div>
+ <div *ngSwitchCase="8" i18n>Wrong Shelf</div>
+ <div *ngSwitchCase="9" i18n>Fulfilled</div>
+ </ng-container>
+ </div>
+ <div class="well-label" i18n>Behind Desk</div>
+ <div class="well-value">{{hold.behind_desk == '1'}}</div>
+ </div>
+ <div class="well-row">
+ <div class="well-label" i18n>Current Shelf Lib</div>
+ <div class="well-value">{{getOrgName(hold.current_shelf_lib)}}</div>
+ <div class="well-label" i18n>Current Shelving Location</div>
+ <div class="well-value">{{hold.acpl_name}}</div>
+ <div class="well-label" i18n>Force Item Quality</div>
+ <div class="well-value">{{hold.mint_condition == '1'}}</div>
+ </div>
+ <div class="well-row">
+ <div class="well-label" i18n>Email Notify</div>
+ <div class="well-value">{{hold.email_notify == '1'}}</div>
+ <div class="well-label" i18n>Phone Notify</div>
+ <div class="well-value">{{hold.phone_notify}}</div>
+ <div class="well-label" i18n>SMS Notify</div>
+ <div class="well-value">{{hold.sms_notify}}</div>
+ </div>
+ <div class="well-row">
+ <div class="well-label" i18n>Cancel Cause</div>
+ <div class="well-value">{{hold.cancel_cause}}</div><!-- TODO: label -->
+ <div class="well-label" i18n>Cancel Time</div>
+ <div class="well-value">{{hold.cancel_time | formatValue:'timestamp'}}</div>
+ <div class="well-label" i18n>Cancel Note</div>
+ <div class="well-value">{{hold.cancel_note}}</div>
+ </div>
+ <div class="well-row">
+ <div class="well-label" i18n>Patron Name</div>
+ <div class="well-value">
+ <a href="/eg/staff/circ/patron/{{hold.usr_id}}/checkout">
+ {{hold.usr_display_name}}
+ </a>
+ </div>
+ <!-- force consistent width -->
+ <div class="well-label" i18n>Patron Barcode</div>
+ <div class="well-value">
+ <a href="/eg/staff/circ/patron/{{hold.usr_id}}/checkout">
+ {{hold.ucard_barcode}}
+ </a>
+ </div>
+ <!-- for balance -->
+ <div class="well-label" i18n></div>
+ <div class="well-label" i18n></div>
+ </div>
+</div>
+
--- /dev/null
+import {Component, OnInit, Input, Output, ViewChild, EventEmitter} from '@angular/core';
+import {Observable, Observer, of} from 'rxjs';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+
+/** Hold details read-only view */
+
+@Component({
+ selector: 'eg-hold-detail',
+ templateUrl: 'detail.component.html'
+})
+export class HoldDetailComponent implements OnInit {
+
+ _holdId: number;
+ @Input() set holdId(id: number) {
+ this._holdId = id;
+ if (this.initDone) {
+ this.fetchHold();
+ }
+ }
+
+ hold: any; // wide hold reference
+ @Input() set wideHold(wh: any) {
+ this.hold = wh;
+ }
+
+ initDone: boolean;
+ @Output() onShowList: EventEmitter<any>;
+
+ constructor(
+ private net: NetService,
+ private org: OrgService,
+ private auth: AuthService,
+ ) {
+ this.onShowList = new EventEmitter<any>();
+ }
+
+ ngOnInit() {
+ this.initDone = true;
+ this.fetchHold();
+ }
+
+ fetchHold() {
+ if (!this._holdId) { return; }
+
+ this.net.request(
+ 'open-ils.circ',
+ 'open-ils.circ.hold.wide_hash.stream',
+ this.auth.token(), {id: this._holdId}
+ ).subscribe(wideHold => {
+ this.hold = wideHold;
+ });
+ }
+
+ getOrgName(id: number) {
+ if (id) {
+ return this.org.get(id).shortname();
+ }
+ }
+
+ showListView() {
+ this.onShowList.emit();
+ }
+}
+
+
--- /dev/null
+<!-- hold grid with jump-off points to detail page and other actions -->
+
+<!-- our on-demand dialogs-->
+<eg-progress-dialog #progressDialog></eg-progress-dialog>
+<eg-hold-transfer-dialog #transferDialog></eg-hold-transfer-dialog>
+<eg-mark-damaged-dialog #markDamagedDialog></eg-mark-damaged-dialog>
+<eg-mark-missing-dialog #markMissingDialog></eg-mark-missing-dialog>
+<eg-hold-retarget-dialog #retargetDialog></eg-hold-retarget-dialog>
+<eg-hold-cancel-dialog #cancelDialog></eg-hold-cancel-dialog>
+<eg-hold-manage-dialog #manageDialog></eg-hold-manage-dialog>
+
+<div class='eg-holds w-100 mt-3'>
+
+ <ng-container *ngIf="mode == 'detail'">
+ <eg-hold-detail [wideHold]="detailHold" (onShowList)="mode='list'">
+ </eg-hold-detail>
+ </ng-container>
+
+ <ng-container *ngIf="mode == 'list'">
+
+ <div class="row" *ngIf="!hidePickupLibFilter">
+ <div class="col-lg-4">
+ <div class="input-group">
+ <div class="input-group-prepend">
+ <div class="input-group-text" i18n>Pickup Library</div>
+ </div>
+ <eg-org-select [initialOrg]="pickupLib" (onChange)="pickupLibChanged($event)">
+ </eg-org-select>
+ </div>
+ </div>
+ </div>
+
+ <eg-grid #holdsGrid [dataSource]="gridDataSource" [sortable]="true"
+ [multiSortable]="true" [persistKey]="persistKey"
+ (onRowActivate)="showDetail($event)">
+
+ <eg-grid-toolbar-action
+ i18n-label label="Show Hold Details" i18n-group group="Hold"
+ (onClick)="showDetails($event)"></eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-label label="Modify Hold(s)" group="Hold" i18n-group
+ (onClick)="showManageDialog($event)">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-label label="Show Last Few Circulations" group="Item" i18n-group
+ (onClick)="showRecentCircs($event)"></eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-label label="Retrieve Patron" group="Patron" i18n-group
+ (onClick)="showPatron($event)">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-group group="Hold" i18n-label label="Transfer To Marked Title"
+ (onClick)="showTransferDialog($event)">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ group="Item" i18n-group i18n-label label="Mark Item Damaged"
+ (onClick)="showMarkDamagedDialog($event)"></eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-group group="Item" i18n-label label="Mark Item Missing"
+ (onClick)="showMarkMissingDialog($event)">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-group group="Hold" i18n-label label="Find Another Target"
+ (onClick)="showRetargetDialog($event)"></eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18-group group="Hold" i18n-label label="Cancel Hold"
+ (onClick)="showCancelDialog($event)"></eg-grid-toolbar-action>
+
+ <eg-grid-column i18n-label label="Hold ID" path='id' [index]="true">
+ </eg-grid-column>
+
+ <ng-template #barcodeTmpl let-hold="row">
+ <a href="/eg/staff/cat/item/{{cp_id}}/summary">
+ {{hold.cp_barcode}}
+ </a>
+ </ng-template>
+ <eg-grid-column i18n-label label="Current Item" name='cp_barcode'
+ [cellTemplate]="barcodeTmpl">
+ </eg-grid-column>
+
+ <eg-grid-column i18n-label label="Patron Barcode"
+ path='ucard_barcode' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Patron alias" path="usr_alias"></eg-grid-column>
+ <eg-grid-column i18n-label label="Request Date"
+ path='request_time' datatype="timestamp"></eg-grid-column>
+ <eg-grid-column i18n-label label="Capture Date" path='capture_time'
+ datatype="timestamp"></eg-grid-column>
+ <eg-grid-column i18n-label label="Available Date" path='shelf_time'
+ datatype="timestamp"></eg-grid-column>
+ <eg-grid-column i18n-label label="Hold Type" path='hold_type'></eg-grid-column>
+ <eg-grid-column i18n-label label="Pickup Library" path='pl_shortname'></eg-grid-column>
+
+ <ng-template #titleTmpl let-hold="row">
+ <a class="no-href" routerLink="/staff/catalog/record/{{hold.record_id}}">
+ {{hold.title}}
+ </a>
+ </ng-template>
+ <eg-grid-column i18n-label label="Title" [hidden]="true"
+ name='title' [cellTemplate]="titleTmpl"></eg-grid-column>
+ <eg-grid-column i18n-label label="Author" path='author'
+ [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Potential Items" path='potentials'>
+ </eg-grid-column>
+ <eg-grid-column i18n-label label="Status" path='status_string'>
+ </eg-grid-column>
+ <eg-grid-column i18n-label label="Queue Position"
+ path='relative_queue_position' [hidden]="true"></eg-grid-column>
+ <eg-grid-column path='usr_id' i18n-label label="User ID" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path='usr_usrname' i18n-label label="Username" [hidden]="true"></eg-grid-column>
+
+ <eg-grid-column path='usr_first_given_name' i18n-label label="First Name" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path='usr_family_name' i18n-label label="Last Name" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path='rusr_id' i18n-label label="Requestor ID" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path='rusr_usrname' i18n-label label="Requestor Username" [hidden]="true"></eg-grid-column>
+
+ <eg-grid-column i18n-label label="Item Status" path="cs_name" [hidden]="true"></eg-grid-column>
+
+ <eg-grid-column path='acnp_label' i18n-label label="CN Prefix" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path='acns_label' i18n-label label="CN Suffix" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path='mvr.*' parent-idl-class="mvr" [hidden]="true"></eg-grid-column>
+
+ <eg-grid-column i18n-label label="Fulfillment Date/Time" path='fulfillment_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Checkin Time" path='checkin_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Return Time" path='return_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Last Targeting Date/Time" path='prev_check_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Expire Time" path='expire_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Hold Cancel Date/Time" path='cancel_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Cancelation note" path='cancel_note' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Hold Target" path='target' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Current Copy" path='current_copy' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Fulfilling Staff" path='fulfillment_staff' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Fulfilling Library" path='fulfillment_lib' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Requesting Library" path='request_lib' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Requesting User" path='requestor' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="User" path='usr' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Selection Library" path='selection_ou' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Item Selection Depth" path='selection_depth' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Holdable Formats (for M-type hold)" path='holdable_formats' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Notifications Phone Number" path='phone_notify' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Notifications SMS Number" path='sms_notify' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Notify by Email?" path='email_notify' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="SMS Carrier" path='sms_carrier' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Currently Frozen" path='frozen' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Activation Date" path='thaw_date' datatype="timestamp" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Top of Queue" path='cut_in_line' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Is Mint Condition" path='mint_condition' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Shelf Expire Time" path='shelf_expire_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Current Shelf Library" path='current_shelf_lib' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Behind Desk" path='behind_desk' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Status" path='hold_status' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Clearable" path='clear_me' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Is Staff-placed Hold" path='is_staff_hold' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Cancelation Cause ID" path='cc_id' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Cancelation Cause" path='cc_label' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Pickup Library" path='pl_shortname'></eg-grid-column>
+ <eg-grid-column i18n-label label="Pickup Library Name" path='pl_name' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Pickup Library Email" path='pl_email' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Pickup Library Phone" path='pl_phone' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Pickup Library Opac Visible" path='pl_opac_visible' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Transit ID" path='tr_id' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Transit Send Time" path='tr_source_send_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Transit Receive Time" path='tr_dest_recv_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Transit Copy" path='tr_target_copy' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Transit Source" path='tr_source' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Transit Destination" path='tr_dest' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Transit Copy Status" path='tr_copy_status' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Transit Hold" path='tr_hold' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Transit Cancel Time" path='tr_cancel_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Hold Note Count" path='note_count' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="User Display Name" path='usr_display_name' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Requestor Username" path='rusr_usrname' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy ID" path='cp_id' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Number on Volume" path='cp_copy_number' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Fine Level" path='cp_fine_level' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Can Circulate" path='cp_circulate' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Deposit Amount" path='cp_deposit_amount' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Is Deposit Required" path='cp_deposit' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Is Reference" path='cp_ref' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Is Holdable" path='cp_holdable' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Price" path='cp_price' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Barcode" path='cp_barcode' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Circulation Modifier" path='cp_circ_modifier' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Circulate as MARC Type" path='cp_circ_as_type' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Precat Dummy Title" path='cp_dummy_title' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Precat Dummy Author" path='cp_dummy_author' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Alert Message (deprecated)" path='cp_alert_message' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy OPAC Visible" path='cp_opac_visible' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Deleted" path='cp_deleted' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Floating Group" path='cp_floating' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Precat Dummy ISBN" path='cp_dummy_isbn' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Status Change Time" path='cp_status_change_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Active Date" path='cp_active_date' datatype="timestamp" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Is Mint Condition" path='cp_mint_condition' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Cost" path='cp_cost' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Status Is Holdable" path='cs_holdable' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Status Is OPAC Visible" path='cs_opac_visible' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Status Is Copy-Active" path='cs_copy_active' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Status Is Deleted" path='cs_restrict_copy_delete' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Status Is Available" path='cs_is_available' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Issuance i18n-label label" path='issuance_label' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Call Number ID" path='cn_id' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="CN i18n-label label" path='cn_label' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="CN i18n-label label Class" path='cn_label_class' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="CN Sort Key" path='cn_label_sortkey' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Part ID" path='p_id' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Part i18n-label label" path='p_label' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Part Sort Key" path='p_label_sortkey' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Part Is Deleted" path='p_deleted' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="CN Full i18n-label label" path='cn_full_label' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Record ID" path='record_id' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Location ID" path='acpl_id' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Location" path='acpl_name' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Location Holdable" path='acpl_holdable' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Location Hold-Verify" path='acpl_hold_verify' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Location OPAC Visible" path='acpl_opac_visible' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Location Can Circulate" path='acpl_circulate' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Location Prefix" path='acpl_label_prefix' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Location Suffix" path='acpl_label_suffix' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Location Checkin Alert" path='acpl_checkin_alert' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Location Is Deleted" path='acpl_deleted' datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Location URL" path='acpl_url' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Location Order" path='copy_location_order_position' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Default Estimated Wait Time" path='default_estimated_wait' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Minimum Estimated Wait Time" path='min_estimated_wait' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Peer Hold Count" path='other_holds' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Total Wait Time" path='total_wait_time' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Notify Count" path='notification_count' [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Last Notify Time" path='last_notification_time' datatype="timestamp" [hidden]="true"></eg-grid-column>
+
+ </eg-grid>
+
+ </ng-container>
+
+</div>
+
+
--- /dev/null
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Observable, Observer, of} from 'rxjs';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {Pager} from '@eg/share/util/pager';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+import {MarkDamagedDialogComponent
+ } from '@eg/staff/share/holdings/mark-damaged-dialog.component';
+import {MarkMissingDialogComponent
+ } from '@eg/staff/share/holdings/mark-missing-dialog.component';
+import {HoldRetargetDialogComponent
+ } from '@eg/staff/share/holds/retarget-dialog.component';
+import {HoldTransferDialogComponent} from './transfer-dialog.component';
+import {HoldCancelDialogComponent} from './cancel-dialog.component';
+import {HoldManageDialogComponent} from './manage-dialog.component';
+
+/** Holds grid with access to detail page and other actions */
+
+@Component({
+ selector: 'eg-holds-grid',
+ templateUrl: 'grid.component.html'
+})
+export class HoldsGridComponent implements OnInit {
+
+ // If either are set/true, the pickup lib selector will display
+ @Input() initialPickupLib: number | IdlObject;
+ @Input() hidePickupLibFilter: boolean;
+
+ // Grid persist key
+ @Input() persistKey: string;
+
+ // How to sort when no sort parameters have been applied
+ // via grid controls. This uses the eg-grid sort format:
+ // [{name: fname, dir: 'asc'}, {name: fname2, dir: 'desc'}]
+ @Input() defaultSort: any[];
+
+ mode: 'list' | 'detail' | 'manage' = 'list';
+ initDone = false;
+ holdsCount: number;
+ pickupLib: IdlObject;
+ gridDataSource: GridDataSource;
+ detailHold: any;
+ editHolds: number[];
+ transferTarget: number;
+ copyStatuses: {[id: string]: IdlObject};
+
+ @ViewChild('holdsGrid') private holdsGrid: GridComponent;
+ @ViewChild('progressDialog')
+ private progressDialog: ProgressDialogComponent;
+ @ViewChild('transferDialog')
+ private transferDialog: HoldTransferDialogComponent;
+ @ViewChild('markDamagedDialog')
+ private markDamagedDialog: MarkDamagedDialogComponent;
+ @ViewChild('markMissingDialog')
+ private markMissingDialog: MarkMissingDialogComponent;
+ @ViewChild('retargetDialog')
+ private retargetDialog: HoldRetargetDialogComponent;
+ @ViewChild('cancelDialog')
+ private cancelDialog: HoldCancelDialogComponent;
+ @ViewChild('manageDialog')
+ private manageDialog: HoldManageDialogComponent;
+
+ // Bib record ID.
+ _recordId: number;
+ @Input() set recordId(id: number) {
+ this._recordId = id;
+ if (this.initDone) { // reload on update
+ this.holdsGrid.reload();
+ }
+ }
+
+ _userId: number;
+ @Input() set userId(id: number) {
+ this._userId = id;
+ if (this.initDone) {
+ this.holdsGrid.reload();
+ }
+ }
+
+ // Include holds canceled on or after the provided date.
+ // If no value is passed, canceled holds are not displayed.
+ _showCanceledSince: Date;
+ @Input() set showCanceledSince(show: Date) {
+ this._showCanceledSince = show;
+ if (this.initDone) { // reload on update
+ this.holdsGrid.reload();
+ }
+ }
+
+ // Include holds fulfilled on or after hte provided date.
+ // If no value is passed, fulfilled holds are not displayed.
+ _showFulfilledSince: Date;
+ @Input() set showFulfilledSince(show: Date) {
+ this._showFulfilledSince = show;
+ if (this.initDone) { // reload on update
+ this.holdsGrid.reload();
+ }
+ }
+
+ constructor(
+ private net: NetService,
+ private org: OrgService,
+ private auth: AuthService
+ ) {
+ this.gridDataSource = new GridDataSource();
+ this.copyStatuses = {};
+ }
+
+ ngOnInit() {
+ this.initDone = true;
+ this.pickupLib = this.org.get(this.initialPickupLib);
+
+ this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
+
+ if (this.defaultSort && sort.length === 0) {
+ // Only use initial sort if sorting has not been modified
+ // by the grid's own sort controls.
+ sort = this.defaultSort;
+ }
+
+ // sorting not currently supported
+ return this.fetchHolds(pager, sort);
+ };
+ }
+
+ pickupLibChanged(org: IdlObject) {
+ this.pickupLib = org;
+ this.holdsGrid.reload();
+ }
+
+ applyFilters(): any {
+ const filters: any = {
+ is_staff_request: true,
+ fulfillment_time: this._showFulfilledSince ?
+ this._showFulfilledSince.toISOString() : null,
+ cancel_time: this._showCanceledSince ?
+ this._showCanceledSince.toISOString() : null,
+ };
+
+ if (this.pickupLib) {
+ filters.pickup_lib =
+ this.org.descendants(this.pickupLib, true);
+ }
+
+ if (this._recordId) {
+ filters.record_id = this._recordId;
+ }
+
+ if (this._userId) {
+ filters.usr_id = this._userId;
+ }
+
+ return filters;
+ }
+
+ fetchHolds(pager: Pager, sort: any[]): Observable<any> {
+
+ // We need at least one filter.
+ if (!this._recordId && !this.pickupLib && !this._userId) {
+ return of([]);
+ }
+
+ const filters = this.applyFilters();
+
+ const orderBy: any = [];
+ sort.forEach(obj => {
+ const subObj: any = {};
+ subObj[obj.name] = {dir: obj.dir, nulls: 'last'};
+ orderBy.push(subObj);
+ });
+
+ let observer: Observer<any>;
+ const observable = new Observable(obs => observer = obs);
+
+ this.progressDialog.open();
+ this.progressDialog.update({value: 0, max: 1});
+ let first = true;
+ let loadCount = 0;
+ this.net.request(
+ 'open-ils.circ',
+ 'open-ils.circ.hold.wide_hash.stream',
+ // Pre-fetch all holds consistent with AngJS version
+ this.auth.token(), filters, orderBy
+ // Alternatively, fetch holds in pages.
+ // this.auth.token(), filters, orderBy, pager.limit, pager.offset
+ ).subscribe(
+ holdData => {
+
+ if (first) { // First response is the hold count.
+ this.holdsCount = Number(holdData);
+ first = false;
+
+ } else { // Subsequent responses are hold data blobs
+
+ this.progressDialog.update(
+ {value: ++loadCount, max: this.holdsCount});
+
+ observer.next(holdData);
+ }
+ },
+ err => {
+ this.progressDialog.close();
+ observer.error(err);
+ },
+ () => {
+ this.progressDialog.close();
+ observer.complete();
+ }
+ );
+
+ return observable;
+ }
+
+ showDetails(rows: any[]) {
+ this.showDetail(rows[0]);
+ }
+
+ showDetail(row: any) {
+ if (row) {
+ this.mode = 'detail';
+ this.detailHold = row;
+ }
+ }
+
+ showManager(rows: any[]) {
+ if (rows.length) {
+ this.mode = 'manage';
+ this.editHolds = rows.map(r => r.id);
+ }
+ }
+
+ handleModify(rowsModified: boolean) {
+ this.mode = 'list';
+
+ if (rowsModified) {
+ // give the grid a chance to render then ask it to reload
+ setTimeout(() => this.holdsGrid.reload());
+ }
+ }
+
+
+
+ showRecentCircs(rows: any[]) {
+ if (rows.length) {
+ const url =
+ '/eg/staff/cat/item/' + rows[0].cp_id + '/circ_list';
+ window.open(url, '_blank');
+ }
+ }
+
+ showPatron(rows: any[]) {
+ if (rows.length) {
+ const url =
+ '/eg/staff/circ/patron/' + rows[0].usr_id + '/checkout';
+ window.open(url, '_blank');
+ }
+ }
+
+ showManageDialog(rows: any[]) {
+ const holdIds = rows.map(r => r.id).filter(id => Boolean(id));
+ if (holdIds.length > 0) {
+ this.manageDialog.holdIds = holdIds;
+ this.manageDialog.open({size: 'lg'}).then(
+ rowsModified => {
+ if (rowsModified) {
+ this.holdsGrid.reload();
+ }
+ },
+ dismissed => {}
+ );
+ }
+ }
+
+ showTransferDialog(rows: any[]) {
+ const holdIds = rows.map(r => r.id).filter(id => Boolean(id));
+ if (holdIds.length > 0) {
+ this.transferDialog.holdIds = holdIds;
+ this.transferDialog.open({}).then(
+ rowsModified => {
+ if (rowsModified) {
+ this.holdsGrid.reload();
+ }
+ },
+ dismissed => {}
+ );
+ }
+ }
+
+ async showMarkDamagedDialog(rows: any[]) {
+ const copyIds = rows.map(r => r.cp_id).filter(id => Boolean(id));
+ if (copyIds.length === 0) { return; }
+
+ let rowsModified = false;
+
+ const markNext = async(ids: number[]) => {
+ if (ids.length === 0) {
+ return Promise.resolve();
+ }
+
+ this.markDamagedDialog.copyId = ids.pop();
+ this.markDamagedDialog.open({size: 'lg'}).then(
+ ok => {
+ if (ok) { rowsModified = true; }
+ return markNext(ids);
+ },
+ dismiss => markNext(ids)
+ );
+ };
+
+ await markNext(copyIds);
+ if (rowsModified) {
+ this.holdsGrid.reload();
+ }
+ }
+
+ showMarkMissingDialog(rows: any[]) {
+ const copyIds = rows.map(r => r.cp_id).filter(id => Boolean(id));
+ if (copyIds.length > 0) {
+ this.markMissingDialog.copyIds = copyIds;
+ this.markMissingDialog.open({}).then(
+ rowsModified => {
+ if (rowsModified) {
+ this.holdsGrid.reload();
+ }
+ },
+ dismissed => {} // avoid console errors
+ );
+ }
+ }
+
+ showRetargetDialog(rows: any[]) {
+ const holdIds = rows.map(r => r.id).filter(id => Boolean(id));
+ if (holdIds.length > 0) {
+ this.retargetDialog.holdIds = holdIds;
+ this.retargetDialog.open({}).then(
+ rowsModified => {
+ if (rowsModified) {
+ this.holdsGrid.reload();
+ }
+ },
+ dismissed => {}
+ );
+ }
+ }
+
+ showCancelDialog(rows: any[]) {
+ const holdIds = rows.map(r => r.id).filter(id => Boolean(id));
+ if (holdIds.length > 0) {
+ this.cancelDialog.holdIds = holdIds;
+ this.cancelDialog.open({}).then(
+ rowsModified => {
+ if (rowsModified) {
+ this.holdsGrid.reload();
+ }
+ },
+ dismissed => {}
+ );
+ }
+ }
+}
+
+
--- /dev/null
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module';
+import {HoldsService} from './holds.service';
+import {HoldsGridComponent} from './grid.component';
+import {HoldDetailComponent} from './detail.component';
+import {HoldManageComponent} from './manage.component';
+import {HoldRetargetDialogComponent} from './retarget-dialog.component';
+import {HoldTransferDialogComponent} from './transfer-dialog.component';
+import {HoldCancelDialogComponent} from './cancel-dialog.component';
+import {HoldManageDialogComponent} from './manage-dialog.component';
+
+@NgModule({
+ declarations: [
+ HoldsGridComponent,
+ HoldDetailComponent,
+ HoldManageComponent,
+ HoldRetargetDialogComponent,
+ HoldTransferDialogComponent,
+ HoldCancelDialogComponent,
+ HoldManageDialogComponent
+ ],
+ imports: [
+ StaffCommonModule,
+ HoldingsModule
+ ],
+ exports: [
+ HoldsGridComponent,
+ HoldDetailComponent,
+ HoldManageComponent,
+ HoldRetargetDialogComponent,
+ HoldTransferDialogComponent,
+ HoldCancelDialogComponent,
+ HoldManageDialogComponent
+ ],
+ providers: [
+ HoldsService
+ ]
+})
+
+export class HoldsModule {}
--- /dev/null
+/**
+ * Common code for mananging holdings
+ */
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs';
+import {map, mergeMap} from 'rxjs/operators';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {EventService, EgEvent} from '@eg/core/event.service';
+import {AuthService} from '@eg/core/auth.service';
+import {BibRecordService,
+ BibRecordSummary} from '@eg/share/catalog/bib-record.service';
+
+// Response from a place-holds API call.
+export interface HoldRequestResult {
+ success: boolean;
+ holdId?: number;
+ evt?: EgEvent;
+}
+
+// Values passed to the place-holds API call.
+export interface HoldRequest {
+ holdType: string;
+ holdTarget: number;
+ recipient: number;
+ requestor: number;
+ pickupLib: number;
+ override?: boolean;
+ notifyEmail?: boolean;
+ notifyPhone?: string;
+ notifySms?: string;
+ smsCarrier?: string;
+ thawDate?: string; // ISO date
+ frozen?: boolean;
+ holdableFormats?: {[target: number]: string};
+ result?: HoldRequestResult;
+}
+
+// A fleshed hold request target object containing whatever data is
+// available for each hold type / target. E.g. a TITLE hold will
+// not have a value for 'volume', but a COPY hold will, since all
+// copies have volumes. Every HoldRequestTarget will have a bibId and
+// bibSummary. Some values come directly from the API call, others
+// applied locally.
+export interface HoldRequestTarget {
+ target: number;
+ metarecord?: IdlObject;
+ bibrecord?: IdlObject;
+ bibId?: number;
+ bibSummary?: BibRecordSummary;
+ part?: IdlObject;
+ volume?: IdlObject;
+ copy?: IdlObject;
+ issuance?: IdlObject;
+ metarecord_filters?: any;
+}
+
+/** Service for performing various hold-related actions */
+
+@Injectable()
+export class HoldsService {
+
+ constructor(
+ private evt: EventService,
+ private net: NetService,
+ private auth: AuthService,
+ private bib: BibRecordService,
+ ) {}
+
+ placeHold(request: HoldRequest): Observable<HoldRequest> {
+
+ let method = 'open-ils.circ.holds.test_and_create.batch';
+ if (request.override) { method = method + '.override'; }
+
+ return this.net.request(
+ 'open-ils.circ', method, this.auth.token(), {
+ patronid: request.recipient,
+ pickup_lib: request.pickupLib,
+ hold_type: request.holdType,
+ email_notify: request.notifyEmail,
+ phone_notify: request.notifyPhone,
+ thaw_date: request.thawDate,
+ frozen: request.frozen,
+ sms_notify: request.notifySms,
+ sms_carrier: request.smsCarrier,
+ holdable_formats_map: request.holdableFormats
+ },
+ [request.holdTarget]
+ ).pipe(map(
+ resp => {
+ let result = resp.result;
+ const holdResult: HoldRequestResult = {success: true};
+
+ // API can return an ID, an array of events, or a hash
+ // of info.
+
+ if (Number(result) > 0) {
+ // On success, the API returns the hold ID.
+ holdResult.holdId = result;
+ console.debug(`Hold successfully placed ${result}`);
+
+ } else {
+ holdResult.success = false;
+ console.info('Hold request failed: ', result);
+
+ if (Array.isArray(result)) { result = result[0]; }
+
+ if (this.evt.parse(result)) {
+ holdResult.evt = this.evt.parse(result);
+ } else {
+ holdResult.evt = this.evt.parse(result.last_event);
+ }
+ }
+
+ request.result = holdResult;
+ return request;
+ }
+ ));
+ }
+
+ getHoldTargetMeta(holdType: string, holdTarget: number | number[],
+ orgId?: number): Observable<HoldRequestTarget> {
+
+ const targetIds = [].concat(holdTarget);
+
+ return this.net.request(
+ 'open-ils.circ',
+ 'open-ils.circ.hold.get_metadata',
+ holdType, targetIds, orgId
+ ).pipe(mergeMap(meta => {
+ const target: HoldRequestTarget = meta;
+ target.bibId = target.bibrecord.id();
+
+ return this.bib.getBibSummary(target.bibId)
+ .pipe(map(sum => {
+ target.bibSummary = sum;
+ return target;
+ }));
+ }));
+ }
+
+ /**
+ * Update a list of holds.
+ * Returns observable of results, one per hold.
+ * Result is either a Number (hold ID) or an EgEvent object.
+ */
+ updateHolds(holds: IdlObject[]): Observable<any> {
+
+ return this.net.request(
+ 'open-ils.circ',
+ 'open-ils.circ.hold.update.batch',
+ this.auth.token(), holds
+ ).pipe(map(response => {
+
+ if (Number(response) > 0) { return Number(response); }
+
+ if (Array.isArray(response)) { response = response[0]; }
+
+ const evt = this.evt.parse(response);
+
+ console.warn('Hold update returned event', evt);
+ return evt;
+ }));
+ }
+}
+
+
+
--- /dev/null
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <ng-container *ngIf="holdIds.length == 1">
+ <h4 class="modal-title" i18n>Modify Hold (#{{holdIds[0]}})</h4>
+ </ng-container>
+ <ng-container *ngIf="holdIds.length > 1">
+ <h4 class="modal-title">Batch Modify {{holdIds.length}} Holds</h4>
+ </ng-container>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="dismiss('cross_click')">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <eg-hold-manage [holdIds]="holdIds" (onComplete)="onComplete($event)">
+ </eg-hold-manage>
+ </div>
+ </ng-template>
\ No newline at end of file
--- /dev/null
+import {Component, OnInit, Input} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+
+/**
+ * Dialog wrapper for ManageHoldsComponent.
+ */
+
+@Component({
+ selector: 'eg-hold-manage-dialog',
+ templateUrl: 'manage-dialog.component.html'
+})
+
+export class HoldManageDialogComponent
+ extends DialogComponent implements OnInit {
+
+ @Input() holdIds: number[];
+
+ constructor(
+ private modal: NgbModal) { // required for passing to parent
+ super(modal); // required for subclassing
+ }
+
+ open(args: NgbModalOptions): Promise<boolean> {
+ return super.open(args);
+ }
+
+ onComplete(changesMade: boolean) {
+ this.close(changesMade);
+ }
+}
+
+
+
--- /dev/null
+
+<form #holdManageForm role="form" *ngIf="hold"
+ class="form-validated common-form striped-odd">
+
+ <div class="form-group row d-flex">
+ <div class="col-lg-2 d-flex">
+ <div class="" *ngIf="isBatch()">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ title="Activate Column Editing" i18n-title
+ name="active_pickup_lib" [(ngModel)]="activeFields.pickup_lib"/>
+ </div>
+ </div>
+ <div class="flex-1"><label i18n>Pickup Library:</label></div>
+ </div>
+ <div class="col-lg-4">
+ <!-- TODO: filter orgs as needed -->
+ <eg-org-select [initialOrgId]="hold.pickup_lib()"
+ [disabled]="isBatch() && !activeFields.pickup_lib"
+ (onChange)="pickupLibChanged($event)">
+ </eg-org-select>
+ </div>
+ <div class="col-lg-2 d-flex">
+ <div class="" *ngIf="isBatch()">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ title="Activate Column Editing" i18n-title
+ name="active_mint_condition" [(ngModel)]="activeFields.mint_condition"/>
+ </div>
+ </div>
+ <div class="flex-1">
+ <label i18n>Desired Item Condition:</label>
+ </div>
+ </div>
+ <div class="col-lg-4">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox" id="mint-condition"
+ name="mint" value="mint"
+ [disabled]="isBatch() && !activeFields.mint_condition"
+ [ngModel]="hold.mint_condition() == 't'"
+ (ngModelChange)="hold.mint_condition($event ? 't' : 'f')">
+ <label class="form-check-label" for="mint-condition">
+ Good Condition Only
+ </label>
+ </div>
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <div class="col-lg-2 d-flex">
+ <div class="" *ngIf="isBatch()">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ title="Activate Column Editing" i18n-title
+ name="active_frozen" [(ngModel)]="activeFields.frozen"/>
+ </div>
+ </div>
+ <div class="flex-1">
+ <label for="frozen" i18n>Hold is Suspended:</label>
+ </div>
+ </div>
+ <div class="col-lg-4">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="frozen" name="frozen"
+ [disabled]="isBatch() && !activeFields.frozen"
+ [ngModel]="hold.frozen() == 't'"
+ (ngModelChange)="hold.frozen($event ? 't' : 'f')">
+ </div>
+ </div>
+ <div class="col-lg-2 d-flex">
+ <div class="" *ngIf="isBatch()">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ title="Activate Column Editing" i18n-title
+ name="active_cut_in_line" [(ngModel)]="activeFields.cut_in_line"/>
+ </div>
+ </div>
+ <div class="flex-1">
+ <label for="cut_in_line" i18n>Top of Queue:</label>
+ </div>
+ </div>
+ <div class="col-lg-4">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ id="cut_in_line" name="cut_in_line"
+ [disabled]="isBatch() && !activeFields.cut_in_line"
+ [ngModel]="hold.cut_in_line() == 't'"
+ (ngModelChange)="hold.cut_in_line($event ? 't' : 'f')">
+ </div>
+ </div>
+ </div>
+
+ <!-- wrap the date mod fields in a border to help
+ differentiate from other fields -->
+ <div class="w-100 border border-primary rounded">
+ <div class="form-group row">
+ <div class="col-lg-2 d-flex">
+ <div class="" *ngIf="isBatch()">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ title="Activate Column Editing" i18n-title
+ name="active_thaw_date" [(ngModel)]="activeFields.thaw_date"/>
+ </div>
+ </div>
+ <div class="flex-1"><label for="thaw_date" i18n>Activate Date:</label></div>
+ </div>
+ <div class="col-lg-4">
+ <eg-date-select
+ domId="thaw_date"
+ [disabled]="isBatch() && !activeFields.thaw_date"
+ (onChangeAsIso)="hold.thaw_date($event)"
+ [initialIso]="hold.thaw_date()">
+ </eg-date-select>
+ </div>
+ <div class="col-lg-2 d-flex">
+ <div class="" *ngIf="isBatch()">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ title="Activate Column Editing" i18n-title
+ name="active_request_time" [(ngModel)]="activeFields.request_time"/>
+ </div>
+ </div>
+ <div class="flex-1"><label for="request_time" i18n>Request Date:</label></div>
+ </div>
+ <div class="col-lg-4">
+ <eg-date-select
+ domId="request_time"
+ [disabled]="isBatch() && !activeFields.request_time"
+ (onChangeAsIso)="hold.request_time($event)"
+ [initialIso]="hold.request_time()">
+ </eg-date-select>
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <div class="col-lg-2 d-flex">
+ <div class="" *ngIf="isBatch()">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ title="Activate Column Editing" i18n-title
+ name="active_expire_time" [(ngModel)]="activeFields.expire_time"/>
+ </div>
+ </div>
+ <div class="flex-1"><label for="expire_time" i18n>Expire Date:</label></div>
+ </div>
+ <div class="col-lg-4">
+ <eg-date-select
+ domId="expire_time"
+ [disabled]="isBatch() && !activeFields.expire_time"
+ (onChangeAsIso)="hold.expire_time($event)"
+ [initialIso]="hold.expire_time()">
+ </eg-date-select>
+ </div>
+ <div class="col-lg-2 d-flex">
+ <div class="" *ngIf="isBatch()">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ title="Activate Column Editing" i18n-title
+ name="active_shelf_expire_time" [(ngModel)]="activeFields.shelf_expire_time"/>
+ </div>
+ </div>
+ <div class="flex-1"><label for="shelf_expire_time" i18n>Shelf Expire Date:</label></div>
+ </div>
+ <div class="col-lg-4">
+ <eg-date-select
+ domId="shelf_expire_time"
+ [disabled]="isBatch() && !activeFields.shelf_expire_time"
+ (onChangeAsIso)="hold.shelf_expire_time($event)"
+ [initialIso]="hold.shelf_expire_time()">
+ </eg-date-select>
+ </div>
+ </div>
+ </div><!-- modify dates group border -->
+
+ <div class="form-group row">
+ <div class="col-lg-2 d-flex">
+ <div class="" *ngIf="isBatch()">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ title="Activate Column Editing" i18n-title
+ name="active_email_notify" [(ngModel)]="activeFields.email_notify"/>
+ </div>
+ </div>
+ <div class="flex-1"><label for="email" i18n>Send Emails:</label></div>
+ </div>
+ <div class="col-lg-4">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox" id="email"
+ name="email" [ngModel]="hold.email_notify() == 't'"
+ [disabled]="isBatch() && !activeFields.email_notify"
+ (ngModelChange)="hold.email_notify($event ? 't' : 'f')"/>
+ </div>
+ </div>
+ <div class="col-lg-2 d-flex">
+ <div class="" *ngIf="isBatch()">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ title="Activate Column Editing" i18n-title
+ name="active_phone_notify" [(ngModel)]="activeFields.phone_notify"/>
+ </div>
+ </div>
+ <div class="flex-1"><label for="phone" i18n>Phone Number:</label></div>
+ </div>
+ <div class="col-lg-4">
+ <input type="text" class="form-control" name="phone" id="phone"
+ placeholder="Phone Number..." i18n-placeholder
+ [disabled]="isBatch() && !activeFields.phone_notify"
+ [ngModel]="hold.phone_notify()"
+ (ngModelChange)="hold.phone_notify($event)"/>
+ </div>
+ </div>
+
+ <ng-container *ngIf="smsEnabled">
+ <div class="form-group row">
+ <div class="col-lg-2 d-flex">
+ <div class="" *ngIf="isBatch()">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ title="Activate Column Editing" i18n-title
+ name="active_sms_notify" [(ngModel)]="activeFields.sms_notify"/>
+ </div>
+ </div>
+ <div class="flex-1"><label for="sms_notify" i18n>Text/SMS Number:</label></div>
+ </div>
+ <div class="col-lg-4">
+ <input type="text" class="form-control" name="sms_notify" id="sms_notify"
+ placeholder="SMS Number..." i18n-placeholder
+ [disabled]="isBatch() && !activeFields.sms_notify"
+ [ngModel]="hold.sms_notify()"
+ (ngModelChange)="hold.sms_notify($event)"/>
+ </div>
+ <div class="col-lg-2 d-flex">
+ <div class="" *ngIf="isBatch()">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="checkbox"
+ title="Activate Column Editing" i18n-title
+ name="active_sms_carrier" [(ngModel)]="activeFields.sms_carrier"/>
+ </div>
+ </div>
+ <div class="flex-1">
+ <label for="sms_carrier" i18n>Text/SMS Number:</label>
+ </div>
+ </div>
+ <div class="col-lg-4">
+ <eg-combobox
+ id="sms_carrier"
+ [disabled]="isBatch() && !activeFields.sms_carrier"
+ (onChange)="hold.sms_carrier($event.id)"
+ [startId]="hold.sms_carrier()"
+ [entries]="smsCarriers"
+ placeholder="SMS Carrier..." i18n-placeholder>
+ </eg-combobox>
+ </div>
+ </div>
+ </ng-container>
+
+
+ <div class="row d-flex justify-content-end">
+ <div>
+ <button type="button" class="btn btn-warning" (click)="exit()" i18n>
+ Cancel
+ </button>
+ <button type="button" class="btn btn-success ml-2" (click)="save()" i18n>
+ Apply
+ </button>
+ </div>
+ </div>
+</form>
+
--- /dev/null
+import {Component, OnInit, Input, Output, ViewChild, EventEmitter} from '@angular/core';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {HoldsService} from './holds.service';
+
+/** Edit holds in single or batch mode. */
+
+@Component({
+ selector: 'eg-hold-manage',
+ templateUrl: 'manage.component.html'
+})
+export class HoldManageComponent implements OnInit {
+
+ // One holds ID means standard edit mode.
+ // >1 hold IDs means batch edit mode.
+ @Input() holdIds: number[];
+
+ hold: IdlObject;
+ smsEnabled: boolean;
+ smsCarriers: ComboboxEntry[];
+ activeFields: {[key: string]: boolean};
+
+ // Emits true if changes were applied to the hold.
+ @Output() onComplete: EventEmitter<boolean>;
+
+ constructor(
+ private idl: IdlService,
+ private org: OrgService,
+ private pcrud: PcrudService,
+ private holds: HoldsService
+ ) {
+ this.onComplete = new EventEmitter<boolean>();
+ this.smsCarriers = [];
+ this.holdIds = [];
+ this.activeFields = {};
+ }
+
+ ngOnInit() {
+ this.org.settings('sms.enable').then(sets => {
+ this.smsEnabled = sets['sms.enable'];
+ if (!this.smsEnabled) { return; }
+
+ this.pcrud.search('csc', {active: 't'}, {order_by: {csc: 'name'}})
+ .subscribe(carrier => {
+ this.smsCarriers.push({
+ id: carrier.id(),
+ label: carrier.name()
+ });
+ });
+ });
+
+ this.fetchHold();
+ }
+
+ fetchHold() {
+ this.hold = null;
+
+ if (this.holdIds.length === 0) {
+ return;
+
+ } else if (this.isBatch()) {
+ // Use a dummy hold to store form values.
+ this.hold = this.idl.create('ahr');
+
+ } else {
+ // Form values are stored in the one hold we're editing.
+ this.pcrud.retrieve('ahr', this.holdIds[0])
+ .subscribe(hold => this.hold = hold);
+ }
+ }
+
+ toFormData() {
+
+ }
+
+ isBatch(): boolean {
+ return this.holdIds.length > 1;
+ }
+
+ pickupLibChanged(org: IdlObject) {
+ if (org) {
+ this.hold.pickup_lib(org.id());
+ }
+ }
+
+ save() {
+ if (this.isBatch()) {
+
+ // Fields with edit-active checkboxes
+ const fields = Object.keys(this.activeFields)
+ .filter(field => this.activeFields[field]);
+
+ const holds: IdlObject[] = [];
+ this.pcrud.search('ahr', {id: this.holdIds})
+ .subscribe(
+ hold => {
+ // Copy form fields to each hold to update.
+ fields.forEach(field => hold[field](this.hold[field]()));
+ holds.push(hold);
+ },
+ err => {},
+ () => {
+ this.saveBatch(holds);
+ }
+ );
+ } else {
+ this.saveBatch([this.hold]);
+ }
+ }
+
+ saveBatch(holds: IdlObject[]) {
+ let successCount = 0;
+ this.holds.updateHolds(holds)
+ .subscribe(
+ res => {
+ if (Number(res) > 0) {
+ successCount++;
+ console.debug('hold update succeeded with ', res);
+ } else {
+ // TODO: toast?
+ }
+ },
+ err => console.error('hold update failed with ', err),
+ () => {
+ if (successCount === holds.length) {
+ this.onComplete.emit(true);
+ } else {
+ // TODO: toast?
+ console.error('Some holds failed to update');
+ }
+ }
+ );
+ }
+
+ exit() {
+ this.onComplete.emit(false);
+ }
+}
+
+
--- /dev/null
+<eg-string #successMsg
+ text="Successfully Retargetd Hold" i18n-text></eg-string>
+<eg-string #errorMsg
+ text="Failed To Retarget Hold" i18n-text></eg-string>
+
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h4 class="modal-title">
+ <span i18n>Retarget Hold</span>
+ </h4>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="dismiss('cross_click')">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <div class="row d-flex justify-content-center">
+ <h5>Retarget {{holdIds.length}} Holds?</h5>
+ </div>
+ <div class="row" *ngIf="numSucceeded > 0">
+ <div class="col-lg-12" i18n>
+ {{numSucceeded}} Hold(s) Successfully Retargeted
+ </div>
+ </div>
+ <div class="row" *ngIf="numFailed > 0">
+ <div class="col-lg-12">
+ <div class="alert alert-warning">
+ {{numFailed}} Hold(s) Failed to Retarget.
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <ng-container *ngIf="!chargeResponse">
+ <button type="button" class="btn btn-warning"
+ (click)="dismiss('canceled')" i18n>Cancel</button>
+ <button type="button" class="btn btn-success"
+ (click)="retargetBatch()" i18n>Retarget</button>
+ </ng-container>
+ </div>
+ </ng-template>
\ No newline at end of file
--- /dev/null
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {AuthService} from '@eg/core/auth.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {StringComponent} from '@eg/share/string/string.component';
+
+
+/**
+ * Dialog for retargeting holds.
+ */
+
+@Component({
+ selector: 'eg-hold-retarget-dialog',
+ templateUrl: 'retarget-dialog.component.html'
+})
+
+export class HoldRetargetDialogComponent
+ extends DialogComponent implements OnInit {
+
+ @Input() holdIds: number | number[];
+ @ViewChild('successMsg') private successMsg: StringComponent;
+ @ViewChild('errorMsg') private errorMsg: StringComponent;
+
+ changesApplied: boolean;
+ numSucceeded: number;
+ numFailed: number;
+
+ constructor(
+ private modal: NgbModal, // required for passing to parent
+ private toast: ToastService,
+ private net: NetService,
+ private evt: EventService,
+ private auth: AuthService) {
+ super(modal); // required for subclassing
+ }
+
+ ngOnInit() {}
+
+ open(args: NgbModalOptions): Promise<boolean> {
+ this.holdIds = [].concat(this.holdIds); // array-ify ints
+ return super.open(args);
+ }
+
+ async retargetNext(ids: number[]): Promise<any> {
+ if (ids.length === 0) {
+ return Promise.resolve();
+ }
+
+ return this.net.request(
+ 'open-ils.circ', 'open-ils.circ.hold.reset',
+ this.auth.token(), ids.pop()
+ ).toPromise().then(
+ async(result) => {
+ if (Number(result) === 1) {
+ this.numSucceeded++;
+ this.toast.success(await this.successMsg.current());
+ } else {
+ this.numFailed++;
+ console.error(this.evt.parse(result));
+ this.toast.warning(await this.errorMsg.current());
+ }
+ this.retargetNext(ids);
+ }
+ );
+ }
+
+ async retargetBatch(): Promise<any> {
+ this.numSucceeded = 0;
+ this.numFailed = 0;
+ const ids = [].concat(this.holdIds);
+ await this.retargetNext(ids);
+ this.close(this.numSucceeded > 0);
+ }
+}
+
+
+
--- /dev/null
+<eg-string #successMsg
+ text="Successfully Transfered Hold" i18n-text></eg-string>
+<eg-string #errorMsg
+ text="Failed To Transfer Hold" i18n-text></eg-string>
+<eg-string #targetNeeded
+ text="Transfer Target Required" i18n-text> </eg-string>
+
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h4 class="modal-title">
+ <span i18n>Transfer Hold(s) To Marked Target</span>
+ </h4>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="dismiss('cross_click')">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <div class="row d-flex justify-content-center">
+ <h5>Transfer {{holdIds.length}} Holds To Record {{transferTarget}}?</h5>
+ </div>
+ <div class="row" *ngIf="numSucceeded > 0">
+ <div class="col-lg-12" i18n>
+ {{numSucceeded}} Hold(s) Successfully Transferred.
+ </div>
+ <div class="row" *ngIf="numFailed > 0">
+ <div class="col-lg-12">
+ <div class="alert alert-warning">
+ {{numFailed}} Hold(s) Failed to Transfer.
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <ng-container *ngIf="!chargeResponse">
+ <button type="button" class="btn btn-warning"
+ (click)="dismiss('canceled')" i18n>Cancel</button>
+ <button type="button" class="btn btn-success"
+ (click)="transferBatch()" i18n>Transfer</button>
+ </ng-container>
+ </div>
+ </ng-template>
\ No newline at end of file
--- /dev/null
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {StoreService} from '@eg/core/store.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {AuthService} from '@eg/core/auth.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {StringComponent} from '@eg/share/string/string.component';
+
+
+/**
+ * Dialog for transferring holds.
+ */
+
+@Component({
+ selector: 'eg-hold-transfer-dialog',
+ templateUrl: 'transfer-dialog.component.html'
+})
+
+export class HoldTransferDialogComponent
+ extends DialogComponent implements OnInit {
+
+ @Input() holdIds: number | number[];
+
+ @ViewChild('successMsg') private successMsg: StringComponent;
+ @ViewChild('errorMsg') private errorMsg: StringComponent;
+ @ViewChild('targetNeeded') private targetNeeded: StringComponent;
+
+ transferTarget: number;
+ changesApplied: boolean;
+ numSucceeded: number;
+ numFailed: number;
+
+ constructor(
+ private modal: NgbModal, // required for passing to parent
+ private toast: ToastService,
+ private store: StoreService,
+ private net: NetService,
+ private evt: EventService,
+ private auth: AuthService) {
+ super(modal); // required for subclassing
+ }
+
+ ngOnInit() {}
+
+ async open(args: NgbModalOptions): Promise<boolean> {
+ this.holdIds = [].concat(this.holdIds); // array-ify ints
+
+ this.transferTarget =
+ this.store.getLocalItem('eg.circ.hold.title_transfer_target');
+
+ if (!this.transferTarget) {
+ this.toast.warning(await this.targetNeeded.current());
+ return Promise.reject('Transfer Target Required');
+ }
+
+ return super.open(args);
+ }
+
+ async transferHolds(): Promise<any> {
+ return this.net.request(
+ 'open-ils.circ',
+ 'open-ils.circ.hold.change_title.specific_holds',
+ this.auth.token(), this.transferTarget, this.holdIds
+ ).toPromise().then(async(result) => {
+ if (Number(result) === 1) {
+ this.numSucceeded++;
+ this.toast.success(await this.successMsg.current());
+ } else {
+ this.numFailed++;
+ console.error('Retarget Failed', this.evt.parse(result));
+ this.toast.warning(await this.errorMsg.current());
+ }
+ });
+ }
+
+ async transferBatch(): Promise<any> {
+ this.numSucceeded = 0;
+ this.numFailed = 0;
+ await this.transferHolds();
+ this.close(this.numSucceeded > 0);
+ }
+}
+
+
+
.flex-4 {flex: 4}
.flex-5 {flex: 5}
+/** BS deprecated the well, but it's replacement is not quite the same.
+ * Define our own version and expand it to a full "table".
+ * */
+.well-row {
+ display: flex;
+}
+.well-table .well-label {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ margin: 4px;
+ padding: 4px;
+ min-height: 40px;
+}
+
+.well-table .well-value {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ background-color: #f5f5f5;
+ border-radius: 5px;
+ box-shadow: inset 0 1px 1px rgba(0,0,0,.05);
+ padding: 4px;
+ margin: 4px;
+ min-height: 40px;
+}
+
/* usefuf for mat-icon buttons without any background or borders */
.material-icon-button {