this.altList = this.altList.concat(list);
}
} else {
- if (4 & displayCode) return; // bitflag 4 == hide on checkin
+ if (4 & displayCode) { return; } // bitflag 4 == hide on checkin
this.altList = this.altList.concat(list);
}
}
import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module';
import {CircService} from './circ.service';
import {CircGridComponent} from './grid.component';
+import {DueDateDialogComponent} from './due-date-dialog.component';
@NgModule({
declarations: [
- CircGridComponent
+ CircGridComponent,
+ DueDateDialogComponent
],
imports: [
StaffCommonModule,
--- /dev/null
+<eg-string #successMsg text="Successfully Modified Due Date" i18n-text></eg-string>
+<eg-string #errorMsg text="Failed To Modify Due Date" i18n-text></eg-string>
+
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h4 class="modal-title">
+ <span i18n>Modify Due Date</span>
+ </h4>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="close()">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <h5 i18n>Modifying Due Date For {{circs.length}} Circulation(s)</h5>
+
+ <div class="row mt-3">
+ <div class="col-lg-3" i18n>New Date</div>
+ <div class="col-lg-9">
+ <eg-datetime-select [required]="true" (onChangeAsIso)="dueDateChange($event)">
+ </eg-datetime-select>
+ </div>
+ </div>
+
+ <div class="row mt-3" *ngIf="!dueDateIso">
+ <div class="col-lg-12 alert-danger" i18n>
+ Selected due date is not valid.
+ </div>
+ </div>
+
+ <div class="row mt-3" *ngIf="numSucceeded > 0">
+ <div class="col-lg-12" i18n>
+ {{numSucceeded}} Due Date(s) Successfully Modified
+ </div>
+ </div>
+ <div class="row mt-3" *ngIf="numFailed > 0">
+ <div class="col-lg-12">
+ <div class="alert alert-warning">
+ {{numFailed}} Due Date(s) Failed to Modify
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Cancel</button>
+ <button type="button" class="btn btn-success" [disabled]="!dueDateIso"
+ (click)="modifyBatch()" i18n>Modify</button>
+ </div>
+ </ng-template>
--- /dev/null
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs';
+import {IdlObject} from '@eg/core/idl.service';
+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 modifying circulation due dates. */
+
+@Component({
+ selector: 'eg-due-date-dialog',
+ templateUrl: 'due-date-dialog.component.html'
+})
+
+export class DueDateDialogComponent
+ extends DialogComponent implements OnInit {
+
+ @Input() circs: IdlObject[] = [];
+ @ViewChild('successMsg', { static: true }) private successMsg: StringComponent;
+ @ViewChild('errorMsg', { static: true }) private errorMsg: StringComponent;
+
+ dueDateIsValid = false;
+ dueDateIso: string;
+ numSucceeded: number;
+ numFailed: number;
+ nowTime: number;
+
+ 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
+ }
+
+ ngOnInit() {
+ this.onOpen$.subscribe(_ => {
+ this.numSucceeded = 0;
+ this.numFailed = 0;
+ this.dueDateIso = new Date().toISOString();
+ this.nowTime = new Date().getTime();
+ });
+ }
+
+ dueDateChange(iso: string) {
+ if (iso && Date.parse(iso) > this.nowTime) {
+ this.dueDateIso = iso;
+ } else {
+ this.dueDateIso = null;
+ }
+ }
+
+ modifyBatch() {
+ if (!this.dueDateIso) { return; }
+
+ let promise = Promise.resolve();
+
+ this.circs.forEach(circ => {
+ promise = promise.then(_ => this.modifyOne(circ));
+ });
+
+ promise.then(_ => {
+ this.close();
+ this.circs = [];
+ });
+ }
+
+ modifyOne(circ: IdlObject): Promise<any> {
+ return this.net.request(
+ 'open-ils.circ',
+ 'open-ils.circ.circulation.due_date.update',
+ this.auth.token(), circ.id(), this.dueDateIso
+
+ ).toPromise().then(modCirc => {
+
+ const evt = this.evt.parse(modCirc);
+
+ if (evt) {
+ this.numFailed++;
+ console.error(evt);
+ } else {
+ this.numSucceeded++;
+ this.respond(modCirc);
+ }
+ });
+ }
+}
<eg-progress-dialog #progressDialog></eg-progress-dialog>
<eg-copy-alerts-dialog #copyAlertsDialog></eg-copy-alerts-dialog>
+<eg-string #overdueString i18n-text text="Overdue"></eg-string>
+<eg-due-date-dialog #dueDateDialog></eg-due-date-dialog>
<ng-template #titleTemplate let-r="row">
<ng-container *ngIf="r.record">
</ng-container>
<ng-container *ngIf="!r.record">{{r.title}}</ng-container>
</ng-template>
+<ng-template #barcodeTemplate let-r="row">
+ <ng-container *ngIf="r.copy">
+ <a href="/eg/staff/cat/item/{{r.copy.id()}}">{{r.copy.barcode()}}</a>
+ </ng-container>
+</ng-template>
+
<eg-grid #circGrid [dataSource]="gridDataSource" [sortable]="true"
+ [rowFlairIsEnabled]="true" [rowFlairCallback]="rowFlair"
+ [rowClassCallback]="rowClass"
[useLocalSort]="true" [cellTextGenerator]="cellTextGenerator"
[disablePaging]="true" [persistKey]="persistKey">
(onClick)="openItemAlerts($event, 'create')">
</eg-grid-toolbar-action>
+ <eg-grid-toolbar-action
+ i18n-group group="Edit" i18n-label label="Edit Due Date"
+ (onClick)="editDueDate($event)">
+ </eg-grid-toolbar-action>
+
<eg-grid-column [index]="true" path="index" [hidden]="true"
label="Row Index" i18n-label></eg-grid-column>
<eg-grid-column path="dueDate" label="Due Date" i18n-label
datatype="timestamp"></eg-grid-column>
- <eg-grid-column path="copy.barcode" label="Barcode" i18n-label></eg-grid-column>
+ <eg-grid-column path="copy.barcode" label="Barcode" i18n-label
+ [cellTemplate]="barcodeTemplate"></eg-grid-column>
<eg-grid-column path="title" label="Title" i18n-label
[cellTemplate]="titleTemplate"></eg-grid-column>
import {PcrudService} from '@eg/core/pcrud.service';
import {CheckoutParams, CheckoutResult, CircService} from './circ.service';
import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
-import {GridDataSource, GridColumn, GridCellTextGenerator} from '@eg/share/grid/grid';
+import {GridDataSource, GridColumn, GridCellTextGenerator,
+ GridRowFlairEntry} from '@eg/share/grid/grid';
import {GridComponent} from '@eg/share/grid/grid.component';
import {Pager} from '@eg/share/util/pager';
import {StoreService} from '@eg/core/store.service';
} from '@eg/staff/share/holdings/copy-alerts-dialog.component';
import {ArrayUtil} from '@eg/share/util/array';
import {PrintService} from '@eg/share/print/print.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {DueDateDialogComponent} from './due-date-dialog.component';
export interface CircGridEntry {
index: string; // class + id -- row index
dueDate?: string;
copyAlertCount?: number;
nonCatCount?: number;
+
+ // useful for reporting precaculated values and avoiding
+ // repetitive date creation on grid render.
+ overdue?: boolean;
}
const CIRC_FLESH_DEPTH = 4;
entries: CircGridEntry[] = null;
gridDataSource: GridDataSource = new GridDataSource();
cellTextGenerator: GridCellTextGenerator;
+ rowFlair: (row: CircGridEntry) => GridRowFlairEntry;
+ rowClass: (row: CircGridEntry) => string;
+
+ nowDate: number = new Date().getTime();
+ @ViewChild('overdueString') private overdueString: StringComponent;
@ViewChild('circGrid') private circGrid: GridComponent;
@ViewChild('copyAlertsDialog')
private copyAlertsDialog: CopyAlertsDialogComponent;
+ @ViewChild('dueDateDialog') private dueDateDialog: DueDateDialogComponent;
constructor(
private org: OrgService,
// The grid never fetches data directly.
// The caller is responsible initiating all data loads.
this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
- if (this.entries) {
- return from(this.entries);
- } else {
- return empty();
- }
+ return this.entries ? from(this.entries) : empty();
};
this.cellTextGenerator = {
- title: row => row.title
+ title: row => row.title,
+ barcode: row => row.copy ? row.copy.barcode() : ''
+ };
+
+ this.rowFlair = (row: CircGridEntry) => {
+ if (this.circIsOverdue(row)) {
+ return {icon: 'error_outline', title: this.overdueString.text};
+ }
+ };
+
+ this.rowClass = (row: CircGridEntry) => {
+ if (this.circIsOverdue(row)) {
+ return 'less-intense-alert';
+ }
};
}
);
}
+ getCopies(rows: any): IdlObject[] {
+ return rows.filter(r => r.copy).map(r => r.copy);
+ }
+
+ getCircs(rows: any): IdlObject[] {
+ return rows.filter(r => r.circ).map(r => r.circ);
+ }
+
printReceipts(rows: any) {
if (rows.length > 0) {
this.printer.print({
});
}
}
+
+ editDueDate(rows: any) {
+ const circs = this.getCircs(rows);
+ if (circs.length === 0) { return; }
+
+ let refreshNeeded = false;
+ this.dueDateDialog.circs = circs;
+ this.dueDateDialog.open().subscribe(
+ circ => {
+ refreshNeeded = true;
+ const row = rows.filter(r => r.circ.id() === circ.id())[0];
+ row.circ.due_date(circ.due_date());
+ row.dueDate = circ.due_date();
+ delete row.overdue; // it will recalculate
+ },
+ err => console.error(err),
+ () => {
+ if (refreshNeeded) {
+ this.reloadGrid();
+ }
+ }
+ );
+ }
+
+ circIsOverdue(row: CircGridEntry): boolean {
+ const circ = row.circ;
+
+ if (!circ) { return false; } // noncat
+
+ if (row.overdue === undefined) {
+ row.overdue = (Date.parse(circ.due_date()) < this.nowDate);
+ }
+ return row.overdue;
+ }
}
width: 4em;
}
+/*
+ * Created initially for styled grid rows where full 'bg-danger' CSS is
+ * intense and not especially readable, more so when rows are stacked.
+ * http://web-accessibility.carnegiemuseums.org/design/color/
+ */
+.less-intense-alert {
+ background-color: #f9dede;
+ color: black;
+}