Merge pull request #1 from sartography/bug/no-internet-connection

Caches sample records locally if frontend cannot connect to backend. Saves records and clears cache when connection is restored.
This commit is contained in:
Dan Funk 2020-10-19 12:47:39 -04:00 committed by GitHub
commit 6f42797dd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 232 additions and 39 deletions

View File

@ -21,14 +21,16 @@ import {AppComponent} from './app.component';
import {CountComponent} from './count/count.component';
import {FooterComponent} from './footer/footer.component';
import {HomeComponent} from './home/home.component';
import {LabelLayoutComponent} from './label-layout/label-layout.component';
import {LoadingComponent} from './loading/loading.component';
import {NavbarComponent} from './navbar/navbar.component';
import {PrintLayoutComponent} from './print-layout/print-layout.component';
import {PrintComponent} from './print/print.component';
import {SampleComponent} from './sample/sample.component';
import {ApiService} from './services/api.service';
import {CacheService} from './services/cache.service';
import {SettingsService} from './services/settings.service';
import {SettingsComponent} from './settings/settings.component';
import { LabelLayoutComponent } from './label-layout/label-layout.component';
import { PrintLayoutComponent } from './print-layout/print-layout.component';
/**
* This function is used internal to get a string instance of the `<base href="" />` value from `index.html`.
@ -48,16 +50,16 @@ export function getBaseHref(platformLocation: PlatformLocation): string {
@NgModule({
declarations: [
AppComponent,
LoadingComponent,
FooterComponent,
NavbarComponent,
HomeComponent,
SampleComponent,
CountComponent,
SettingsComponent,
PrintComponent,
FooterComponent,
HomeComponent,
LabelLayoutComponent,
LoadingComponent,
NavbarComponent,
PrintComponent,
PrintLayoutComponent,
SampleComponent,
SettingsComponent,
],
imports: [
BrowserAnimationsModule,
@ -65,25 +67,26 @@ export function getBaseHref(platformLocation: PlatformLocation): string {
FormlyModule,
FormsModule,
HttpClientModule,
MatProgressSpinnerModule,
ReactiveFormsModule,
MatCardModule,
MatInputModule,
MatToolbarModule,
MatButtonModule,
MatIconModule,
MatCardModule,
MatFormFieldModule,
QRCodeSVGModule,
AppRoutingModule,
MatIconModule,
MatInputModule,
MatOptionModule,
MatProgressSpinnerModule,
MatSelectModule,
// <-- This line MUST be last (https://angular.io/guide/router#module-import-order-matters)
MatToolbarModule,
QRCodeSVGModule,
ReactiveFormsModule,
AppRoutingModule, // <-- This line MUST be last (https://angular.io/guide/router#module-import-order-matters)
],
providers: [
{provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: {appearance: 'outline'}},
ApiService,
CacheService,
SettingsService,
{provide: 'APP_ENVIRONMENT', useClass: ThisEnvironment},
{provide: APP_BASE_HREF, useFactory: getBaseHref, deps: [PlatformLocation]},
{provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: {appearance: 'outline'}},
],
bootstrap: [AppComponent],
entryComponents: []

View File

@ -1,19 +1,35 @@
import {APP_BASE_HREF} from '@angular/common';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {async, ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {ApiService} from '../services/api.service';
import {CacheService} from '../services/cache.service';
import {MockEnvironment} from '../testing/environment.mock';
import { HomeComponent } from './home.component';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
let httpMock: HttpTestingController;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ HomeComponent ]
declarations: [ HomeComponent ],
imports: [
HttpClientTestingModule,
],
providers: [
ApiService,
CacheService,
{provide: 'APP_ENVIRONMENT', useClass: MockEnvironment},
{provide: APP_BASE_HREF, useValue: '/'},
],
})
.compileComponents();
}));
beforeEach(() => {
httpMock = TestBed.inject(HttpTestingController);
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();

View File

@ -1,4 +1,6 @@
import { Component, OnInit } from '@angular/core';
import {Component, OnInit} from '@angular/core';
import {ApiService} from '../services/api.service';
import {CacheService} from '../services/cache.service';
@Component({
selector: 'app-home',
@ -7,9 +9,33 @@ import { Component, OnInit } from '@angular/core';
})
export class HomeComponent implements OnInit {
constructor() { }
constructor(
private cacheService: CacheService,
private apiService: ApiService,
) {
}
ngOnInit(): void {
const cachedRecords = this.cacheService.getRecords();
let numSuccess = 0;
if (cachedRecords && cachedRecords.length > 0) {
cachedRecords.forEach(r => {
this.apiService.addSample(r).subscribe(() => {
numSuccess++;
console.log('cachedRecords', cachedRecords);
console.log('numSuccess', numSuccess);
if (numSuccess === cachedRecords.length) {
console.log('Cache cleared.');
this.cacheService.clearCache();
}
}, error => {
console.log('Cannot connect to server. Cache not cleared.');
});
});
} else {
console.log('No cached records to upload.');
}
}
}

View File

@ -1,5 +1,14 @@
<div *ngIf="showSpinner" class="loading" fxLayoutAlign="center center">
{{message || ''}}
<mat-spinner [diameter]="diameter"></mat-spinner>
<div *ngIf="showSpinner"
class="loading"
fxLayoutAlign="center center"
fxLayout="column"
fxLayoutGap="40px"
>
<div>
<mat-spinner [diameter]="diameter"></mat-spinner>
</div>
<div>
{{message || ''}}
</div>
</div>
<span *ngIf="!showSpinner">{{message || '...'}}</span>

View File

@ -1,7 +1,7 @@
<mat-toolbar color="primary">
<mat-toolbar-row>
<button id="nav_logo" mat-button routerLink="/" class="logo">BeSAFE</button>
<button id="nav_location" mat-button routerLink="/settings">{{testingLocation ? testingLocation.name : 'No location found'}} ({{locationId}})</button>
<button id="nav_location" mat-button routerLink="/settings">{{locationId === '0000' ? 'Click here to set location' : 'Location: ' + locationId}}</button>
<span fxFlex></span>
<button id="nav_home" mat-icon-button routerLink="/"><mat-icon>home</mat-icon></button>
<button id="nav_settings" mat-icon-button routerLink="/settings"><mat-icon>settings</mat-icon></button>

View File

@ -9,14 +9,15 @@ import {TestingLocation} from '../models/testingLocation.interface';
})
export class NavbarComponent implements OnInit {
@Input() testingLocation: TestingLocation;
locationId: string;
constructor(private settingsService: SettingsService) {
const settings = this.settingsService.getSettings();
this.locationId = settings.locationId;
}
ngOnInit(): void {
}
get locationId(): string {
const settings = this.settingsService.getSettings();
return settings.locationId;
}
}

View File

@ -10,7 +10,11 @@
<h1>Print Labels</h1>
</mat-card-header>
<mat-card-content fxLayout="column" fxLayoutAlign="center center">
<mat-card-content
fxLayout="column"
fxLayoutAlign="center center"
*ngIf="isSaved; else loadingMessage"
>
<app-print-layout
[barCode]="barCode"
[initials]="initials"
@ -46,3 +50,10 @@
[dateCreated]="dateCreated"
></app-print-layout>
</div>
<ng-template #loadingMessage>
<app-loading
message="Contacting the server..."
size="lg"
></app-loading>
</ng-template>

View File

@ -1,8 +1,21 @@
@import 'src/config';
::ng-deep .loading {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: white;
z-index: 100;
}
@media screen {
#media-print { display: none !important; }
#media-screen { display: flex !important; }
#media-screen {
display: flex !important;
}
}
@media print {
@ -34,4 +47,3 @@
background-color: $brand-gray-tint-2;
height: 500px;
}

View File

@ -6,6 +6,7 @@ import {AppDefaults} from '../models/appDefaults.interface';
import {LabelLayout} from '../models/labelLayout.interface';
import {Sample} from '../models/sample.interface';
import {ApiService} from '../services/api.service';
import {CacheService} from '../services/cache.service';
import {SettingsService} from '../services/settings.service';
@Component({
@ -20,13 +21,14 @@ export class PrintComponent implements AfterViewInit {
settings: AppDefaults;
@ViewChild('saveAndPrintButton') saveAndPrintButton: MatButton;
@ViewChild('doneButton') doneButton: MatButton;
isSaved = false;
isSaved: boolean;
constructor(
private api: ApiService,
private route: ActivatedRoute,
private changeDetector: ChangeDetectorRef,
private settingsService: SettingsService
private settingsService: SettingsService,
private cacheService: CacheService,
) {
this.dateCreated = new Date();
this.route.queryParamMap.subscribe(queryParamMap => {
@ -34,6 +36,11 @@ export class PrintComponent implements AfterViewInit {
this.initials = queryParamMap.get('initials');
});
this.settings = this.settingsService.getSettings();
this.isSaved = false;
this.save(s => {
this.isSaved = true;
});
}
ngAfterViewInit() {
@ -63,7 +70,7 @@ export class PrintComponent implements AfterViewInit {
headEl.appendChild(styleEl);
}
saveAndPrint() {
save(callback: (s: Sample) => void) {
const id = createQrCodeValue(
this.barCode,
this.initials,
@ -78,8 +85,22 @@ export class PrintComponent implements AfterViewInit {
location: this.settings.locationId,
};
this.api.addSample(newSample).subscribe(() => {
this.isSaved = true;
this.api.addSample(newSample).subscribe((result) => {
console.log('addSample subscribe callback');
callback(result);
}, err => {
if (err) {
console.error(err);
}
const cachedRecords = this.cacheService.saveRecord(newSample);
console.log('cachedRecords', cachedRecords);
callback(newSample);
});
}
saveAndPrint() {
this.save(s => {
window.print();
this.doneButton.focus();
this.changeDetector.detectChanges();

View File

@ -34,11 +34,11 @@ export class ApiService {
}
/** Add new sample */
addSample(sample: Sample): Observable<Sample> {
addSample(sample: Sample): Observable<null> {
const url = this.apiRoot + this.endpoints.sample;
return this.httpClient
.post<Sample>(url, sample)
.post<null>(url, sample)
.pipe(catchError(err => this._handleError(err)));
}

View File

@ -0,0 +1,20 @@
import {TestBed} from '@angular/core/testing';
import {CacheService} from './cache.service';
describe('CacheService', () => {
let service: CacheService;
beforeEach(() => TestBed.configureTestingModule({
providers: [
CacheService,
],
}));
beforeEach(() => {
service = TestBed.inject(CacheService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,54 @@
import {Injectable} from '@angular/core';
import serializeJs from 'serialize-javascript';
import {Sample} from '../models/sample.interface';
@Injectable({
providedIn: 'root'
})
export class CacheService {
// Default form field and data values
records: Sample[] = [];
// localStorage key
private localStorageKey = 'cachedRecords';
// Deserializes settings from local storage and returns AppDefaults instance
getStoredRecords(): Sample[] {
// tslint:disable-next-line:no-eval
return (eval(`(${localStorage.getItem(this.localStorageKey)})`) || []) as Sample[];
}
// Returns true if settings are found in local storage
hasStoredRecords(): boolean {
return this.getStoredRecords().length > 0;
}
// Returns records from local storage, or [] if none have been saved yet.
getRecords(): Sample[] {
if (this.hasStoredRecords()) {
return this.getStoredRecords();
} else {
return this.saveRecords(this.records);
}
}
// Serializes given record and adds it to cache in local storage
saveRecord(newRecord: Sample): Sample[] {
const records = this.getRecords();
records.push(newRecord);
return this.saveRecords(records);
}
// Serializes multiple given records and stores them in local storage
saveRecords(newRecords: Sample[]): Sample[] {
localStorage.setItem(this.localStorageKey, serializeJs(newRecords));
return newRecords;
}
// Clears cached records.
clearCache(): Sample[] {
return this.saveRecords([]);
}
}

View File

@ -0,0 +1,20 @@
import {TestBed} from '@angular/core/testing';
import {SettingsService} from './settings.service';
describe('SettingsService', () => {
let service: SettingsService;
beforeEach(() => TestBed.configureTestingModule({
providers: [
SettingsService,
],
}));
beforeEach(() => {
service = TestBed.inject(SettingsService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});