Merge pull request #2 from sartography/bug/layout-problems

Bug/layout problems
This commit is contained in:
Aaron Louie 2020-11-15 19:44:59 -08:00 committed by GitHub
commit 95fe8e413d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 989 additions and 231 deletions

View File

@ -29,6 +29,11 @@
],
"styles": [
"src/styles.scss"
],
"allowedCommonJsDependencies": [
"bwip-js",
"qrcode",
"serialize-javascript"
]
},
"configurations": {

View File

@ -10,9 +10,10 @@ describe('COVID19 Testing Kiosk App', () => {
http = new HttpClient('http://localhost:5001');
});
it('should automatically sign-in and redirect to home screen', () => {
it('should automatically sign-in and redirect to sample input screen', () => {
page.navigateTo();
expect(page.getRoute()).toEqual('/');
});
it('should navigate to settings screen', () => {
@ -23,15 +24,7 @@ describe('COVID19 Testing Kiosk App', () => {
page.clickAndExpectRoute('#nav_home', '/');
});
it('should navigate to occupancy count input screen', () => {
page.clickAndExpectRoute('#nav_count', '/count');
});
it('should navigate back to home screen', () => {
it('should navigate back to sample input screen', () => {
page.clickAndExpectRoute('#nav_home', '/');
});
it('should navigate to sample input screen', () => {
page.clickAndExpectRoute('#nav_sample', '/sample');
});
});

5
package-lock.json generated
View File

@ -2959,6 +2959,11 @@
"integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=",
"dev": true
},
"bwip-js": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/bwip-js/-/bwip-js-2.0.11.tgz",
"integrity": "sha512-8hAwUM+5FrGguKRaFi2QxKF9SxQM4fAfTy5TVAkeBODH6Eq3Q8MMEoBN+ymIBxiGKL+wNHB1Z1pv5f9oWGLUKQ=="
},
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",

View File

@ -35,6 +35,7 @@
"@angular/router": "~11.0.0-next.2",
"@ngx-formly/core": "^5.8.0",
"@ngx-formly/material": "^5.8.0",
"bwip-js": "^2.0.11",
"lodash.isequal": "^4.5.0",
"ngx-qrcode-svg": "^2.0.0",
"rfdc": "^1.1.4",

View File

@ -3,7 +3,7 @@ import {formatDate} from '@angular/common';
export const createQrCodeValue = (barCode: string, initials: string, dateCreated: Date, locationId: string): string => {
const valArray = [
barCode,
initials,
initials.toUpperCase(),
formatDate(dateCreated, 'yyyyMMddHHmm', 'en-us'),
locationId,
];

View File

@ -11,7 +11,7 @@ export const routes: Routes = [
{
path: '',
pathMatch: 'full',
component: HomeComponent
component: SampleComponent
},
{
path: 'sample',

View File

@ -1,5 +1,5 @@
<div class="mat-typography">
<app-navbar [testingLocation]="testingLocation"></app-navbar>
<app-navbar></app-navbar>
<router-outlet *ngIf="!loading; else loadingMessage"></router-outlet>
<app-footer></app-footer>
</div>

View File

@ -1,22 +1,15 @@
import {Component, Inject} from '@angular/core';
import {MatIconRegistry} from '@angular/material/icon';
import {DomSanitizer, Title} from '@angular/platform-browser';
import {Router} from '@angular/router';
import {Title} from '@angular/platform-browser';
import {AppEnvironment} from './models/appEnvironment.interface';
import {TestingLocation} from './models/testingLocation.interface';
import {GoogleAnalyticsService} from './services/google-analytics.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
loading: boolean;
testingLocation: TestingLocation = {
id: '0000',
name: 'Click here to set location',
};
constructor(
@Inject('APP_ENVIRONMENT') private environment: AppEnvironment,
@ -25,6 +18,7 @@ export class AppComponent {
this.titleService.setTitle(this.environment.title);
}
reload() {
this.loading = true;
setTimeout(() => this.loading = false, 300);

View File

@ -18,9 +18,14 @@ import {QRCodeSVGModule} from 'ngx-qrcode-svg';
import {ThisEnvironment} from '../environments/environment.injectable';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {BarcodeSvgDirective} from './barcode-svg/barcode-svg.directive';
import {CountComponent} from './count/count.component';
import {FooterComponent} from './footer/footer.component';
import {HomeComponent} from './home/home.component';
import {CircleQRcodeDoubleComponent} from './label-layout/formats/circle-qrcode-double/circle-qrcode-double.component';
import {CircleQRcodeSingleComponent} from './label-layout/formats/circle-qrcode-single/circle-qrcode-single.component';
import {RectangleCode128Component} from './label-layout/formats/rectangle-code128/rectangle-code128.component';
import {RectangleDatamatrixComponent} from './label-layout/formats/rectangle-datamatrix/rectangle-datamatrix.component';
import {LabelLayoutComponent} from './label-layout/label-layout.component';
import {LoadingComponent} from './loading/loading.component';
import {NavbarComponent} from './navbar/navbar.component';
@ -50,6 +55,9 @@ export function getBaseHref(platformLocation: PlatformLocation): string {
@NgModule({
declarations: [
AppComponent,
BarcodeSvgDirective,
CircleQRcodeDoubleComponent,
CircleQRcodeSingleComponent,
CountComponent,
FooterComponent,
HomeComponent,
@ -58,6 +66,8 @@ export function getBaseHref(platformLocation: PlatformLocation): string {
NavbarComponent,
PrintComponent,
PrintLayoutComponent,
RectangleCode128Component,
RectangleDatamatrixComponent,
SampleComponent,
SettingsComponent,
],

View File

@ -0,0 +1,8 @@
import { BarcodeSvgDirective } from './barcode-svg.directive';
describe('BarcodeSvgDirective', () => {
it('should create an instance', () => {
const directive = new BarcodeSvgDirective(null);
expect(directive).toBeTruthy();
});
});

View File

@ -0,0 +1,40 @@
import bwipjs from 'bwip-js';
import DrawingSVG from './draw-svg.js';
import {Directive, ElementRef, Input, OnInit} from '@angular/core';
@Directive({
selector: '[appBarcodeSvg]'
})
export class BarcodeSvgDirective implements OnInit {
@Input() format: string;
@Input() value: string;
@Input() x: number;
@Input() y: number;
@Input() width: number;
@Input() height: number;
constructor(private barcodeContainer: ElementRef) {
}
ngOnInit() {
if (!!(bwipjs && bwipjs.render && this.format)) {
const opts: { [key: string]: any } = {
bcid: this.format,
text: this.value,
scale: 1,
width: this.width,
height: this.height,
// includetext: false,
// textalign: 'center',
// version: '12x64',
// padding: this.settings.labelLayout.marginSize,
};
if (this.format === 'qrcode') {
opts.eclevel = 'H';
}
this.barcodeContainer.nativeElement.innerHTML = bwipjs.render(opts, DrawingSVG(opts, bwipjs.FontLib));
}
}
}

View File

@ -0,0 +1,270 @@
// bwip-js/examples/drawing-svg.js
//
// This is an advanced demonstation of using the drawing interface.
//
// It converts the drawing primitives into the equivalent SVG. Linear barcodes
// are rendered as a series of stroked paths. 2D barcodes are rendered as a
// series of filled paths.
//
// Rotation is handled during drawing. The resulting SVG will contain the
// already-rotated barcode without an SVG transform.
//
// If the requested barcode image contains text, the glyph paths are
// extracted from the font file (via the builtin FontLib and stb_truetype.js)
// and added as filled SVG paths.
//
// This code can run in the browser and in nodejs.
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else {
root.DrawingSVG = factory();
}
}(typeof self !== 'undefined' ? self : this, function () {
"use strict";
function DrawingSVG(opts, FontLib) {
// Unrolled x,y rotate/translate matrix
var tx0 = 0, tx1 = 0, tx2 = 0, tx3 = 0;
var ty0 = 0, ty1 = 0, ty2 = 0, ty3 = 0;
var svg = '';
var path;
var lines = {};
// Magic number to approximate an ellipse/circle using 4 cubic beziers.
var ELLIPSE_MAGIC = 0.55228475 - 0.00045;
// Global graphics state
var gs_width, gs_height; // image size, in pixels
var gs_dx, gs_dy; // x,y translate (padding)
return {
// Make no adjustments
scale(sx, sy) {
},
// Measure text. This and scale() are the only drawing primitives that
// are called before init().
//
// `font` is the font name typically OCR-A or OCR-B.
// `fwidth` and `fheight` are the requested font cell size. They will
// usually be the same, except when the scaling is not symetric.
measure(str, font, fwidth, fheight) {
fwidth = fwidth|0;
fheight = fheight|0;
var fontid = FontLib.lookup(font);
var width = 0;
var ascent = 0;
var descent = 0;
for (var i = 0; i < str.length; i++) {
var ch = str.charCodeAt(i);
var glyph = FontLib.getpaths(fontid, ch, fwidth, fheight);
if (!glyph) {
continue;
}
ascent = Math.max(ascent, glyph.ascent);
descent = Math.max(descent, -glyph.descent);
width += glyph.advance;
}
return { width, ascent, descent };
},
// width and height represent the maximum bounding box the graphics will occupy.
// The dimensions are for an unrotated rendering. Adjust as necessary.
init(width, height) {
// Add in the effects of padding. These are always set before the
// drawing constructor is called.
var padl = opts.paddingleft || 0;
var padr = opts.paddingright || 0;
var padt = opts.paddingtop || 0;
var padb = opts.paddingbottom || 0;
var rot = opts.rotate || 'N';
width += padl + padr;
height += padt + padb;
// Transform indexes are: x, y, w, h
switch (rot) {
// tx = w-y, ty = x
case 'R': tx1 = -1; tx2 = 1; ty0 = 1; break;
// tx = w-x, ty = h-y
case 'I': tx0 = -1; tx2 = 1; ty1 = -1; ty3 = 1; break;
// tx = y, ty = h-x
case 'L': tx1 = 1; ty0 = -1; ty3 = 1; break;
// tx = x, ty = y
default: tx0 = ty1 = 1; break;
}
// Setup the graphics state
var swap = rot == 'L' || rot == 'R';
gs_width = swap ? height : width;
gs_height = swap ? width : height;
gs_dx = padl;
gs_dy = padt;
svg = '';
},
// Unconnected stroked lines are used to draw the bars in linear barcodes.
// No line cap should be applied. These lines are always orthogonal.
line(x0, y0, x1, y1, lw, rgb) {
// Try to get non-blurry lines...
x0 = x0|0;
y0 = y0|0;
x1 = x1|0;
y1 = y1|0;
lw = Math.round(lw);
// Try to keep the lines "crisp" by using with the SVG line drawing spec to
// our advantage.
if (lw & 1) {
if (x0 === x1) {
x0 += 0.5;
x1 += 0.5;
}
if (y0 === y1) {
y0 += 0.5;
y1 += 0.5;
}
}
// Group together all lines of the same width and emit as single paths.
// Dramatically reduces resulting text size.
var key = '' + lw + '#' + rgb;
if (!lines[key]) {
lines[key] = '<path stroke="#' + rgb + '" stroke-width="' + lw + '" d="';
}
lines[key] += 'M' + transform(x0, y0) + 'L' + transform(x1, y1);
},
// Polygons are used to draw the connected regions in a 2d barcode.
// These will always be unstroked, filled, non-intersecting,
// orthogonal shapes.
// You will see a series of polygon() calls, followed by a fill().
polygon(pts) {
if (!path) {
path = '<path d="';
}
path += 'M' + transform(pts[0][0], pts[0][1]);
for (var i = 1, n = pts.length; i < n; i++) {
var p = pts[i];
path += 'L' + transform(p[0], p[1]);
}
path += 'Z';
},
// An unstroked, filled hexagon used by maxicode. You can choose to fill
// each individually, or wait for the final fill().
//
// The hexagon is drawn from the top, counter-clockwise.
hexagon(pts, rgb) {
this.polygon(pts); // A hexagon is just a polygon...
},
// An unstroked, filled ellipse. Used by dotcode and maxicode at present.
// maxicode issues pairs of ellipse calls (one cw, one ccw) followed by a fill()
// to create the bullseye rings. dotcode issues all of its ellipses then a
// fill().
ellipse(x, y, rx, ry, ccw) {
if (!path) {
path = '<path d="';
}
var dx = rx * ELLIPSE_MAGIC;
var dy = ry * ELLIPSE_MAGIC;
// Since we fill with even-odd, don't worry about cw/ccw
path += 'M' + transform(x - rx, y) +
'C' + transform(x - rx, y - dy) + ' ' +
transform(x - dx, y - ry) + ' ' +
transform(x, y - ry) +
'C' + transform(x + dx, y - ry) + ' ' +
transform(x + rx, y - dy) + ' ' +
transform(x + rx, y) +
'C' + transform(x + rx, y + dy) + ' ' +
transform(x + dx, y + ry) + ' ' +
transform(x, y + ry) +
'C' + transform(x - dx, y + ry) + ' ' +
transform(x - rx, y + dy) + ' ' +
transform(x - rx, y) +
'Z';
},
// PostScript's default fill rule is even-odd.
fill(rgb) {
if (path) {
svg += path + '" fill="#' + rgb + '" fill-rule="evenodd" />\n';
path = null;
}
},
// Draw text with optional inter-character spacing. `y` is the baseline.
// font is an object with properties { name, width, height, dx }
// width and height are the font cell size.
// dx is extra space requested between characters (usually zero).
text(x, y, str, rgb, font) {
var fontid = FontLib.lookup(font.name);
var fwidth = font.width|0;
var fheight = font.height|0;
var dx = font.dx|0;
var path = '';
for (var k = 0; k < str.length; k++) {
var ch = str.charCodeAt(k);
var glyph = FontLib.getpaths(fontid, ch, fwidth, fheight);
if (!glyph) {
continue;
}
if (glyph.length) {
// A glyph is composed of sequence of curve and line segments.
// M is move-to
// L is line-to
// Q is quadratic bezier curve-to
// C is cubic bezier curve-to
for (var i = 0, l = glyph.length; i < l; i++) {
let seg = glyph[i];
if (seg.type == 'M' || seg.type == 'L') {
path += seg.type + transform(seg.x + x, y - seg.y);
} else if (seg.type == 'Q') {
path += seg.type + transform(seg.cx + x, y - seg.cy) + ' ' +
transform(seg.x + x, y - seg.y);
} else if (seg.type == 'C') {
path += seg.type + transform(seg.cx1 + x, y - seg.cy1) + ' ' +
transform(seg.cx2 + x, y - seg.cy2) + ' ' +
transform(seg.x + x, y - seg.y);
}
}
// Close the shape
path += 'Z';
}
x += glyph.advance + dx;
}
if (path) {
svg += '<path d="' + path + '" fill="#' + rgb + '" />\n';
}
},
// Called after all drawing is complete. The return value from this method
// is the return value from `bwipjs.render()`.
end() {
var linesvg = '';
for (var key in lines) {
linesvg += lines[key] + '" />\n';
}
var bg = opts.backgroundcolor;
return '<svg version="1.1" width="' + gs_width + '" height="' + gs_height +
'" xmlns="http://www.w3.org/2000/svg">\n' +
(/^[0-9A-Fa-f]{6}$/.test(''+bg)
? '<rect width="100%" height="100%" fill="#' + bg + '" />\n'
: '') +
linesvg + svg + '</svg>\n';
},
};
// translate/rotate and return as an SVG coordinate pair
function transform(x, y) {
x += gs_dx;
y += gs_dy;
var tx = tx0 * x + tx1 * y + tx2 * (gs_width-1) + tx3 * (gs_height-1);
var ty = ty0 * x + ty1 * y + ty2 * (gs_width-1) + ty3 * (gs_height-1);
return '' + ((tx|0) === tx ? tx : tx.toFixed(2)) + ' ' +
((ty|0) === ty ? ty : ty.toFixed(2));
}
}
return DrawingSVG;
}));

View File

@ -2,34 +2,52 @@ import {AppDefaultsOptions} from '../models/appDefaults.interface';
import {LabelLayout} from '../models/labelLayout.interface';
export const labelLayouts = {
round_32mm_1up: new LabelLayout({
name: '32mm Round Label - 1up',
type: 'round_32mm_1up',
numCols: 1,
columnGap: 0,
circle_qrcode_single: new LabelLayout({
name: '32mm Round Label - QR Code (1up)',
barcodeType: 'qrcode',
id: 'circle_qrcode_single',
pageWidth: 32,
pageHeight: 32,
}),
round_32mm_2up: new LabelLayout({
name: '32mm Round Label - 2up',
type: 'round_32mm_2up',
numCols: 2,
columnGap: 3.4,
circle_qrcode_double: new LabelLayout({
name: '32mm Round Label - QR Code (2up)',
barcodeType: 'qrcode',
id: 'circle_qrcode_double',
pageWidth: 64,
pageHeight: 32,
}),
rectangle_code128: new LabelLayout({
name: '2in x 1.25in Rectangular Label - CODE128',
barcodeType: 'code128',
id: 'rectangle_code128',
pageWidth: 54,
pageHeight: 34,
}),
rectangle_datamatrix: new LabelLayout({
name: '2in x 1.25in Rectangular Label - DataMatrix',
barcodeType: 'datamatrix',
id: 'rectangle_datamatrix',
pageWidth: 54,
pageHeight: 34,
}),
};
export const defaultOptions: AppDefaultsOptions = {
barCodeNumLength: 9, // Number of digits in Bar Code.
barCodeRegExp: /^[\d]{14}$|^[\d]{9}$/, // Pattern for Bar Code data. Scanned barcodes will be either 9 or 14 digits long.
// Manually-entered ID numbers will be exactly 9 digits long.
countsCollection: 'counts', // Name of collection for Line Counts in Firebase.
dateDisplayFormat: 'MM/dd/yyyy, hh:mm aa', // Format for dates when displayed to user.
dateEncodedFormat: 'yyyyMMddHHmm', // Format for dates when encoded in IDs for database records.
barCodeNumLength: 9, // Number of digits in Bar Code.
barCodeRegExp: /^[\d]{14}$|^[\d]{9}$/, // Pattern for Bar Code data.
// Scanned barcodes will be either 9 or 14 digits long.
// Manually-entered ID numbers will be exactly 9 digits long.
countsCollection: 'counts', // Name of collection for Line Counts in Firebase.
dateDisplayFormat: 'MM/dd/yyyy, hh:mm aa', // Format for dates when displayed to user.
dateEncodedFormat: 'yyyyMMddHHmm', // Format for dates when encoded in IDs for database records.
initialsLength: 5,
initialsRegExp: /^[a-zA-Z]{2,5}$/,
labelLayout: labelLayouts.round_32mm_1up, // Which label layout to use for printing. Can be overridden by user setting.
lineCountRegExp: /^[\d]{4}-[\d]{12}$/, // ID format for Line Count records.
locationId: '0000', // Default location ID. Can be overridden by user setting.
locationIdRegExp: /^[\d]{4}$/, // ID format for Line Count records.
numCopies: 1, // Default number of copies of labels to print. Can be overridden by user setting.
qrCodeRegExp: /^[\d]{9}-[a-zA-Z]+-[\d]{12}-[\d]{4}$/, // ID format for QR Code records.
samplesCollection: 'samples', // Name of collection for Line Counts in Firebase.
labelLayout: labelLayouts.circle_qrcode_single, // Which label layout to use for printing. Can be overridden by user setting.
lineCountRegExp: /^[\d]{4}-[\d]{12}$/, // ID format for Line Count records.
locationId: '0000', // Default location ID. Can be overridden by user setting.
locationIdRegExp: /^[\d]{4}$/, // ID format for Line Count records.
numCopies: 1, // Default number of copies of labels to print.
// Can be overridden by user setting.
qrCodeRegExp: /^[\d]{9}-[a-zA-Z]+-[\d]{12}-[\d]{4}$/, // ID format for QR Code records.
samplesCollection: 'samples', // Name of collection for Line Counts in Firebase.
};

View File

@ -0,0 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {mockSample} from '../../../testing/sample.mock';
import { CircleQRcodeDoubleComponent } from './circle-qrcode-double.component';
describe('CircleQRcodeDoubleComponent', () => {
let component: CircleQRcodeDoubleComponent;
let fixture: ComponentFixture<CircleQRcodeDoubleComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ CircleQRcodeDoubleComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(CircleQRcodeDoubleComponent);
component = fixture.componentInstance;
component.sample = mockSample;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,52 @@
<svg
class="label-layout-format"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="64mm"
height="32mm"
viewBox="0 0 64 32"
>
<defs>
<g id="circle_qrcode_single">
<circle
class="label-layout-border"
cx="16"
cy="16"
fill="#FFFFFF"
stroke="#CCCCCC"
stroke-width="0.2"
r="14.3"/>
<g transform="translate(7, 6.8)">
<g
appBarcodeSvg
[format]="'qrcode'"
[value]="sample.barcode"
[width]="18"
[height]="18"
transform="scale(0.35)"
/>
</g>
<g transform="translate(4.2, 14)">
<text x="0" y="0">T</text>
<text x="0" y="2">{{sample.date | date: 'HH'}}</text>
<text x="0" y="4">{{sample.date | date: 'mm'}}</text>
</g>
<g transform="translate(27.4, 14)">
<text x="0" y="0">L</text>
<text x="0" y="2">{{sample.location.slice(0,2)}}</text>
<text x="0" y="4">{{sample.location.slice(2,4)}}</text>
</g>
</g>
</defs>
<use xlink:href="#circle_qrcode_single" x="0" y="0"></use>
<use xlink:href="#circle_qrcode_single" x="32" y="0"></use>
<g>
<text x="25%" y="6">#{{sample.student_id}}</text>
<text x="25%" y="27.6">{{sample.date | date:'yyMMdd'}}{{sample.initials}}</text>
</g>
<g>
<text x="75%" y="6">#{{sample.student_id}}</text>
<text x="75%" y="27.6">{{sample.date | date:'yyMMdd'}}{{sample.initials}}</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,23 @@
import {Component, Input, OnInit} from '@angular/core';
import {AppDefaults} from '../../../models/appDefaults.interface';
import {Sample} from '../../../models/sample.interface';
@Component({
selector: 'app-circle-qrcode-double',
templateUrl: './circle-qrcode-double.component.svg',
styleUrls: ['./circle-qrcode-double.component.scss']
})
export class CircleQRcodeDoubleComponent implements OnInit {
@Input() sample: Sample;
@Input() settings: AppDefaults;
@Input() x: number;
@Input() y: number;
@Input() width: number;
@Input() height: number;
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {mockSample} from '../../../testing/sample.mock';
import { CircleQRcodeSingleComponent } from './circle-qrcode-single.component';
describe('CircleQRcodeSingleComponent', () => {
let component: CircleQRcodeSingleComponent;
let fixture: ComponentFixture<CircleQRcodeSingleComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ CircleQRcodeSingleComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(CircleQRcodeSingleComponent);
component = fixture.componentInstance;
component.sample = mockSample;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,40 @@
<svg
class="label-layout-format"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="32mm"
height="32mm"
viewBox="0 0 32 32"
>
<circle
class="label-layout-border"
cx="16"
cy="16"
fill="#FFFFFF"
stroke="#CCCCCC"
stroke-width="0.2"
r="14.3"/>
<g transform="translate(7, 6.8)">
<g
appBarcodeSvg
[format]="'qrcode'"
[value]="sample.barcode"
[width]="18"
[height]="18"
transform="scale(0.35)"
/>
</g>
<text x="50%" y="6">#{{sample.student_id}}</text>
<text x="50%" y="27.6">{{sample.date | date:'yyMMdd'}}{{sample.initials}}</text>
<g transform="translate(4.2, 14)">
<text x="0" y="0">T</text>
<text x="0" y="2">{{sample.date | date: 'HH'}}</text>
<text x="0" y="4">{{sample.date | date: 'mm'}}</text>
</g>
<g transform="translate(27.4, 14)">
<text x="0" y="0">L</text>
<text x="0" y="2">{{sample.location.slice(0,2)}}</text>
<text x="0" y="4">{{sample.location.slice(2,4)}}</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,27 @@
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {AppDefaults} from '../../../models/appDefaults.interface';
import {Sample} from '../../../models/sample.interface';
@Component({
selector: 'app-circle-qrcode-single',
templateUrl: './circle-qrcode-single.component.svg',
styleUrls: ['./circle-qrcode-single.component.scss']
})
export class CircleQRcodeSingleComponent implements OnInit {
@Input() sample: Sample;
@Input() settings: AppDefaults;
@Input() x: number;
@Input() y: number;
@Input() width: number;
@Input() height: number;
constructor(private changeDetector: ChangeDetectorRef) {
}
ngOnInit(): void {
if (this.sample) {
this.changeDetector.detectChanges();
}
}
}

View File

@ -0,0 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {mockSample} from '../../../testing/sample.mock';
import { RectangleCode128Component } from './rectangle-code128.component';
describe('RectangleCode128Component', () => {
let component: RectangleCode128Component;
let fixture: ComponentFixture<RectangleCode128Component>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ RectangleCode128Component ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(RectangleCode128Component);
component = fixture.componentInstance;
component.sample = mockSample;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,46 @@
<svg
class="label-layout-format"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="54mm"
height="34mm"
viewBox="0 0 54 34"
>
<defs>
<g id="up_arrow">
<g transform="translate(2.5, 2)">
<g transform="translate(-2.5, -2) scale(0.1)">
<path d="M 0,0 H 24 V 24 H 0 Z" style="fill:none"/>
<path d="M 20,11 H 7.83 L 13.42,5.41 12,4 4,12 12,20 13.41,18.59 7.83,13 H 20 Z" />
</g>
<text x="1" y="0">UP</text>
</g>
</g>
</defs>
<g transform="translate(1.5, 1)">
<rect
class="label-layout-border"
fill="#FFFFFF"
stroke="#CCCCCC"
stroke-width="0.2"
width="51"
height="32"
x="0"
y="0"
rx="2"
/>
<use xlink:href="#up_arrow" x="2" y="2" />
<g transform="translate(2, 11)">
<g
appBarcodeSvg
[format]="'code128'"
[height]="10"
[value]="sample.barcode"
[width]="47"
transform="scale(0.345)"/>
</g>
<use xlink:href="#up_arrow" x="2" y="27.5" />
</g>
<text x="50%" y="75%">{{sample.barcode}}</text>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,25 @@
import {Component, Input, OnInit} from '@angular/core';
import {AppDefaults} from '../../../models/appDefaults.interface';
import {Sample} from '../../../models/sample.interface';
@Component({
selector: 'app-rectangle-code128',
templateUrl: './rectangle-code128.component.svg',
styleUrls: ['./rectangle-code128.component.scss']
})
export class RectangleCode128Component implements OnInit {
@Input() sample: Sample;
@Input() settings: AppDefaults;
@Input() x: number;
@Input() y: number;
@Input() width: number;
@Input() height: number;
constructor() { }
ngOnInit(): void {
// Replace "#barcode" element with svg of barcode
}
}

View File

@ -0,0 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {mockSample} from '../../../testing/sample.mock';
import { RectangleDatamatrixComponent } from './rectangle-datamatrix.component';
describe('RectangleDatamatrixComponent', () => {
let component: RectangleDatamatrixComponent;
let fixture: ComponentFixture<RectangleDatamatrixComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ RectangleDatamatrixComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(RectangleDatamatrixComponent);
component = fixture.componentInstance;
component.sample = mockSample;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,56 @@
<svg
class="label-layout-format"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="54mm"
height="34mm"
viewBox="0 0 54 34"
>
<defs>
<g id="up_arrow">
<g transform="translate(2.5, 2)">
<g transform="translate(-2.5, -2) scale(0.1)">
<path d="M 0,0 H 24 V 24 H 0 Z" style="fill:none"/>
<path d="M 20,11 H 7.83 L 13.42,5.41 12,4 4,12 12,20 13.41,18.59 7.83,13 H 20 Z" />
</g>
<text x="1" y="0">UP</text>
</g>
</g>
</defs>
<g transform="translate(1.5, 1)">
<rect
class="label-layout-border"
fill="#FFFFFF"
stroke="#CCCCCC"
stroke-width="0.2"
width="51"
height="32"
x="0"
y="0"
rx="2"
/>
<use xlink:href="#up_arrow" x="2" y="2" />
<g
appBarcodeSvg
format="datamatrix"
[value]="sample.barcode"
[width]="3.5"
[height]="3.5"
transform="translate(3.5, 10)"
/>
<g transform="translate(18, 14)">
<text x="0" y="0" style="text-anchor: start; text-align: left;">
#{{sample.student_id}}
</text>
<text x="0" y="3" style="text-anchor: start; text-align: left;">
{{sample.date | date:'yyyy-MM-dd HH:mm'}}
</text>
<text x="0" y="6" style="text-anchor: start; text-align: left;">
{{sample.initials}} {{sample.location}}
</text>
</g>
<use xlink:href="#up_arrow" x="2" y="27.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,23 @@
import {Component, Input, OnInit} from '@angular/core';
import {AppDefaults} from '../../../models/appDefaults.interface';
import {Sample} from '../../../models/sample.interface';
@Component({
selector: 'app-rectangle-datamatrix',
templateUrl: './rectangle-datamatrix.component.svg',
styleUrls: ['./rectangle-datamatrix.component.scss']
})
export class RectangleDatamatrixComponent implements OnInit {
@Input() sample: Sample;
@Input() settings: AppDefaults;
@Input() x: number;
@Input() y: number;
@Input() width: number;
@Input() height: number;
constructor() { }
ngOnInit(): void {
}
}

View File

@ -1,18 +1,4 @@
<div class="label-layout">
<div class="date">{{dateCreated | date:'yyMMdd'}}{{initials}}</div>
<div class="time">
T<br />
{{dateCreated | date:'HH'}}<br />
{{dateCreated | date:'mm'}}
</div>
<qrcode-svg
[value]="qrCodeValue"
errorCorrectionLevel="H"
></qrcode-svg>
<div class="location">
L<br />
{{settings.locationId.slice(0, 2)}}<br />
{{settings.locationId.slice(2, 4)}}
</div>
<div class="barcode">#{{barCode}}</div>
</div>
<app-circle-qrcode-single *ngIf="isLayout('circle_qrcode_single')" [sample]="sample"></app-circle-qrcode-single>
<app-circle-qrcode-double *ngIf="isLayout('circle_qrcode_double')" [sample]="sample"></app-circle-qrcode-double>
<app-rectangle-code128 *ngIf="isLayout('rectangle_code128')" [sample]="sample"></app-rectangle-code128>
<app-rectangle-datamatrix *ngIf="isLayout('rectangle_datamatrix')" [sample]="sample"></app-rectangle-datamatrix>

View File

@ -1,51 +1,13 @@
.date,
.time,
.location,
.barcode {
position: absolute;
font-family: monospace;
font-weight: bolder;
text-align: center;
font-size: 6pt;
}
.label-layout {
position: relative;
width: 24.6mm;
height: 24.6mm;
border: 2mm solid white;
border-radius: 100%;
background-color: white;
padding: 0;
margin: 0;
.date {
top: 0;
width: 100%;
}
.barcode {
bottom: 0;
width: 100%;
}
.time {
top: calc(50% - 3mm);
left: 0;
}
.location {
top: calc(50% - 3mm);
right: 0;
}
qrcode-svg {
position: absolute;
left: 50%;
top: 50%;
margin-left: -11mm;
margin-top: -11mm;
width: 22mm;
height: 22mm;
}
::ng-deep app-circle-qrcode-single,
::ng-deep app-circle-qrcode-double,
::ng-deep app-rectangle-code128,
::ng-deep app-rectangle-datamatrix {
background-color: transparent;
margin: 0 !important;
padding: 0 !important;
overflow: hidden !important;
line-height: 0;
width: inherit;
height: inherit;
display: block;
}

View File

@ -1,6 +1,8 @@
import {Component, Input, OnInit} from '@angular/core';
import {createQrCodeValue} from '../_util/qrCode';
import {AppDefaults} from '../models/appDefaults.interface';
import {CssStyle} from '../models/cssStyle.interface';
import {Sample} from '../models/sample.interface';
import {SettingsService} from '../services/settings.service';
@Component({
@ -8,26 +10,36 @@ import {SettingsService} from '../services/settings.service';
templateUrl: './label-layout.component.html',
styleUrls: ['./label-layout.component.scss']
})
export class LabelLayoutComponent implements OnInit {
export class LabelLayoutComponent {
@Input() dateCreated: Date;
@Input() barCode: string;
@Input() initials: string;
settings: AppDefaults;
sample: Sample;
barcodeValue: string;
constructor(private settingsService: SettingsService) {
this.settings = this.settingsService.getSettings();
}
get qrCodeValue(): string {
return createQrCodeValue(
this.barCode,
this.initials,
this.dateCreated,
this.settings.locationId
this.sample = {
barcode: '',
student_id: '123456789',
initials: 'ABCDE',
date: new Date(),
location: this.settings.locationId,
};
this.barcodeValue = createQrCodeValue(
this.sample.student_id,
this.sample.initials,
this.sample.date,
this.sample.location
);
this.sample.barcode = this.barcodeValue;
}
ngOnInit(): void {
isLayout(layoutId: string) {
return this.settings.labelLayout.id === layoutId;
}
}

View File

@ -0,0 +1 @@
export interface CssStyle { [klass: string]: any; }

View File

@ -1,78 +1,42 @@
export interface LayoutOptions {
type?: string;
barcodeType?: string;
id?: string;
name?: string;
units?: string;
pointsPerUnit?: number;
labelSize?: number;
marginSize?: number;
pageHeight?: number;
pageWidth?: number;
numCols?: number;
columnGap?: number;
sideTextWidth?: number;
sideTextTop?: number;
sideTextMargin?: number;
topTextMargin?: number;
bottomTextMargin?: number;
fontSizePt?: number;
numCopies?: number;
}
export class LabelLayout {
type = 'round_32mm_1up';
name = '32mm Round Label - 1up';
name = '32mm Round Label - QR Code (1up)';
barcodeType = 'qrcode';
id = 'circle_qrcode_double';
units = 'mm';
pointsPerUnit = 0.3528;
labelSize = 28.6;
marginSize = 1.7;
pageHeight = 32;
pageWidth = 32;
numCols = 1;
columnGap = 4;
sideTextWidth = 4;
sideTextTop = 11;
sideTextMargin = 1.5;
topTextMargin = 3;
bottomTextMargin = 2.5;
fontSizePt = 6;
numCopies = 1;
constructor(private options: LayoutOptions) {
const keys = Object.keys(options);
keys.forEach(k => {
this[k] = options[k];
});
if (options) {
const keys = Object.keys(options);
keys.forEach(k => {
this[k] = options[k];
});
} else {
console.error('options is empty:', options);
}
}
get dimensions() {
return {
bottomTextMargin: this._toUnits(this.bottomTextMargin),
columnGap: this._toUnits(this.columnGap),
fontSize: this._toUnits(this.fontSize),
labelSize: this._toUnits(this.labelSize),
labelSizeWithMargins: this._toUnits(this.labelSizeWithMargins),
marginWidth: this._toUnits(this.marginSize),
pageHeight: this._toUnits(this.pageHeight),
pageWidth: this._toUnits(this.pageWidth),
sideTextMargin: this._toUnits(this.sideTextMargin),
sideTextTop: this._toUnits(this.sideTextTop),
sideTextWidth: this._toUnits(this.sideTextWidth),
topTextMargin: this._toUnits(this.topTextMargin),
pageHeight: this._toUnits(this.pageHeight),
};
}
get labelSizeWithMargins(): number {
return (this.labelSize + (this.marginSize * 2));
}
get pageWidth(): number {
return (this.labelSizeWithMargins * this.numCols);
}
get pageHeight(): number {
return (this.labelSizeWithMargins * this.numCopies);
}
get fontSize(): number {
return this.fontSizePt * this.pointsPerUnit;
}
private _toUnits(num: number) {
return `${num}${this.units}`;
}

View File

@ -1,6 +1,7 @@
export interface Sample {
barcode: string;
student_id: string;
initials?: string;
date: Date;
location: string;
}

View File

@ -1,6 +1,6 @@
import {Component, Input, OnInit} from '@angular/core';
import {Component, OnInit} from '@angular/core';
import {AppDefaults} from '../models/appDefaults.interface';
import {SettingsService} from '../services/settings.service';
import {TestingLocation} from '../models/testingLocation.interface';
@Component({
selector: 'app-navbar',
@ -8,16 +8,20 @@ import {TestingLocation} from '../models/testingLocation.interface';
styleUrls: ['./navbar.component.scss']
})
export class NavbarComponent implements OnInit {
@Input() testingLocation: TestingLocation;
settings: AppDefaults;
locationId: string;
constructor(private settingsService: SettingsService) {
this.settingsService.settings.subscribe(settings => {
this.settings = settings;
this.locationId = !this.hasDefaultId ? this.settings.locationId : '0000';
});
}
get hasDefaultId(): boolean {
return (!this.settings || this.settings.locationId === '0000');
}
ngOnInit(): void {
}
get locationId(): string {
const settings = this.settingsService.getSettings();
return settings.locationId;
}
}

View File

@ -2,7 +2,6 @@
class="print-layout"
[style]="{width: pageWidth, height: pageHeight}"
[gdRows]="pagesToGridFractions"
[gdGap]="marginWidth"
gdAlignColumns="center"
gdAlignRows="center"
>
@ -11,12 +10,11 @@
[gdColumns]="colsToGridFractions"
gdAlignColumns="center"
gdAlignRows="center"
[gdGap]="columnGap"
>
<app-label-layout
*ngFor="let col of columns"
[barCode]="barCode"
[initials]="initials"
[initials]="initials.toUpperCase()"
[dateCreated]="dateCreated"
></app-label-layout>
</div>

View File

@ -16,6 +16,7 @@ describe('PrintLayoutComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(PrintLayoutComponent);
component = fixture.componentInstance;
component.initials = 'abcde';
fixture.detectChanges();
});

View File

@ -37,14 +37,6 @@ export class PrintLayoutComponent implements AfterViewInit {
return this.layout.dimensions.pageWidth;
}
get marginWidth(): string {
return this.layout.dimensions.marginWidth;
}
get columnGap(): string {
return this.layout.dimensions.columnGap;
}
get pages() {
return Array(this.layout.numCopies).fill('');
}

View File

@ -17,7 +17,7 @@
>
<app-print-layout
[barCode]="barCode"
[initials]="initials"
[initials]="initials.toUpperCase()"
[dateCreated]="dateCreated"
></app-print-layout>
</mat-card-content>
@ -26,7 +26,7 @@
#saveAndPrintButton="matButton"
mat-flat-button
class="btn-lg"
[color]="isSaved ? '' : 'accent'"
color="accent"
(click)="saveAndPrint()"
autofocus
fxFlex="33%"
@ -37,8 +37,7 @@
class="btn-lg"
routerLink="/"
fxFlex="33%"
[color]="isSaved ? 'accent' : ''"
><mat-icon>done</mat-icon> Done</button>
><mat-icon>cancel</mat-icon> Cancel</button>
</mat-card-actions>
</mat-card>
</div>
@ -46,7 +45,7 @@
<div id="media-print">
<app-print-layout
[barCode]="barCode"
[initials]="initials"
[initials]="initials.toUpperCase()"
[dateCreated]="dateCreated"
></app-print-layout>
</div>

View File

@ -39,7 +39,11 @@
display: none;
}
#media-print { display: block !important; }
#media-print {
display: block !important;
margin: 0 !important;
padding: 0 !important;
}
#media-screen { display: none !important; }
}

View File

@ -1,6 +1,6 @@
import {AfterViewInit, ChangeDetectorRef, Component, OnInit, ViewChild} from '@angular/core';
import {MatButton} from '@angular/material/button';
import {ActivatedRoute} from '@angular/router';
import {ActivatedRoute, Router} from '@angular/router';
import {createQrCodeValue} from '../_util/qrCode';
import {AppDefaults} from '../models/appDefaults.interface';
import {LabelLayout} from '../models/labelLayout.interface';
@ -26,6 +26,7 @@ export class PrintComponent implements AfterViewInit {
constructor(
private api: ApiService,
private route: ActivatedRoute,
private router: Router,
private changeDetector: ChangeDetectorRef,
private settingsService: SettingsService,
private cacheService: CacheService,
@ -33,13 +34,14 @@ export class PrintComponent implements AfterViewInit {
this.dateCreated = new Date();
this.route.queryParamMap.subscribe(queryParamMap => {
this.barCode = queryParamMap.get('barCode');
this.initials = queryParamMap.get('initials');
this.initials = queryParamMap.get('initials').toUpperCase();
});
this.settings = this.settingsService.getSettings();
this.isSaved = false;
this.save(s => {
this.isSaved = true;
this.changeDetector.detectChanges();
});
}
@ -102,8 +104,7 @@ export class PrintComponent implements AfterViewInit {
saveAndPrint() {
this.save(s => {
window.print();
this.doneButton.focus();
this.changeDetector.detectChanges();
this.router.navigate(['/']);
});
}
}

View File

@ -45,20 +45,14 @@
color="accent"
[disabled]="hasErrors"
(click)="goPrint()"
fxFlex="33%"
fxFlex="50%"
>Next <mat-icon>navigate_next</mat-icon></button>
<button
mat-flat-button
class="btn-lg"
(click)="resetForm()"
fxFlex="33%"
fxFlex="50%"
><mat-icon>restore</mat-icon> Reset</button>
<button
mat-flat-button
class="btn-lg"
routerLink="/"
fxFlex="33%"
><mat-icon>cancel</mat-icon> Cancel</button>
</mat-card-actions>
</mat-card>

View File

@ -41,7 +41,7 @@ export class SampleComponent implements AfterViewInit {
get queryParams(): Params {
return {
barCode: this.barCodeValue.slice(0, 9),
initials: this.initialsValue,
initials: this.initialsValue.toUpperCase(),
};
}
@ -50,7 +50,7 @@ export class SampleComponent implements AfterViewInit {
}
get initialsValue(): string {
return this.initialsFormControl.value;
return this.initialsFormControl.value.toUpperCase();
}
get hasBarCode(): boolean {
@ -58,7 +58,7 @@ export class SampleComponent implements AfterViewInit {
}
get hasInitials(): boolean {
return this.settings.initialsRegExp.test(this.initialsValue);
return this.settings.initialsRegExp.test(this.initialsValue.toUpperCase());
}
get hasErrors(): boolean {

View File

@ -1,5 +1,6 @@
import {Injectable} from '@angular/core';
import createClone from 'rfdc';
import {BehaviorSubject} from 'rxjs';
import serializeJs from 'serialize-javascript';
import {defaultOptions} from '../config/defaults';
import {AppDefaults, AppDefaultsOptions} from '../models/appDefaults.interface';
@ -11,10 +12,15 @@ export class SettingsService {
// Default form field and data values
defaults: AppDefaults = new AppDefaults(defaultOptions);
// Observable to subscribe to settings updates
settings = new BehaviorSubject<AppDefaults>(this.defaults);
// Deserializes settings from local storage and returns AppDefaults instance
getStoredSettings(): AppDefaults {
// tslint:disable-next-line:no-eval
return new AppDefaults(eval(`(${localStorage.getItem('settings')})`));
const storedSettings = new AppDefaults(eval(`(${localStorage.getItem('settings')})`));
this.settings.next(storedSettings);
return storedSettings;
}
// Returns true if settings are found in local storage
@ -40,6 +46,7 @@ export class SettingsService {
});
localStorage.setItem('settings', serializeJs(settings));
this.settings.next(settings);
return settings;
}

View File

@ -10,13 +10,14 @@
</mat-card-header>
<mat-card-content fxLayout="row wrap" fxLayoutAlign="center center">
<mat-form-field class="location-input" fxFlex="95%">
<mat-form-field class="location-input" fxFlex="95%" >
<mat-label>Location ID #</mat-label>
<input
#locationIdInput="matInput"
matInput
type="text"
[formControl]="locationIdFormControl"
(keyup.enter)="save()"
>
<mat-error *ngIf="locationIdFormControl.hasError('required')">This field is required.</mat-error>
<mat-error *ngIf="locationIdFormControl.hasError('pattern')">Please enter exactly 4 digits.</mat-error>
@ -27,7 +28,19 @@
#labelLayoutSelect="matSelect"
[formControl]="labelLayoutFormControl"
>
<mat-option *ngFor="let layout of labelLayouts" [value]="layout.type">{{layout.name}}</mat-option>
<mat-option *ngFor="let layout of labelLayouts" [value]="layout.id" [ngStyle]="{height: 'inherit', width: 'inherit'}">
<div fxLayout="row" fxLayoutAlign="center center" fxLayoutGap="40px" (click)="selectLabelLayout(layout)">
<div fxFlex="50%" [ngStyle]="{whiteSpace: 'normal', lineHeight: '1.2'}" (click)="selectLabelLayout(layout)">
{{layout.name}}
</div>
<div fxFlex="50%" (click)="selectLabelLayout(layout)">
<app-circle-qrcode-single *ngIf="layout.id === 'circle_qrcode_single'" [sample]="fakeSample"></app-circle-qrcode-single>
<app-circle-qrcode-double *ngIf="layout.id === 'circle_qrcode_double'" [sample]="fakeSample"></app-circle-qrcode-double>
<app-rectangle-code128 *ngIf="layout.id === 'rectangle_code128'" [sample]="fakeSample"></app-rectangle-code128>
<app-rectangle-datamatrix *ngIf="layout.id === 'rectangle_datamatrix'" [sample]="fakeSample"></app-rectangle-datamatrix>
</div>
</div>
</mat-option>
</mat-select>
<mat-error *ngIf="labelLayoutFormControl.hasError('required')">This field is required.</mat-error>
</mat-form-field>
@ -38,6 +51,7 @@
matInput
type="number"
[formControl]="numCopiesFormControl"
(keyup.enter)="save()"
>
<mat-error *ngIf="numCopiesFormControl.hasError('required')">This field is required.</mat-error>
<mat-error *ngIf="numCopiesFormControl.hasError('pattern')">Please enter only 2-5 letters.</mat-error>

View File

@ -1,9 +1,12 @@
import {Component, OnInit} from '@angular/core';
import {AfterViewInit, ChangeDetectorRef, Component, OnInit, ViewChild} from '@angular/core';
import {FormControl, Validators} from '@angular/forms';
import {MatInput} from '@angular/material/input';
import {Router} from '@angular/router';
import {createQrCodeValue} from '../_util/qrCode';
import {labelLayouts} from '../config/defaults';
import {AppDefaults} from '../models/appDefaults.interface';
import {LabelLayout} from '../models/labelLayout.interface';
import {Sample} from '../models/sample.interface';
import {SettingsService} from '../services/settings.service';
@Component({
@ -11,23 +14,28 @@ import {SettingsService} from '../services/settings.service';
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss']
})
export class SettingsComponent implements OnInit {
export class SettingsComponent implements AfterViewInit {
settings: AppDefaults;
numCopiesFormControl: FormControl;
labelLayoutFormControl: FormControl;
locationIdFormControl: FormControl;
labelLayouts: LabelLayout[];
fakeSample: Sample;
fakeBarcodeValue: string;
@ViewChild('locationIdInput') locationIdInput: MatInput;
constructor(
private router: Router,
private settingsService: SettingsService
private settingsService: SettingsService,
private changeDetector: ChangeDetectorRef,
) {
this.settings = this.settingsService.getSettings();
this.numCopiesFormControl = new FormControl(this.settings.numCopies, [
Validators.required,
]);
this.labelLayoutFormControl = new FormControl(this.settings.labelLayout.type, [
this.labelLayoutFormControl = new FormControl(this.settings.labelLayout.id, [
Validators.required,
]);
@ -37,21 +45,51 @@ export class SettingsComponent implements OnInit {
]);
this.labelLayouts = Object.values(labelLayouts);
this._loadFakeData();
}
get hasInfo(): boolean {
return this.numCopiesFormControl.valid && this.locationIdFormControl.valid;
}
ngOnInit(): void {
ngAfterViewInit(): void {
this.locationIdInput.focus();
this.changeDetector.detectChanges();
}
save() {
this.settingsService.saveSettings({
labelLayout: labelLayouts[this.labelLayoutFormControl.value],
numCopies: this.numCopiesFormControl.value,
locationId: this.locationIdFormControl.value,
});
this.router.navigate(['/']);
if (this.hasInfo) {
this.settingsService.saveSettings({
labelLayout: labelLayouts[this.labelLayoutFormControl.value],
numCopies: this.numCopiesFormControl.value,
locationId: this.locationIdFormControl.value,
});
this.router.navigate(['/']);
}
}
// Make some fake data for sample barcodes
private _loadFakeData() {
this.fakeSample = {
barcode: '',
student_id: '123456789',
initials: 'ABCDE',
date: new Date(),
location: this.settings.locationId,
};
this.fakeBarcodeValue = createQrCodeValue(
this.fakeSample.student_id,
this.fakeSample.initials,
this.fakeSample.date,
this.fakeSample.location
);
this.fakeSample.barcode = this.fakeBarcodeValue;
}
selectLabelLayout(layout: LabelLayout) {
this.labelLayoutFormControl.patchValue(layout);
}
}

View File

@ -0,0 +1,9 @@
import {Sample} from '../models/sample.interface';
export const mockSample: Sample = {
barcode: '123456789-ABCDE-202101231234-0101',
student_id: '123456789',
date: new Date(),
initials: 'ABCDE',
location: '0101',
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -442,4 +442,31 @@
}
}
}
svg.label-layout-format {
text, tspan {
font-style: normal;
font-variant: normal;
font-weight: bold;
font-stretch: normal;
font-size: 2.11666656px; // 6pt
line-height: 1.25;
font-family: monospace;
text-align: center;
font-variant-ligatures: normal;
font-variant-caps: normal;
font-variant-numeric: normal;
font-feature-settings: normal;
font-variant-numeric: normal;
font-feature-settings: normal;
text-anchor: middle;
}
@media print {
.label-layout-border {
stroke: transparent;
}
}
}
}