LPXXX new headings getting started
authorBill Erickson <berickxx@gmail.com>
Fri, 28 Apr 2023 14:49:19 +0000 (10:49 -0400)
committerBill Erickson <berickxx@gmail.com>
Fri, 28 Apr 2023 14:49:19 +0000 (10:49 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/staff/cat/authority/heading-detail.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/cat/authority/heading-detail.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/cat/authority/new-headings.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/cat/authority/new-headings.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/cat/authority/routing.module.ts

diff --git a/Open-ILS/src/eg2/src/app/staff/cat/authority/heading-detail.component.html b/Open-ILS/src/eg2/src/app/staff/cat/authority/heading-detail.component.html
new file mode 100644 (file)
index 0000000..4ee8e3f
--- /dev/null
@@ -0,0 +1,113 @@
+
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title">
+      <span i18n>Heading Details</span>
+    </h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" (click)="close()">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <table id='info-table'>
+      <tbody>
+
+        <tr>
+          <td class='label' i18n>Previous Heading</td>
+          <td><span class='text-danger'>{{heading.prev_entry_value()}}</span></td>
+        </tr>
+
+        <tr>
+          <td class='label' i18n>Previous Indexed As</td>
+          <td><span>{{heading.prev_field_label()}}</span></td>
+          <td class='label' i18n>Previous TCN</td>
+          <td><span>{{heading.prev_bib_record()}}</span></td>
+        </tr>
+
+        <tr>
+          <td class='label' i18n>Previous Authority Tag</td>
+          <td><span>{{heading.prev_auth_tag()}}</span></td>
+          <td class='label' i18n>Previous Browse Entry ID</td>
+          <td><span>{{heading.prev_entry()}}</span></td>
+        </tr>
+
+        <tr><td colspan='4'><hr/></td></tr>
+
+        <tr>
+          <td class='label' i18n>New Heading</td>
+          <td><span class='text-danger'>{{heading.entry_value()}}</span>
+        </tr>
+
+        <tr>
+          <td class='label' i18n>Heading Date</td>
+          <td><span>{{heading.heading_date() | date:'short'}}</span></td>
+          <td class='label' i18n>Indexed As</td>
+          <td><span>{{heading.field_label()}}</span></td>
+        </tr>
+
+        <tr>
+          <td class='label' i18n>From TCN</td>
+          <td><span>{{heading.bib_record()}}</span></td>
+          <td class='label' i18n>Browse Entry ID</td>
+          <td><span>{{heading.entry()}}</span></td>
+        </tr>
+
+        <tr>
+          <td class='label' i18n>Heading Create Date</td>
+          <td><span>{{heading.entry_create_date() | date:'short'}}</span></td>
+          <td class='label' i18n>Bib Record Edit Date</td>
+          <td><span>{{heading.bib_edit_date() | date:'short'}}</span></td>
+        </tr>
+
+        <tr>
+          <td class='label' i18n>Bib Record Cataloging Date</td>
+          <td><span>{{heading.bib_cataloging_date() | date:'short'}}</span></td>
+          <td class='label' i18n>Bib Record Create Date</td>
+          <td><span>{{heading.bib_create_date() | date:'short'}}</span></td>
+        </tr>
+
+        <tr>
+          <td class='label' i18n>Bib Record Editor Username</td>
+          <td><span>{{heading.bib_editor_usrname()}}</span></td>
+        </tr>
+
+        <tr>
+          <td class='label' i18n>From 1XX</td>
+          <td colspan='3'><span>{{heading.bib_marc_1xx()}}</span></td>
+        </tr>
+
+        <tr>
+          <td class='label' i18n>From 245</td>
+          <td colspan='3'><span>{{heading.bib_marc_245()}}</span></td>
+        </tr>
+
+        <tr><td colspan='4'><hr/></td></tr>
+
+        <tr>
+          <td class='label' i18n>Next Heading</td>
+          <td><span class='text-danger'>{{heading.next_entry_value()}}</span>
+        </tr>
+
+        <tr>
+          <td class='label' i18n>Next Indexed As</td>
+          <td><span>{{heading.next_field_label()}}</span></td>
+          <td class='label' i18n>Next TCN</td>
+          <td><span>{{heading.next_bib_record()}}</span></td>
+        </tr>
+
+        <tr>
+          <td class='label' i18n>Next Authority Tag</td>
+          <td><span>{{heading.next_auth_tag()}}</span></td>
+          <td class='label' i18n>Next Browse Entry ID</td>
+          <td><span>{{heading.next_entry()}}</span></td>
+        </tr>
+
+      </tbody>
+    </table>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-warning" (click)="close()" i18n>Close</button>
+  </div>
+</ng-template>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/authority/heading-detail.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/authority/heading-detail.component.ts
new file mode 100644 (file)
index 0000000..58d8215
--- /dev/null
@@ -0,0 +1,28 @@
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {IdlObject} from '@eg/core/idl.service';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+
+@Component({
+  selector: 'eg-heading-detail-dialog',
+  templateUrl: 'heading-detail.component.html',
+  styles: [
+    `td { padding:3px 8px 3px 8px; font-size: 98% }`,
+    `.label {
+        font-weight: bold;
+        white-space : nowrap;
+    }`
+
+  ]
+})
+export class HeadingDetailComponent extends DialogComponent {
+
+    heading: IdlObject;
+
+    constructor(
+        private modal: NgbModal // required for passing to parent
+    ) {
+        super(modal); // required for subclassing
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/authority/new-headings.component.html b/Open-ILS/src/eg2/src/app/staff/cat/authority/new-headings.component.html
new file mode 100644 (file)
index 0000000..cd6f591
--- /dev/null
@@ -0,0 +1,191 @@
+<eg-staff-banner bannerText="New Headings" i18n-bannerText></eg-staff-banner>
+
+<eg-heading-detail-dialog #detailDialog></eg-heading-detail-dialog>
+
+<ng-template #headingTemplate let-axis="axis" let-heading="heading">
+  <div class="card-body">
+    <table>
+      <tbody>
+        <tr>
+          <td class="pr-2 label" i18n>Previous:</td>
+          <td>
+            <span i18n>
+              <span *ngIf="heading.prev_bib_record()">B: </span>
+              <span *ngIf="!heading.prev_bib_record()">A: </span>
+              {{heading.prev_entry_value()}}
+            </span>
+            <ng-container *ngIf="heading.prev_auth_tag()">
+              <span class="pl-2" i18n>(From {{heading.prev_auth_tag()}})</span>
+            </ng-container>
+          </td>
+        </tr>
+        <tr>
+          <td class="pr-2 label" i18n>New:</td>
+          <td>
+            <a target="_blank" routerLink="/staff/catalog/browse"
+              [queryParams]="{browseTerm: heading.entry_value(), browseClass: axis}">
+              {{heading.entry_value()}}
+            </a>
+            <a href="javascript:;" class="pl-3" (click)="openDetailDialog(heading)">
+              (details)
+            </a>
+          </td>
+        </tr>
+        <tr>
+          <td class="pr-2 label" i18n>Next:</td>
+          <td>
+            <span i18n>
+              <span *ngIf="heading.next_bib_record()">B: </span>
+              <span *ngIf="!heading.next_bib_record()">A: </span>
+              {{heading.next_entry_value()}}
+            </span>
+            <ng-container *ngIf="heading.next_auth_tag()">
+              <span class="pl-2" i18n>(From {{heading.next_auth_tag()}})</span>
+            </ng-container>
+          </td>
+        </tr>
+        <tr>
+          <td class="pr-2 label" i18n>From TCN:</td>
+          <td>
+            <a target="_blank" routerLink="/staff/catalog/record/{{heading.bib_record()}}">
+              {{heading.bib_record()}}
+            </a>
+            <span class="pl-2 label" i18n>Indexed As:</span>
+            <span>{{heading.field_label()}}</span>
+          </td>
+        </tr>
+        <tr>
+          <td class="pr-2 label" i18n>Edited By:</td>
+          <td>
+            <span>{{heading.bib_editor_usrname()}}</span>
+            <span class="pl-2 label" i18n>Heading Date:</span>
+            <span>{{heading.heading_date() | date:'shortDate'}}</span>
+          </td>
+        </tr>
+        <tr>
+          <td class="pr-2 label" i18n>From 1XX:</td>
+          <td>{{heading.bib_marc_1xx()}}</td>
+        </tr>
+        <tr>
+          <td class="pr-2 label" i18n>From 245:</td>
+          <td>{{heading.bib_marc_245()}}</td>
+        </tr>
+        <tr>
+          <td class="pr-2 label" i18n>Format:</td>
+          <td>{{mattypeLabel(heading.mattype())}}</td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+</ng-template>
+
+<div class="row d-flex">
+  <div class="flex-1">
+
+    <div class="row form-inline border border-primary p-2 pr-3 mr-3 sticky-top-with-nav bg-white">
+      <label class="mr-2" i18n>Start Date: </label>
+      <eg-date-select #startDateSelect [(ngModel)]="startDate"></eg-date-select> 
+
+      <label class="ml-2 mr-2" i18n>End Date: </label>
+      <eg-date-select #endDateSelect [(ngModel)]="endDate"></eg-date-select> 
+
+      <button class="ml-2 btn btn-outline-dark" (click)="getHeadings(true)" i18n>Go</button>
+
+      <span class="border-left border-dark pl-3 ml-3">
+        <button class="btn btn-outline-dark" (click)="prevPage()" 
+          [disabled]="pager.isFirstPage()" i18n>Previous Page</button>
+      </span>
+
+      <label class="ml-3 mr-3" i18n>Page {{pager.currentPage()}}</label>
+
+      <button class="btn btn-outline-dark" (click)="nextPage()" 
+        [disabled]="pager.isLastPage()" i18n>Next Page</button>
+    </div>
+
+    <div class="row mt-3" *ngIf="loading">
+      <div class="col-lg-6 offset-lg-3">
+        <eg-progress-inline></eg-progress-inline>
+      </div>
+    </div>
+
+    <div class="row mt-3" 
+      *ngIf="!loading && hasLoaded && headings.length === 0">
+      <div class="col-lg-6 offset-lg-3">
+        <div class="w-100 alert alert-info" i18n>No headings found</div>
+      </div>
+    </div>
+
+    <div class="mt-3 pb-3" *ngIf="!loading">
+
+      <ng-container *ngFor="let heading of headingsByAxisForPage('author'); let idx = index">
+        <div class="card tight-card">
+          <div *ngIf="idx === 0" class="card-header font-weight-bold bg-info" i18n>New Author Headings</div>
+          <ng-container
+            *ngTemplateOutlet="headingTemplate;context:{heading:heading,axis:'author'}">
+          </ng-container>
+        </div>
+      </ng-container>
+
+      <ng-container *ngFor="let heading of headingsByAxisForPage('subject'); let idx = index">
+        <div class="card tight-card">
+        <div *ngIf="idx === 0" class="card-header font-weight-bold bg-info" i18n>New Subject Headings</div>
+          <ng-container
+            *ngTemplateOutlet="headingTemplate;context:{heading:heading,axis:'subject'}">
+          </ng-container>
+        </div>
+      </ng-container>
+
+      <ng-container *ngFor="let heading of headingsByAxisForPage('series'); let idx = index">
+        <div class="card tight-card">
+          <div *ngIf="idx === 0" class="card-header font-weight-bold bg-info" i18n>New Series Headings</div>
+          <ng-container
+            *ngTemplateOutlet="headingTemplate;context:{heading:heading,axis:'series'}">
+          </ng-container>
+        </div>
+      </ng-container>
+
+    </div>
+  </div>
+
+  <div>
+    <div class="sticky-top-with-nav bg-white">
+    <div class="mt-2 p-2 border border-info rounded">
+      <h3 i18n>Exclude These Editors</h3>
+      <div class="w-100 justify-content-right mattype-list">
+        <eg-combobox #usrCbox [entries]="usrCboxEntries" required="true"
+          (onChange)="usrChanged($event)"
+          [selectedId]="usrId" [asyncDataSource]="usrCboxSource">
+        </eg-combobox>
+      </div>
+      <div class="w-100 justify-content-right mattype-list">
+        <div class="mt-1" *ngFor="let entry of excludeUsers">
+          <button class="btn-sm btn-danger" (click)="removeExcludedUser(entry.id)">X</button>
+          <span class="ml-2">{{entry.label}}</span>
+        </div>
+      </div>
+    </div>
+    <div class="mt-2 p-2 border border-info rounded">
+      <div class="form-check">
+        <input class="form-check-input" type="checkbox" 
+          [(ngModel)]="exclude001ODN" id="exclude-001-odn"/>
+        <label class="form-check-label" for="exclude-001-odn" i18n>
+          Exclude 001 ODN Records
+        </label>
+      </div>
+    </div>
+    <div class="mt-2 p-2 border border-info rounded">
+      <h3 i18n>Exclude These Formats</h3>
+      <div class="w-100 justify-content-right mattype-list">
+        <ng-container *ngFor="let mat of mattypes">
+          <div class="form-check">
+            <input class="form-check-input" type="checkbox" 
+              [(ngModel)]="selectedMattypes[mat.code()]" id="mattype-{{mat.code()}}"/>
+            <label class="form-check-label" for="mattype-{{mat.code()}}">
+              {{mat.value()}}</label>
+          </div>
+        </ng-container>
+      </div>
+    </div>
+    </div>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/authority/new-headings.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/authority/new-headings.component.ts
new file mode 100644 (file)
index 0000000..87144d3
--- /dev/null
@@ -0,0 +1,241 @@
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Observable, empty} from 'rxjs';
+import {map, switchMap} from 'rxjs/operators';
+import {IdlObject} from '@eg/core/idl.service';
+import {Pager} from '@eg/share/util/pager';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {ComboboxEntry, ComboboxComponent} from '@eg/share/combobox/combobox.component';
+import {StringComponent} from '@eg/share/string/string.component';
+import {HeadingDetailComponent} from './heading-detail.component';
+import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
+import {DateUtil} from '@eg/share/util/date';
+
+/* New Headings Report */
+
+// Force a minimum start date for new headings to avoid reporting on
+// (practically) all headings, which occurs when the start date preceeds
+// or includes the SQL deployment date, which stamps a create_date on
+// every heading to NOW().  Such queries cause heavy load and eventually
+// time out anyway.
+// NOTE: using English dates instead of ISO dates since English
+// dates tell Date.parse() to use the local time zone instead of UTC.
+const MIN_START_DATE = new Date(Date.parse('December 5, 2016 00:00:00'));
+
+// Grab and cache this many, plus the first entry of the would-be next
+// batch to test whether a next batch exists.
+const PRE_FETCH_COUNT = 201;
+
+@Component({
+  templateUrl: 'new-headings.component.html',
+  styles: [
+    `.card {border-bottom: 2px dashed grey}`,
+    `.tight-card:nth-child(odd) { background-color: rgb(23,162,184,0.2); }`,
+    `.tight-card { font-size: 98%; }`,
+    `.mattype-list { font-size: 98%; }`,
+    `.label { white-space : nowrap; }`,
+    `.card-body {font-family: 'Lucida Console', Monaco, monospace; font-size: 94%}`
+  ]
+})
+export class NewHeadingsComponent implements OnInit {
+
+    pager: Pager;
+    startDate: Date;
+    endDate: Date;
+    mattypes: IdlObject[] = [];
+    selectedMattypes: any = {};
+    loading = false;
+    hasLoaded = false; // true after first search
+    headings = [];
+    usrId: number;
+    usrCboxSource: (term: string) => Observable<ComboboxEntry>;
+    usrCboxEntries: ComboboxEntry[];
+    excludeUsers: ComboboxEntry[] = [];
+    exclude001ODN = false;
+
+    @ViewChild('usrCbox') usrCbox: ComboboxComponent;
+    @ViewChild('startDateSelect') startDateSelect: DateSelectComponent;
+    @ViewChild('endDateSelect') endDateSelect: DateSelectComponent;
+    @ViewChild('detailDialog') detailDialog: HeadingDetailComponent;
+
+    constructor(
+        private net: NetService,
+        private org: OrgService,
+        private pcrud: PcrudService
+    ) {
+        this.pager = new Pager();
+        this.pager.limit = 50;
+    }
+
+
+    ngOnInit() {
+
+        this.usrCboxSource = term => {
+            if (term.length < 2) { return empty(); }
+
+            const filter: any = {deleted: 'f', active: 't'};
+            filter.usrname = {'ilike': `%${term}%`};
+
+            return this.pcrud.search('au', filter, {
+                order_by: {au: 'usrname'},
+                limit: 50 // Avoid huge lists
+            }
+            ).pipe(map(user => {
+                return {id: user.id(), label: user.usrname()};
+            }));
+        };
+
+        // seed the report with yesterday's date.
+        const d = new Date();
+        d.setDate(d.getDate() - 1);
+        this.startDate = d;
+        this.endDate = new Date(d);
+
+        this.pcrud.search('ccvm',
+            {ctype: 'mattype'}, {order_by: {ccvm: 'value'}})
+        .subscribe(mt => this.mattypes.push(mt));
+    }
+
+    usrChanged(entry: ComboboxEntry) {
+        if (entry && entry.id) {
+            // TODO avoid double entries
+            this.excludeUsers.push(entry);
+            this.usrCbox.selectedId = null;
+        }
+    }
+
+    removeExcludedUser(id: number) {
+        let users: ComboboxEntry[] = [];
+        this.excludeUsers.forEach(e => {
+            if (e.id !== id) {
+                users.push(e);
+            }
+        });
+        this.excludeUsers = users;
+    }
+
+    openDetailDialog(heading: IdlObject) {
+
+        this.detailDialog.heading = heading;
+        this.detailDialog.open({size: 'lg'});
+    }
+
+    headingsByAxisForPage(axis: string): IdlObject[] {
+        const subset = this.headings.slice(
+            this.pager.offset, this.pager.offset + this.pager.limit);
+
+        return subset.filter(heading => heading.browse_axis() === axis);
+    }
+
+    getHeadings(isNew?: boolean) {
+        this.loading = true;
+
+        if (isNew) {
+            this.headings = [];
+            this.pager.reset();
+        }
+
+        let counter = 0;
+        this.pcrud.search('rcbed', this.compileQueryFilter(), {
+            limit: PRE_FETCH_COUNT,
+            offset: this.pager.offset
+        }).subscribe(
+            heading => {
+                counter++;
+                this.headings.push(heading);
+            },
+            err => {},
+            () => {
+                this.loading = false;
+                this.hasLoaded = true;
+
+                console.debug(`Fetched ${counter} headings`);
+
+                if (counter < PRE_FETCH_COUNT) {
+                    this.pager.resultCount = this.headings.length;
+
+                } else {
+                    // Drop the final heading since it's the first
+                    // heading for the next batch.
+                    this.headings.length--;  // neato
+                }
+            }
+        );
+    }
+
+    mattypeLabel(code: string): string {
+        const mattype = this.mattypes.filter(mt => mt.code() === code)[0];
+        if (mattype) { return mattype.value(); }
+        return '';
+    }
+
+    compileQueryFilter(): any {
+
+        if (!this.startDate || this.startDate < MIN_START_DATE) {
+            console.log('Selected start date ' + this.startDate + ' is too early. '
+                + 'Using min start date ' + MIN_START_DATE + ' instead');
+
+            // clone the date since it will get clobbered by getYMD.
+            this.startDate = MIN_START_DATE;
+        }
+
+        const startDate = this.startDateSelect.currentAsYmd();
+        let endDate;
+
+        if (this.endDate) {
+            // the end date has to be extended by one day, since the between
+            // query cuts off at midnight (0 hour) on the end date, which
+            // would miss all headings for that date created after hour 0.
+            // note: setDate() will rollover to the next month when needed.
+            endDate = new Date(this.endDate);
+            endDate.setDate(endDate.getDate() + 1);
+            endDate = DateUtil.localYmdFromDate(endDate);
+        }
+
+        const filter: any = {};
+
+        if (startDate && endDate) {
+            // use -and instead of BETWEEN so that endDate is not inclusive.
+            filter['-and'] = [
+                {heading_date : {'>=' : startDate}},
+                {heading_date : {'<' : endDate}}
+            ];
+        } else if (startDate) {
+            filter.heading_date = {'>=' : startDate};
+        } else {
+            filter.heading_date = {'<' : endDate};
+        }
+
+        const matTypes = Object.keys(this.selectedMattypes)
+            .filter(m => this.selectedMattypes[m] === true);
+
+        if (matTypes.length > 0) {
+            filter.mattype = {'not in': matTypes};
+        }
+
+        if (this.excludeUsers.length > 0) {
+            filter.bib_editor = {'not in': this.excludeUsers.map(e => e.id)};
+        }
+
+        if (this.exclude001ODN) {
+            filter.bib_marc_001 = {'!~': '^ODN'};
+        }
+
+        return filter;
+    }
+
+    prevPage() {
+       // Previous pages will always be cached.
+       this.pager.decrement();
+    }
+
+    nextPage() {
+        this.pager.increment();
+        if (!this.headings[this.pager.offset] && this.pager.resultCount === null) {
+            this.getHeadings();
+        }
+    }
+}
+
+
index b3fcf36..36d6c36 100644 (file)
@@ -19,6 +19,9 @@ const routes: Routes = [{
   }, {
     path: 'manage/:id/:tab',
     component: ManageAuthorityComponent
+  }, {
+    path: 'new-headings',
+    component: NewHeadingsComponent
 }];
 
 @NgModule({