Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(#6669): barcode scanner #8684

Merged
merged 20 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions api/resources/translations/messages-en.properties
latin-panda marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ app.version.unknown = Unknown - internet connection required.
associated.contact = Associated contact
associated.contact.help = When this user creates reports they will be assigned to this contact
autoreply = autoreply
barcode_scanner.error.cannot_read_barcode = Failed to read the barcode. Retry.
birth_date = Birth date
branding = Branding
branding.favicon.field = Small icon
Expand Down
1 change: 1 addition & 0 deletions api/resources/translations/messages-es.properties
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ app.version.unknown = Desconocido - conexión a internet requerida.
associated.contact = Contacto asociado
associated.contact.help = Cuando este usuario crea informes, será asignado a este contacto
autoreply = autorespuesta
barcode_scanner.error.cannot_read_barcode = No se puede leer el código de barras. Vuelva a intentarlo.
birth_date = Fecha del parto
branding =
branding.favicon.field =
Expand Down
1 change: 1 addition & 0 deletions api/resources/translations/messages-fr.properties
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ app.version.unknown =
associated.contact = Contact associé
associated.contact.help = Lorsque cet utilisateur crée des rapport, ils seront assignés à ce contact
autoreply = auto-réponse
barcode_scanner.error.cannot_read_barcode = Impossible de lire le code-barres. Retenter.
latin-panda marked this conversation as resolved.
Show resolved Hide resolved
birth_date = Date de naissance
branding =
branding.favicon.field =
Expand Down
1 change: 1 addition & 0 deletions api/resources/translations/messages-id.properties
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ app.version.unknown =
associated.contact = Kontak yang berhubungan
associated.contact.help = Ketika pengguna ini membuat laporan, mereka akan dihubungkan kepada kontak ini
autoreply = jawab otomatis
barcode_scanner.error.cannot_read_barcode = Gagal membaca barcode. Mencoba kembali.
birth_date = Tanggal Lahir
branding =
branding.favicon.field =
Expand Down
1 change: 1 addition & 0 deletions api/resources/translations/messages-ne.properties
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ app.version.unknown =
associated.contact = सम्बद्ध सम्पर्क
associated.contact.help = जब यो प्रयोगकर्ताले रिपोर्ट निर्माण गर्छ, तिनिहरु यो सम्पर्कमा निर्दिष्ट हुनेछन्
autoreply = स्वचालित जवाफ
barcode_scanner.error.cannot_read_barcode = बारकोड पढ्न सकिएन। पुन: प्रयास गर्नुहोस्।
birth_date = जन्म मिति
branding =
branding.favicon.field =
Expand Down
1 change: 1 addition & 0 deletions api/resources/translations/messages-sw.properties
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ app.version.unknown =
associated.contact = Nambari za mawasiliano zinazohusika
associated.contact.help = Mhusika huyu akitoa ripoti, zitahusishwa na mwenzi huyu
autoreply = ujumbe wa moja kwa moja
barcode_scanner.error.cannot_read_barcode = Imeshindwa kusoma msimbo pau. Jaribu tena.
birth_date = Tarehe ya kuzaliwa
branding =
branding.favicon.field =
Expand Down
3 changes: 2 additions & 1 deletion config/covid-19/app_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@
],
"can_aggregate_targets": [
"chw_supervisor"
]
],
"can_use_barcode_scanner": []
lorerod marked this conversation as resolved.
Show resolved Hide resolved
},
"place_hierarchy_types": [
"district_hospital",
Expand Down
3 changes: 2 additions & 1 deletion config/default/app_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,8 @@
],
"can_view_old_filter_and_search": [],
"can_view_old_action_bar": [],
"can_default_facility_filter": []
"can_default_facility_filter": [],
"can_use_barcode_scanner": []
},
"uhc": {
"contacts_default_sort": "",
Expand Down
3 changes: 2 additions & 1 deletion config/demo/app_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,8 @@
"program_officer"
],
"can_view_old_filter_and_search": [],
"can_view_old_action_bar": []
"can_view_old_action_bar": [],
"can_use_barcode_scanner": []
},
"uhc": {
"contacts_default_sort": "",
Expand Down
3 changes: 2 additions & 1 deletion config/standard/app_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,8 @@
],
"can_view_uhc_stats": [],
"can_view_old_filter_and_search": [],
"can_view_old_action_bar": []
"can_view_old_action_bar": [],
"can_use_barcode_scanner": []
},
"outgoing_phone_replace": {
"match": "",
Expand Down
16 changes: 15 additions & 1 deletion webapp/src/css/inbox.less
Original file line number Diff line number Diff line change
Expand Up @@ -1189,7 +1189,7 @@ mm-search-bar {

.search-bar-left-icon {
align-self: center;
width: 18px;
min-width: 18px;
height: 20px;
margin: 0 15px;
}
Expand Down Expand Up @@ -1258,11 +1258,25 @@ mm-search-bar {
.fa {
font-size: @font-extra-large;
color: @filter-icon-color;
vertical-align: text-top;
&.fa-qrcode {
font-size: @font-extra-extra-large;
}

&:not(:first-child) {
margin-left: 10px;
}
}

&.disabled .search-bar-left-icon .fa {
color: @inactive-color;
}

#barcode-scanner-input {
visibility: hidden;
position: absolute;
left: -1000px;
latin-panda marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions webapp/src/ts/components/search-bar/search-bar.component.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<section class="mm-search-bar-container" [class.open-search]="openSearch" [class.disabled]="disabled">
<div class="search-bar-left-icon">
<label for="barcode-scanner-input" class="fa fa-qrcode" *ngIf="isBarcodeScannerAvailable && showSearchIcon()">
<input type="file" id="barcode-scanner-input" data-type-xml="binary" accept="image/*"/>
lorerod marked this conversation as resolved.
Show resolved Hide resolved
</label>
<span class="fa fa-search" *ngIf="showSearchIcon()" (click)="toggleMobileSearch()"></span>
<a class="fa fa-chevron-left search-bar-clear" *ngIf="showClearIcon()" (click)="clear()"></a>
</div>
Expand Down
103 changes: 101 additions & 2 deletions webapp/src/ts/components/search-bar/search-bar.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,25 @@ import {
AfterViewInit,
Output,
ViewChild,
Inject,
} from '@angular/core';
import { Store } from '@ngrx/store';
import { combineLatest, Subscription } from 'rxjs';
import { DOCUMENT } from '@angular/common';

import { Selectors } from '@mm-selectors/index';
import { FreetextFilterComponent } from '@mm-components/filters/freetext-filter/freetext-filter.component';
import { ResponsiveService } from '@mm-services/responsive.service';
import { SearchFiltersService } from '@mm-services/search-filters.service';
import { AuthService } from '@mm-services/auth.service';
import { SessionService } from '@mm-services/session.service';
import { GlobalActions } from '@mm-actions/global';
import { TranslateService } from '@mm-services/translate.service';
import { TelemetryService } from '@mm-services/telemetry.service';
import { BrowserDetectorService } from '@mm-services/browser-detector.service';
import { FeedbackService } from '@mm-services/feedback.service';

export const CAN_USE_BARCODE_SCANNER = 'can_use_barcode_scanner';

@Component({
selector: 'mm-search-bar',
Expand All @@ -30,10 +41,15 @@ export class SearchBarComponent implements AfterContentInit, AfterViewInit, OnDe
@Output() toggleFilter: EventEmitter<any> = new EventEmitter();
@Output() search: EventEmitter<any> = new EventEmitter();

private TELEMETRY_PREFIX = 'search_by_barcode';
latin-panda marked this conversation as resolved.
Show resolved Hide resolved
private globalAction: GlobalActions;
private barcodeDetector;
private filters;
windowRef;
latin-panda marked this conversation as resolved.
Show resolved Hide resolved
subscription: Subscription = new Subscription();
activeFilters: number = 0;
openSearch = false;
isBarcodeScannerAvailable = false;

@ViewChild(FreetextFilterComponent)
freetextFilter: FreetextFilterComponent;
Expand All @@ -42,14 +58,26 @@ export class SearchBarComponent implements AfterContentInit, AfterViewInit, OnDe
private store: Store,
private responsiveService: ResponsiveService,
private searchFiltersService: SearchFiltersService,
) { }
private authService: AuthService,
private sessionService: SessionService,
private translateService: TranslateService,
private telemetryService: TelemetryService,
private browserDetectorService: BrowserDetectorService,
private feedbackService: FeedbackService,
@Inject(DOCUMENT) private document: Document,
) {
this.windowRef = this.document.defaultView;
this.globalAction = new GlobalActions(store);
}

ngAfterContentInit() {
this.subscribeToStore();
}

ngAfterViewInit() {
async ngAfterViewInit() {
this.isBarcodeScannerAvailable = await this.checkBarcodeScanner();
this.searchFiltersService.init(this.freetextFilter);
await this.initBarcodeScanner();
}

private subscribeToStore() {
Expand All @@ -67,12 +95,38 @@ export class SearchBarComponent implements AfterContentInit, AfterViewInit, OnDe
this.subscription.add(subscription);
}

private async initBarcodeScanner() {
if (!this.isBarcodeScannerAvailable) {
return;
}

const barcodeTypes = await this.windowRef.BarcodeDetector.getSupportedFormats();
console.info(`Supported barcode formats: ${barcodeTypes?.join(', ')}`);
this.barcodeDetector = new this.windowRef.BarcodeDetector({ formats: barcodeTypes });

const imageHolder = this.windowRef.document.createElement('img');
imageHolder?.addEventListener('load', () => this.scanBarcode(imageHolder)); // NOSONAR

const input = this.windowRef.document.getElementById('barcode-scanner-input');
input?.addEventListener('click', () => this.telemetryService.record(`${this.TELEMETRY_PREFIX}:open`)); // NOSONAR
input?.addEventListener('change', () => {
if (!input.files) {
return;
}
const reader = new FileReader();
reader.addEventListener('load', event => imageHolder.src = event?.target?.result);
reader.readAsDataURL(input.files[0]);
input.value = '';
});
}

clear() {
if (this.disabled) {
return;
}
this.freetextFilter.clear(true);
this.toggleMobileSearch(false);
this.initBarcodeScanner();
}

toggleMobileSearch(forcedValue?) {
Expand Down Expand Up @@ -100,6 +154,51 @@ export class SearchBarComponent implements AfterContentInit, AfterViewInit, OnDe
return this.openSearch || !!this.filters?.search;
}

private async checkBarcodeScanner() {
const canUseBarcodeScanner = !this.sessionService.isAdmin() && await this.authService.has(CAN_USE_BARCODE_SCANNER);

if (!canUseBarcodeScanner) {
return false;
}

// It's okay to show the barcode scanner on mobile's browser, PWA and CHT Android.
// But we won't support it in desktop's browser.
if (!('BarcodeDetector' in this.windowRef) || this.browserDetectorService.isDesktopUserAgent()) {
const message = 'Barcode Detector API is not supported in this browser.';
console.error(message);
this.feedbackService.submit(message);
this.telemetryService.record(`${this.TELEMETRY_PREFIX}:not_supported`);
return false;
}

return true;
}

private async scanBarcode(imageHolder) {
const errorMessageKey = 'barcode_scanner.error.cannot_read_barcode';
this.telemetryService.record(`${this.TELEMETRY_PREFIX}:scan`);

try {
const barcodes = await this.barcodeDetector.detect(imageHolder);
if (barcodes.length) {
this.searchFiltersService.freetextSearch(barcodes[0].rawValue);
this.telemetryService.record(`${this.TELEMETRY_PREFIX}:trigger_search`);
return;
}

const message = this.translateService.instant(errorMessageKey);
this.globalAction.setSnackbarContent(message);
this.telemetryService.record(`${this.TELEMETRY_PREFIX}:barcode_not_detected`);

} catch (error) {
const message = this.translateService.instant(errorMessageKey);
this.globalAction.setSnackbarContent(message);
console.error(message, error);
this.feedbackService.submit(message);
this.telemetryService.record(`${this.TELEMETRY_PREFIX}:failure`);
}
}

ngOnDestroy() {
this.subscription.unsubscribe();
}
Expand Down
5 changes: 5 additions & 0 deletions webapp/src/ts/services/browser-detector.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import * as Bowser from 'bowser';
import { Store } from '@ngrx/store';

import { Selectors } from '@mm-selectors/index';

type VersionSuffix = `` | `-${string}`;
Expand Down Expand Up @@ -50,4 +51,8 @@ export class BrowserDetectorService {

return this.androidAppVersion?.startsWith('v1.');
}

isDesktopUserAgent() {
return this.parser.getPlatformType(true) === 'desktop';
}
}