From f4751c036172894b18661acf1df71b4a0505684e Mon Sep 17 00:00:00 2001
From: ievavold <ievavold@wisc.edu>
Date: Wed, 21 Aug 2019 09:58:04 -0500
Subject: [PATCH] ROENROLL-1918 show spinner while audit in progress

---
 .../dars-audit-view.component.ts              | 20 ++--
 .../dars/dars-view/dars-view.component.html   |  6 +-
 src/app/dars/dars-view/dars-view.component.ts | 70 +++++++++++---
 .../metadata-table.component.html             | 27 +++---
 .../metadata-table.component.scss             |  5 +-
 .../metadata-table.component.ts               | 74 +++++++-------
 src/app/dars/models/audit-metadata.ts         | 10 +-
 src/app/dars/services/api.service.ts          |  8 +-
 src/app/dars/store/actions.ts                 | 78 ++++++++-------
 src/app/dars/store/effects.ts                 | 94 +++++++++---------
 src/app/dars/store/reducer.ts                 | 96 +++++++++++++++----
 src/app/dars/store/selectors.ts               | 38 +++++++-
 src/app/dars/store/state.ts                   | 11 ++-
 src/app/dars/store/utils.ts                   | 53 ++++++++++
 14 files changed, 397 insertions(+), 193 deletions(-)
 create mode 100644 src/app/dars/store/utils.ts

diff --git a/src/app/dars/dars-audit-view/dars-audit-view.component.ts b/src/app/dars/dars-audit-view/dars-audit-view.component.ts
index e57f115..96995a6 100644
--- a/src/app/dars/dars-audit-view/dars-audit-view.component.ts
+++ b/src/app/dars/dars-audit-view/dars-audit-view.component.ts
@@ -12,8 +12,11 @@ import { AuditStatus, MetadataStatus } from '../store/state';
 import { Store } from '@ngrx/store';
 import { GlobalState } from '@app/core/state';
 import * as selectors from '../store/selectors';
-import { StartLoadingAudit, StartLoadingMetadata } from '../store/actions';
-import { AuditMetadata } from '../models/audit-metadata';
+import { StartLoadingAudit, StartLoadingDARSView } from '../store/actions';
+import {
+  AuditMetadata,
+  AuditMetadataWithReportId,
+} from '../models/audit-metadata';
 
 @Component({
   selector: 'cse-dars-audit-view',
@@ -23,7 +26,7 @@ import { AuditMetadata } from '../models/audit-metadata';
 export class AuditViewComponent implements OnInit {
   public metadataStatus$: Observable<MetadataStatus['status']>;
   public darsDegreeAuditReportId$: Observable<number>;
-  public metadata$: Observable<AuditMetadata>;
+  public metadata$: Observable<AuditMetadataWithReportId>;
   public audit$: Observable<AuditStatus>;
 
   constructor(
@@ -44,7 +47,7 @@ export class AuditViewComponent implements OnInit {
 
     this.metadataStatus$.subscribe(metadataStatus => {
       if (metadataStatus === 'NotLoaded') {
-        this.store.dispatch(new StartLoadingMetadata());
+        this.store.dispatch(new StartLoadingDARSView());
       }
     });
 
@@ -65,11 +68,12 @@ export class AuditViewComponent implements OnInit {
       this.store.select(selectors.whatIfMetadata),
     ]).pipe(
       map(([darsDegreeAuditReportId, programMetadata, whatIfMetadata]) => {
-        return programMetadata.concat(whatIfMetadata).find(md => {
-          return md.darsDegreeAuditReportId === darsDegreeAuditReportId;
-        });
+        return (
+          programMetadata[darsDegreeAuditReportId] ||
+          whatIfMetadata[darsDegreeAuditReportId]
+        );
       }),
-      filter((metadata): metadata is AuditMetadata => !!metadata),
+      filter((metadata): metadata is AuditMetadataWithReportId => !!metadata),
       shareReplay(),
     );
 
diff --git a/src/app/dars/dars-view/dars-view.component.html b/src/app/dars/dars-view/dars-view.component.html
index 22fa601..69390e8 100644
--- a/src/app/dars/dars-view/dars-view.component.html
+++ b/src/app/dars/dars-view/dars-view.component.html
@@ -28,8 +28,9 @@
 
         <div class="dars-audit-group" >
           <cse-dars-metadata-table
+            [waiting]="programWaiting$"
             [metadata]="programMetadata$"
-            [type]="'normal'"
+            [type]="'program'"
             [title]="'Degree Audit'"
             [tagline]="'See the progress towards your current academic plan/program and degree plans.'"
             [button]="'Run new degree audit'"
@@ -39,8 +40,9 @@
 
         <div class="dars-audit-group">
           <cse-dars-metadata-table
+            [waiting]="whatIfWaiting$"
             [metadata]="whatIfMetadata$"
-            [type]="'what-if'"
+            [type]="'whatIf'"
             [title]="'&lsquo;What if&rsquo; Audit'"
             [tagline]="'See the progress towards a new academic plan/program and degree plans.'"
             [button]="'Run new &lsquo;what if&rsquo; audit'"
diff --git a/src/app/dars/dars-view/dars-view.component.ts b/src/app/dars/dars-view/dars-view.component.ts
index cfbf577..2ae6026 100644
--- a/src/app/dars/dars-view/dars-view.component.ts
+++ b/src/app/dars/dars-view/dars-view.component.ts
@@ -1,13 +1,13 @@
 import { StartSendingAudit } from './../store/actions';
 import { MediaMatcher } from '@angular/cdk/layout';
-import { Component, OnInit } from '@angular/core';
-import { AuditMetadata } from '../models/audit-metadata';
-import { MatDialog } from '@angular/material';
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { AuditMetadata, AuditMetadataMap } from '../models/audit-metadata';
+import { MatDialog, MatSnackBar } from '@angular/material';
 import { DARSState } from '../store/state';
 import { Store } from '@ngrx/store';
 import { GlobalState } from '@app/core/state';
 import * as selectors from '../store/selectors';
-import { Observable } from 'rxjs';
+import { Observable, Subscription } from 'rxjs';
 import * as darsActions from '../store/actions';
 import { Alert } from '@app/core/models/alert';
 import {
@@ -18,40 +18,82 @@ import {
   NewWhatIfAuditDialogComponent,
   NewWhatIfAuditFields,
 } from '../new-what-if-audit-dialog/new-what-if-audit-dialog.component';
-import { shareReplay } from 'rxjs/operators';
+import { shareReplay, distinctUntilChanged, map } from 'rxjs/operators';
+import { WebsocketService } from '@app/shared/services/websocket.service';
+
+const flattenAndSortMetadataMap = (metadataMap: AuditMetadataMap) => {
+  return (Object.values(metadataMap) as AuditMetadata[]).sort((a, b) => {
+    if (a.darsAuditRunDate > b.darsAuditRunDate) {
+      return 1;
+    } else if (a.darsAuditRunDate < b.darsAuditRunDate) {
+      return -1;
+    } else {
+      return 0;
+    }
+  });
+};
 
 @Component({
   selector: 'cse-dars-view',
   templateUrl: './dars-view.component.html',
   styleUrls: ['./dars-view.component.scss'],
 })
-export class DARSViewComponent implements OnInit {
+export class DARSViewComponent implements OnInit, OnDestroy {
   public metadataStatus$: Observable<DARSState['metadata']['status']>;
+  public programWaiting$: Observable<{ outstanding: number; pending: number }>;
   public programMetadata$: Observable<AuditMetadata[]>;
+  public whatIfWaiting$: Observable<{ outstanding: number; pending: number }>;
   public whatIfMetadata$: Observable<AuditMetadata[]>;
   public mobileView: MediaQueryList;
   public alerts$: Observable<Alert[]>;
+  public messageSub: Subscription;
 
   constructor(
     private store: Store<GlobalState>,
     public dialog: MatDialog,
     public mediaMatcher: MediaMatcher,
+    websocketService: WebsocketService,
+    private snackBar: MatSnackBar,
   ) {
     this.mobileView = mediaMatcher.matchMedia('(max-width: 959px)');
+    this.messageSub = websocketService.messages.subscribe(message => {
+      const newMessage = JSON.parse(message);
+      if (newMessage && newMessage.type === 'auditStatus') {
+        this.store.dispatch(
+          new darsActions.RefreshMetadata({
+            callback: () => this.snackBar.open('Audit Complete'),
+          }),
+        );
+      }
+    });
   }
 
   public ngOnInit() {
-    this.store.dispatch(new darsActions.StartLoadingMetadata());
+    this.store.dispatch(new darsActions.StartLoadingDARSView());
     this.metadataStatus$ = this.store.select(selectors.metadataStatus);
-    this.programMetadata$ = this.store
-      .select(selectors.programMetadata)
-      .pipe(shareReplay());
-    this.whatIfMetadata$ = this.store
-      .select(selectors.whatIfMetadata)
-      .pipe(shareReplay());
+    this.programWaiting$ = this.store.select(
+      selectors.outstandingAndPendingPrograms,
+    );
+    this.programMetadata$ = this.store.select(selectors.programMetadata).pipe(
+      distinctUntilChanged(),
+      map(flattenAndSortMetadataMap),
+      shareReplay(),
+    );
+    this.whatIfWaiting$ = this.store.select(
+      selectors.outstandingAndPendingWhatIf,
+    );
+    this.whatIfMetadata$ = this.store.select(selectors.whatIfMetadata).pipe(
+      distinctUntilChanged(),
+      map(flattenAndSortMetadataMap),
+      shareReplay(),
+    );
     this.alerts$ = this.store.select(selectors.alerts);
   }
 
+  public ngOnDestroy() {
+    this.messageSub.unsubscribe();
+  }
+
   public onDismissAlert(key: string) {
     this.store.dispatch(new darsActions.DismissAlert({ key }));
   }
@@ -64,6 +106,7 @@ export class DARSViewComponent implements OnInit {
         if (event) {
           return this.store.dispatch(
             new StartSendingAudit({
+              auditType: 'program',
               darsInstitutionCode: event.darsInstitutionCode,
               darsDegreeProgramCode: event.darsDegreeProgramCode,
               degreePlannerPlanName: event.degreePlannerPlanName,
@@ -82,6 +125,7 @@ export class DARSViewComponent implements OnInit {
         if (event) {
           return this.store.dispatch(
             new StartSendingAudit({
+              auditType: 'whatIf',
               darsInstitutionCode: event.darsInstitutionCode,
               darsDegreeProgramCode: event.darsDegreeProgramCode,
               degreePlannerPlanName: event.degreePlannerPlanName,
diff --git a/src/app/dars/metadata-table/metadata-table.component.html b/src/app/dars/metadata-table/metadata-table.component.html
index d481d30..ec54d65 100644
--- a/src/app/dars/metadata-table/metadata-table.component.html
+++ b/src/app/dars/metadata-table/metadata-table.component.html
@@ -11,11 +11,11 @@
     <div *ngIf="(metadata | async).length <= 0" class="no-audits">
       <ng-container [ngSwitch]="type">
 
-        <ng-container *ngSwitchCase="'normal'">
+        <ng-container *ngSwitchCase="'program'">
           <p>Stay on top of your required credits and courses by running a degree audit that compares your academic record with your declared program requirements.</p>
         </ng-container>
 
-        <ng-container *ngSwitchCase="'what-if'">
+        <ng-container *ngSwitchCase="'whatIf'">
           <p>Still deciding on a program of study? A What-If audit can help you determine your academic progress towards any degree.</p>
         </ng-container>
 
@@ -57,7 +57,7 @@
           <div>Run {{audit.darsAuditRunDate | date:'M/d/yy \'at\' h:mm a'}}</div>
         </div>
         <ng-template #loading>
-          <p class="in-progress">Audit in progress <mat-progress-spinner mode="indeterminate" diameter="24"></mat-progress-spinner></p>
+          <p class="in-progress">Audit in progress</p>
         </ng-template>
       </td>
     </ng-container>
@@ -66,14 +66,19 @@
     <ng-container matColumnDef="actions">
       <th mat-header-cell *matHeaderCellDef scope="col">Actions</th>
       <td mat-cell *matCellDef="let audit">
-        <!-- TODO: While an audit is in progress the route is null which causes an error '|| 0' is a working fix -->
-        <a class="mat-stroked-button mat-primary view-audit-btn" attr.aria-label="View full audit report for {{(audit.darsDegreeProgramDescription || '').toLowerCase()}}  Program of Study" [routerLink]="['/dars', audit.darsDegreeAuditReportId || 0]">View</a>
-        <a mat-stroked-button color="primary" class="download-audit-pdf-btn" href="/api/darspdfservice?reportId={{ audit?.darsDegreeAuditReportId }}" target="_blank">
-          <mat-icon
-          matTooltip="Download PDF"
-          matTooltipPosition="right">vertical_align_bottom</mat-icon>
-          <span class="cdk-visually-hidden">Download Audit PDF for {{(audit.darsDegreeProgramDescription || '').toLowerCase()}} Program of Study</span>
-        </a>
+        <ng-container *ngIf="audit.darsDegreeAuditReportId; else loading">
+          <!-- TODO: While an audit is in progress the route is null which causes an error '|| 0' is a working fix -->
+          <a class="mat-stroked-button mat-primary view-audit-btn" attr.aria-label="View full audit report for {{(audit.darsDegreeProgramDescription || '').toLowerCase()}}  Program of Study" [routerLink]="['/dars', audit.darsDegreeAuditReportId || 0]">View</a>
+          <a mat-stroked-button color="primary" class="download-audit-pdf-btn" href="/api/darspdfservice?reportId={{ audit?.darsDegreeAuditReportId }}" target="_blank">
+            <mat-icon
+              matTooltip="Download PDF"
+              matTooltipPosition="right">vertical_align_bottom</mat-icon>
+            <span class="cdk-visually-hidden">Download Audit PDF for {{(audit.darsDegreeProgramDescription || '').toLowerCase()}} Program of Study</span>
+          </a>
+        </ng-container>
+        <ng-template #loading>
+          <mat-progress-spinner mode="indeterminate" diameter="24"></mat-progress-spinner>
+        </ng-template>
       </td>
     </ng-container>
 
diff --git a/src/app/dars/metadata-table/metadata-table.component.scss b/src/app/dars/metadata-table/metadata-table.component.scss
index e9ae13f..66dadd6 100644
--- a/src/app/dars/metadata-table/metadata-table.component.scss
+++ b/src/app/dars/metadata-table/metadata-table.component.scss
@@ -45,12 +45,9 @@ $black: #000000;
     }
   }
   .in-progress {
+    margin-bottom: 0;
     color: mat-color($my-app-primary);
     font-weight: 500;
-    mat-progress-spinner {
-      display: inline-block;
-      top: 7px;
-    }
   }
 
   .mat-paginator {
diff --git a/src/app/dars/metadata-table/metadata-table.component.ts b/src/app/dars/metadata-table/metadata-table.component.ts
index c69e009..7b1fb28 100644
--- a/src/app/dars/metadata-table/metadata-table.component.ts
+++ b/src/app/dars/metadata-table/metadata-table.component.ts
@@ -1,7 +1,3 @@
-import { StartLoadingMetadata } from './../store/actions';
-import { Store } from '@ngrx/store';
-import { GlobalState } from '@app/core/state';
-import { WebsocketService } from './../../shared/services/websocket.service';
 import {
   Component,
   Input,
@@ -9,20 +5,20 @@ import {
   EventEmitter,
   ViewChild,
   OnInit,
-  OnDestroy,
 } from '@angular/core';
-import { MatSnackBar } from '@angular/material/snack-bar';
 import { AuditMetadata } from '../models/audit-metadata';
 import { MatPaginator } from '@angular/material/paginator';
 import { MatTableDataSource } from '@angular/material/table';
-import { Observable, Subscription } from 'rxjs';
+import { Observable, combineLatest } from 'rxjs';
 import { PageEvent } from '@angular/material/paginator';
 
-export class AuditStatusMessage {
-  status: string;
-  jobId: string;
-  netId: string;
-  type: string;
+interface MinimumAuditMetadata {
+  darsAuditRunDate: AuditMetadata['darsAuditRunDate'];
+  darsDegreeAuditReportId: AuditMetadata['darsDegreeAuditReportId'];
+  darsHonorsOptionDescription: AuditMetadata['darsHonorsOptionDescription'];
+  darsInstitutionCodeDescription: AuditMetadata['darsInstitutionCodeDescription'];
+  darsStatusOfDegreeAuditRequest: AuditMetadata['darsStatusOfDegreeAuditRequest'];
+  degreePlannerPlanName: AuditMetadata['degreePlannerPlanName'];
 }
 
 @Component({
@@ -30,9 +26,10 @@ export class AuditStatusMessage {
   templateUrl: './metadata-table.component.html',
   styleUrls: ['./metadata-table.component.scss'],
 })
-export class DarsMetadataTableComponent implements OnInit, OnDestroy {
+export class DarsMetadataTableComponent implements OnInit {
+  @Input() public waiting: Observable<{ outstanding: number; pending: number }>;
   @Input() public metadata: Observable<AuditMetadata[]>;
-  @Input() public type: string;
+  @Input() public type: 'program' | 'whatIf';
   @Input() public title: string;
   @Input() public tagline: string;
   @Input() public button: string;
@@ -42,9 +39,7 @@ export class DarsMetadataTableComponent implements OnInit, OnDestroy {
 
   @ViewChild(MatPaginator) paginator: MatPaginator;
   public pageEvent: PageEvent;
-  public dataSource: MatTableDataSource<AuditMetadata>;
-  public messageSub: Subscription;
-  public newMessage: AuditStatusMessage;
+  public dataSource: MatTableDataSource<MinimumAuditMetadata>;
   public displayedColumns = [
     'school',
     'program',
@@ -54,21 +49,7 @@ export class DarsMetadataTableComponent implements OnInit, OnDestroy {
     'actions',
   ];
 
-  constructor(
-    private websocketService: WebsocketService,
-    private store: Store<GlobalState>,
-    private snackBar: MatSnackBar,
-  ) {
-    this.messageSub = websocketService.messages.subscribe(message => {
-      this.newMessage = JSON.parse(message);
-      if (this.newMessage && this.newMessage.type === 'auditStatus') {
-        this.store.dispatch(new StartLoadingMetadata());
-        this.snackBar.open('Audit Complete');
-      }
-    });
-  }
-
-  public static sortMetadata(metadata: AuditMetadata[]): AuditMetadata[] {
+  public static sortMetadata(metadata: MinimumAuditMetadata[]) {
     return metadata.sort((a, b) => {
       const aDate = new Date(a.darsAuditRunDate);
       const bDate = new Date(b.darsAuditRunDate);
@@ -84,14 +65,27 @@ export class DarsMetadataTableComponent implements OnInit, OnDestroy {
   }
 
   public ngOnInit() {
-    this.metadata.subscribe(metadata => {
-      const sorted = DarsMetadataTableComponent.sortMetadata(metadata);
-      this.dataSource = new MatTableDataSource<AuditMetadata>(sorted);
-      this.dataSource.paginator = this.paginator;
-    });
-  }
+    combineLatest([this.waiting, this.metadata]).subscribe(
+      ([waiting, metadata]) => {
+        const sorted = DarsMetadataTableComponent.sortMetadata(metadata);
+
+        const totalWaiting = waiting.outstanding + waiting.pending;
+        const synthetic = Array.from({ length: totalWaiting }).map(() => ({
+          darsAuditRunDate: '',
+          darsDegreeAuditReportId: null,
+          darsHonorsOptionDescription: '',
+          darsInstitutionCodeDescription: '',
+          darsStatusOfDegreeAuditRequest: '',
+          degreePlannerPlanName: '',
+        }));
+
+        this.dataSource = new MatTableDataSource<MinimumAuditMetadata>([
+          ...synthetic,
+          ...sorted,
+        ]);
 
-  public ngOnDestroy() {
-    this.messageSub.unsubscribe();
+        this.dataSource.paginator = this.paginator;
+      },
+    );
   }
 }
diff --git a/src/app/dars/models/audit-metadata.ts b/src/app/dars/models/audit-metadata.ts
index c49cca8..4c7b3db 100644
--- a/src/app/dars/models/audit-metadata.ts
+++ b/src/app/dars/models/audit-metadata.ts
@@ -9,8 +9,16 @@ export interface AuditMetadata {
   darsStatusOfDegreeAuditRequest: string;
   darsHonorsOptionCode: string;
   darsHonorsOptionDescription: string;
-  darsDegreeAuditReportId: number;
+  darsDegreeAuditReportId: number | null;
   darsCatalogYearTerm: string;
   whichEnrolledCoursesIncluded: string;
   degreePlannerPlanName: string;
 }
+
+export type AuditMetadataWithReportId = AuditMetadata & {
+  darsDegreeAuditReportId: number;
+};
+
+export interface AuditMetadataMap {
+  [darsDegreeAuditReportId: number]: AuditMetadataWithReportId;
+}
diff --git a/src/app/dars/services/api.service.ts b/src/app/dars/services/api.service.ts
index 26a0736..0b384b7 100644
--- a/src/app/dars/services/api.service.ts
+++ b/src/app/dars/services/api.service.ts
@@ -7,8 +7,6 @@ import { StudentDegreeProgram } from '../models/student-degree-program';
 import { environment } from './../../../environments/environment';
 import { Audit } from '../models/audit/audit';
 import { CourseBase } from '@app/core/models/course';
-import { delay } from 'rxjs/operators';
-const auditResponse: Audit = require('../../../assets/mock-data/audit-response.json');
 const degreeProgramsResponse: DegreePrograms = require('../../../assets/mock-data/degreeprograms-response.json');
 
 const HTTP_OPTIONS = {
@@ -59,7 +57,7 @@ export class DarsApiService {
    */
   public getAudits(): Observable<AuditMetadata[]> {
     const url = `${environment.apiDarsUrl}/auditmetadata`;
-    return this.http.get<AuditMetadata[]>(url, HTTP_OPTIONS).pipe(delay(5000));
+    return this.http.get<AuditMetadata[]>(url, HTTP_OPTIONS);
   }
 
   /**
@@ -78,9 +76,9 @@ export class DarsApiService {
     darsDegreeProgramCode: string,
     degreePlannerPlanName?: string,
     whichEnrolledCoursesIncluded?: string,
-  ): Observable<AuditMetadata> {
+  ): Observable<{ darsJobId: string }> {
     const url = `${environment.apiDarsUrl}/auditrequest`;
-    return this.http.post<AuditMetadata>(
+    return this.http.post<{ darsJobId: string }>(
       url,
       {
         darsInstitutionCode,
diff --git a/src/app/dars/store/actions.ts b/src/app/dars/store/actions.ts
index 1100c81..f269ce3 100644
--- a/src/app/dars/store/actions.ts
+++ b/src/app/dars/store/actions.ts
@@ -1,80 +1,89 @@
 import { DARSState } from '@app/dars/store/state';
 import { Action } from '@ngrx/store';
-import { AuditMetadata } from '../models/audit-metadata';
+import {
+  AuditMetadata,
+  AuditMetadataWithReportId,
+} from '../models/audit-metadata';
 import { Audit } from '../models/audit/audit';
-import { SingleAuditRequest } from '../models/single-audit-request';
 
 export enum DarsActionTypes {
-  ErrorLoadingMetadata = '[DARS] Error Loading Metadata',
-  StartLoadingMetadata = '[DARS] Start Loading Metadata',
-  DoneLoadingMetadata = '[DARS] Done Loading Metadata',
-  AddAuditMetadata = '[DARS] Add Audit Metadata',
+  ErrorLoadingDARSView = '[DARS] Error Loading DARS View',
+  StartLoadingDARSView = '[DARS] Start Loading DARS View',
+  DoneLoadingDARSView = '[DARS] Done Loading DARS View',
+
+  RefreshMetadata = '[DARS] Refresh Metadata',
+  DoneRefreshingMetadata = '[DARS] Done Refreshing Metadata',
+  ErrorRefreshingMetadata = '[DARS] Error Refreshing Metadata',
 
   ErrorLoadingAudit = '[DARS] Error Loading Audit',
   StartLoadingAudit = '[DARS] Start Loading Audit',
   DoneLoadingAudit = '[DARS] Done Loading Audit',
-  PopulateDarsState = '[DARS] Done Loading state',
 
-  ErrorSendingAudit = 'DARS Error Sending Audit',
+  ErrorSendingAudit = '[DARS] Error Sending Audit',
   StartSendingAudit = '[DARS] Start Sending Audit',
   DoneSendingAudit = '[DARS] Done Sending Audit',
 
   DismissAlert = '[DARS] Dismiss Alert',
 }
 
-export class ErrorLoadingMetadata implements Action {
-  public readonly type = DarsActionTypes.ErrorLoadingMetadata;
+export class ErrorLoadingDARSView implements Action {
+  public readonly type = DarsActionTypes.ErrorLoadingDARSView;
   constructor(public payload: { message: string }) {}
 }
 
-export class StartLoadingMetadata implements Action {
-  public readonly type = DarsActionTypes.StartLoadingMetadata;
+export class StartLoadingDARSView implements Action {
+  public readonly type = DarsActionTypes.StartLoadingDARSView;
 }
 
-export class DoneLoadingMetadata implements Action {
-  public readonly type = DarsActionTypes.DoneLoadingMetadata;
+export class DoneLoadingDARSView implements Action {
+  public readonly type = DarsActionTypes.DoneLoadingDARSView;
   constructor(public payload: DARSState) {}
 }
 
-export class ErrorLoadingAudit implements Action {
-  public readonly type = DarsActionTypes.ErrorLoadingAudit;
-  constructor(public payload: { message: string }) {}
+export class RefreshMetadata implements Action {
+  public readonly type = DarsActionTypes.RefreshMetadata;
+  constructor(public payload?: { callback: () => void }) {}
 }
 
-export class AddAuditMetadata implements Action {
-  public readonly type = DarsActionTypes.AddAuditMetadata;
-  constructor(
-    public payload: {
-      programMetadata: AuditMetadata[];
-      whatIfMetadata: AuditMetadata[];
-    },
-  ) {}
+export class DoneRefreshingMetadata implements Action {
+  public readonly type = DarsActionTypes.DoneRefreshingMetadata;
+  constructor(public payload: AuditMetadata[]) {}
 }
 
-export class PopulateDarsState implements Action {
-  public readonly type = DarsActionTypes.PopulateDarsState;
-  constructor(public payload: Partial<DARSState>) {}
+export class ErrorRefreshingMetadata implements Action {
+  public readonly type = DarsActionTypes.ErrorRefreshingMetadata;
+  constructor(public payload: { message: string }) {}
+}
+
+export class ErrorLoadingAudit implements Action {
+  public readonly type = DarsActionTypes.ErrorLoadingAudit;
+  constructor(public payload: { message: string }) {}
 }
 
 export class StartLoadingAudit implements Action {
   public readonly type = DarsActionTypes.StartLoadingAudit;
-  constructor(public payload: AuditMetadata) {}
+  constructor(public payload: AuditMetadataWithReportId) {}
 }
 
 export class DoneLoadingAudit implements Action {
   public readonly type = DarsActionTypes.DoneLoadingAudit;
-  constructor(public payload: { metadata: AuditMetadata; audit: Audit }) {}
+  constructor(
+    public payload: { metadata: AuditMetadataWithReportId; audit: Audit },
+  ) {}
 }
 
 export class ErrorSendingAudit implements Action {
   public readonly type = DarsActionTypes.ErrorSendingAudit;
-  constructor(public payload: { message: string }) {}
+  constructor(
+    public payload: { auditType: 'program' | 'whatIf'; message: string },
+  ) {}
 }
 
 export class StartSendingAudit implements Action {
   public readonly type = DarsActionTypes.StartSendingAudit;
   constructor(
     public payload: {
+      auditType: 'program' | 'whatIf';
       darsInstitutionCode: string;
       darsDegreeProgramCode: string;
       degreePlannerPlanName?: string;
@@ -86,12 +95,7 @@ export class StartSendingAudit implements Action {
 export class DoneSendingAudit implements Action {
   public readonly type = DarsActionTypes.DoneSendingAudit;
   constructor(
-    public payload: {
-      darsInstitutionCode: string;
-      darsDegreeProgramCode: string;
-      degreePlannerPlanName?: string;
-      whichEnrolledCoursesIncluded?: string;
-    },
+    public payload: { auditType: 'program' | 'whatIf'; darsJobId: string },
   ) {}
 }
 
diff --git a/src/app/dars/store/effects.ts b/src/app/dars/store/effects.ts
index 581c21f..f8d06be 100644
--- a/src/app/dars/store/effects.ts
+++ b/src/app/dars/store/effects.ts
@@ -6,22 +6,14 @@ import { Injectable } from '@angular/core';
 import { Actions, Effect, ofType } from '@ngrx/effects';
 import { DarsActionTypes } from '@app/dars/store/actions';
 import * as darsActions from '@app/dars/store/actions';
-import { flatMap, map, catchError, withLatestFrom } from 'rxjs/operators';
+import { flatMap, map, catchError, withLatestFrom, tap } from 'rxjs/operators';
 import { DarsApiService } from '../services/api.service';
 import { Alert, DarsDisclaimerAlert } from '@app/core/models/alert';
 import { DegreePlannerApiService } from '@app/degree-planner/services/api.service';
-import { AuditMetadata } from '../models/audit-metadata';
-import { SingleAuditRequest } from '../models/single-audit-request';
-import { StudentDegreeProgram } from '../models/student-degree-program';
 import { of } from 'rxjs';
 import * as selectors from '@app/dars/store/selectors';
-
-const isFromDegreePlan = (md: AuditMetadata, pg: StudentDegreeProgram) => {
-  return (
-    md.darsDegreeProgramCode === pg.darsDegreeProgramCode &&
-    md.darsInstitutionCode === pg.darsInstitutionCode
-  );
-};
+import { groupAuditMetadata } from './utils';
+import { MatSnackBar } from '@angular/material';
 
 @Injectable()
 export class DARSEffects {
@@ -30,11 +22,12 @@ export class DARSEffects {
     private api: DarsApiService,
     private degreeAPI: DegreePlannerApiService,
     private store$: Store<GlobalState>,
+    private snackBar: MatSnackBar,
   ) {}
 
   @Effect()
   load$ = this.actions$.pipe(
-    ofType(DarsActionTypes.StartLoadingMetadata),
+    ofType(DarsActionTypes.StartLoadingDARSView),
 
     flatMap(() => {
       return forkJoinWithKeys({
@@ -59,37 +52,47 @@ export class DARSEffects {
         );
       }
 
-      const programMetadata: AuditMetadata[] = [];
-      const whatIfMetadata: AuditMetadata[] = [];
-
-      metadata.forEach(md => {
-        if (degreePrograms.some(dp => isFromDegreePlan(md, dp))) {
-          programMetadata.push(md);
-        } else {
-          whatIfMetadata.push(md);
-        }
-      });
-
-      return new darsActions.PopulateDarsState({
+      return new darsActions.DoneLoadingDARSView({
         degreePlans,
+        degreePrograms,
         metadata: {
           status: 'Loaded',
-          programMetadata,
-          whatIfMetadata,
+          outstanding: { program: 0, whatIf: 0 },
+          pending: { program: 0, whatIf: 0 },
+          ...groupAuditMetadata(metadata, degreePrograms),
         },
+        audits: {},
         alerts,
       });
     }),
 
     catchError(() => {
       return of(
-        new darsActions.ErrorLoadingMetadata({
-          message: 'Error loading metadata. Please try again',
+        new darsActions.ErrorLoadingDARSView({
+          message: 'Error loading DARS information. Please try again',
         }),
       );
     }),
   );
 
+  @Effect()
+  refreshMetadata$ = this.actions$.pipe(
+    ofType<darsActions.RefreshMetadata>(DarsActionTypes.RefreshMetadata),
+    flatMap(action =>
+      this.api.getAudits().pipe(
+        map(metadata => new darsActions.DoneRefreshingMetadata(metadata)),
+        tap(() => action.payload && action.payload.callback()),
+        catchError(() => {
+          return of(
+            new darsActions.ErrorRefreshingMetadata({
+              message: 'Error loading metadata. Please try again',
+            }),
+          );
+        }),
+      ),
+    ),
+  );
+
   @Effect()
   getAudit$ = this.actions$.pipe(
     ofType<darsActions.StartLoadingAudit>(DarsActionTypes.StartLoadingAudit),
@@ -117,35 +120,34 @@ export class DARSEffects {
   newAudit$ = this.actions$.pipe(
     ofType(DarsActionTypes.StartSendingAudit),
     flatMap((action: darsActions.StartSendingAudit) => {
-      const metadata = action.payload;
+      const auditType = action.payload.auditType;
       return this.api
         .newAudit(
-          metadata.darsInstitutionCode,
-          metadata.darsDegreeProgramCode,
-          metadata.degreePlannerPlanName,
-          metadata.whichEnrolledCoursesIncluded,
+          action.payload.darsInstitutionCode,
+          action.payload.darsDegreeProgramCode,
+          action.payload.degreePlannerPlanName,
+          action.payload.whichEnrolledCoursesIncluded,
         )
         .pipe(
-          map(audit => {
-            return new darsActions.DoneSendingAudit(action.payload);
+          map(({ darsJobId }) => {
+            return new darsActions.DoneSendingAudit({ auditType, darsJobId });
+          }),
+          catchError(_err => {
+            this.snackBar.open('Unable to generate audit');
+            return of(
+              new darsActions.ErrorSendingAudit({
+                auditType,
+                message: 'Unable to generate audit',
+              }),
+            );
           }),
         );
     }),
-
-    catchError(error => {
-      return of(
-        new darsActions.ErrorSendingAudit({
-          message: 'Unable to add course',
-        }),
-      );
-    }),
   );
 
   @Effect()
   updateAudits$ = this.actions$.pipe(
     ofType(DarsActionTypes.DoneSendingAudit),
-    map(() => {
-      return new darsActions.StartLoadingMetadata();
-    }),
+    map(() => new darsActions.RefreshMetadata()),
   );
 }
diff --git a/src/app/dars/store/reducer.ts b/src/app/dars/store/reducer.ts
index 7889329..b29e60c 100644
--- a/src/app/dars/store/reducer.ts
+++ b/src/app/dars/store/reducer.ts
@@ -1,11 +1,15 @@
 import { DARSState, INITIAL_DARS_STATE } from '@app/dars/store/state';
-import { DarsActionTypes, AddAuditMetadata } from './actions';
+import { DarsActionTypes } from './actions';
 import * as darsActions from './actions';
+import { groupAuditMetadata } from './utils';
 
 type SupportedActions =
-  | darsActions.ErrorLoadingMetadata
-  | darsActions.AddAuditMetadata
-  | darsActions.PopulateDarsState
+  | darsActions.DoneLoadingDARSView
+  | darsActions.ErrorLoadingDARSView
+  | darsActions.DoneRefreshingMetadata
+  | darsActions.StartSendingAudit
+  | darsActions.DoneSendingAudit
+  | darsActions.ErrorSendingAudit
   | darsActions.StartLoadingAudit
   | darsActions.DoneLoadingAudit
   | darsActions.DismissAlert;
@@ -15,35 +19,85 @@ export function darsReducer(
   action: SupportedActions,
 ): DARSState {
   switch (action.type) {
-    case DarsActionTypes.PopulateDarsState: {
-      return { ...state, ...action.payload };
+    case DarsActionTypes.DoneLoadingDARSView: {
+      return {
+        ...state,
+        ...action.payload,
+      };
     }
-    case DarsActionTypes.ErrorLoadingMetadata: {
+    case DarsActionTypes.DoneRefreshingMetadata: {
+      const outstanding =
+        state.metadata.status === 'Loaded'
+          ? { ...state.metadata.outstanding }
+          : { program: 0, whatIf: 0 };
+
       return {
         ...state,
         metadata: {
-          status: 'Error',
-          message: action.payload.message,
+          status: 'Loaded',
+          outstanding,
+          ...groupAuditMetadata(action.payload, state.degreePrograms),
         },
       };
     }
-    case DarsActionTypes.AddAuditMetadata: {
-      const programMetadata = (state.metadata.status === 'Loaded'
-        ? state.metadata.programMetadata
-        : []
-      ).concat(action.payload.programMetadata);
+    case DarsActionTypes.StartSendingAudit: {
+      if (state.metadata.status === 'Loaded') {
+        const outstanding = { ...state.metadata.outstanding };
+        outstanding[action.payload.auditType]++;
+
+        return {
+          ...state,
+          metadata: {
+            ...state.metadata,
+            outstanding,
+          },
+        };
+      } else {
+        return state;
+      }
+    }
+    case DarsActionTypes.DoneSendingAudit: {
+      if (state.metadata.status === 'Loaded') {
+        const outstanding = { ...state.metadata.outstanding };
+        outstanding[action.payload.auditType]--;
 
-      const whatIfMetadata = (state.metadata.status === 'Loaded'
-        ? state.metadata.whatIfMetadata
-        : []
-      ).concat(action.payload.whatIfMetadata);
+        const pending = { ...state.metadata.pending };
+        pending[action.payload.auditType]++;
 
+        return {
+          ...state,
+          metadata: {
+            ...state.metadata,
+            outstanding,
+            pending,
+          },
+        };
+      } else {
+        return state;
+      }
+    }
+    case DarsActionTypes.ErrorSendingAudit: {
+      if (state.metadata.status === 'Loaded') {
+        const outstanding = { ...state.metadata.outstanding };
+        outstanding[action.payload.auditType]--;
+
+        return {
+          ...state,
+          metadata: {
+            ...state.metadata,
+            outstanding,
+          },
+        };
+      } else {
+        return state;
+      }
+    }
+    case DarsActionTypes.ErrorLoadingDARSView: {
       return {
         ...state,
         metadata: {
-          status: 'Loaded',
-          programMetadata: programMetadata,
-          whatIfMetadata,
+          status: 'Error',
+          message: action.payload.message,
         },
       };
     }
diff --git a/src/app/dars/store/selectors.ts b/src/app/dars/store/selectors.ts
index 2402931..ef72651 100644
--- a/src/app/dars/store/selectors.ts
+++ b/src/app/dars/store/selectors.ts
@@ -27,7 +27,24 @@ export const programMetadata = createSelector(
     if (state.metadata.status === 'Loaded') {
       return state.metadata.programMetadata;
     } else {
-      return [];
+      return {};
+    }
+  },
+);
+
+export const outstandingAndPendingPrograms = createSelector(
+  getDARSState,
+  (state: DARSState) => {
+    if (state.metadata.status === 'Loaded') {
+      return {
+        outstanding: state.metadata.outstanding.program,
+        pending: state.metadata.pending.program,
+      };
+    } else {
+      return {
+        outstanding: 0,
+        pending: 0,
+      };
     }
   },
 );
@@ -38,7 +55,24 @@ export const whatIfMetadata = createSelector(
     if (state.metadata.status === 'Loaded') {
       return state.metadata.whatIfMetadata;
     } else {
-      return [];
+      return {};
+    }
+  },
+);
+
+export const outstandingAndPendingWhatIf = createSelector(
+  getDARSState,
+  (state: DARSState) => {
+    if (state.metadata.status === 'Loaded') {
+      return {
+        outstanding: state.metadata.outstanding.whatIf,
+        pending: state.metadata.pending.whatIf,
+      };
+    } else {
+      return {
+        outstanding: 0,
+        pending: 0,
+      };
     }
   },
 );
diff --git a/src/app/dars/store/state.ts b/src/app/dars/store/state.ts
index af68200..ac2f2d3 100644
--- a/src/app/dars/store/state.ts
+++ b/src/app/dars/store/state.ts
@@ -1,7 +1,8 @@
 import { Alert } from './../../core/models/alert';
-import { AuditMetadata } from '../models/audit-metadata';
+import { AuditMetadata, AuditMetadataMap } from '../models/audit-metadata';
 import { DegreePlan } from '@app/core/models/degree-plan';
 import { Audit } from '../models/audit/audit';
+import { StudentDegreeProgram } from '../models/student-degree-program';
 
 export type MetadataStatus =
   | { status: 'Error'; message: string }
@@ -9,8 +10,10 @@ export type MetadataStatus =
   | { status: 'Loading' }
   | {
       status: 'Loaded';
-      programMetadata: AuditMetadata[];
-      whatIfMetadata: AuditMetadata[];
+      outstanding: { program: number; whatIf: number };
+      pending: { program: number; whatIf: number };
+      programMetadata: AuditMetadataMap;
+      whatIfMetadata: AuditMetadataMap;
     };
 
 export type AuditStatus =
@@ -21,6 +24,7 @@ export type AuditStatus =
 
 export interface DARSState {
   degreePlans: DegreePlan[];
+  degreePrograms: StudentDegreeProgram[];
   metadata: MetadataStatus;
   audits: { [darsDegreeAuditReportId: number]: AuditStatus };
   alerts: Alert[];
@@ -28,6 +32,7 @@ export interface DARSState {
 
 export const INITIAL_DARS_STATE: DARSState = {
   degreePlans: [],
+  degreePrograms: [],
   metadata: { status: 'NotLoaded' },
   audits: {},
   alerts: [],
diff --git a/src/app/dars/store/utils.ts b/src/app/dars/store/utils.ts
new file mode 100644
index 0000000..1265a24
--- /dev/null
+++ b/src/app/dars/store/utils.ts
@@ -0,0 +1,53 @@
+import {
+  AuditMetadataMap,
+  AuditMetadata,
+  AuditMetadataWithReportId,
+} from '../models/audit-metadata';
+import { StudentDegreeProgram } from '../models/student-degree-program';
+
+interface SortedMetadataTuple {
+  pending: { program: number; whatIf: number };
+  programMetadata: AuditMetadataMap;
+  whatIfMetadata: AuditMetadataMap;
+}
+
+const isFromDegreeProgram = (md: AuditMetadata, pg: StudentDegreeProgram) => {
+  return (
+    md.darsDegreeProgramCode === pg.darsDegreeProgramCode &&
+    md.darsInstitutionCode === pg.darsInstitutionCode
+  );
+};
+
+export const groupAuditMetadata = (
+  metadata: AuditMetadata[],
+  degreePrograms: StudentDegreeProgram[],
+): SortedMetadataTuple => {
+  const empty: SortedMetadataTuple = {
+    pending: { program: 0, whatIf: 0 },
+    programMetadata: {},
+    whatIfMetadata: {},
+  };
+
+  return metadata.reduce((tuple, next) => {
+    const auditType = degreePrograms.some(dp => isFromDegreeProgram(next, dp))
+      ? 'program'
+      : 'whatIf';
+
+    if (next.darsDegreeAuditReportId === null) {
+      tuple.pending[auditType]++;
+      return tuple;
+    }
+
+    if (auditType === 'program') {
+      tuple.programMetadata[
+        next.darsDegreeAuditReportId
+      ] = next as AuditMetadataWithReportId;
+    } else {
+      tuple.whatIfMetadata[
+        next.darsDegreeAuditReportId
+      ] = next as AuditMetadataWithReportId;
+    }
+
+    return tuple;
+  }, empty);
+};
-- 
GitLab