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 10 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
3 changes: 3 additions & 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,9 @@ 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 = Cannot read the barcode, contact your supervisor for assistance.
barcode_scanner.warning.no_barcode_detected = Cannot detect the barcode, try again. If the problem continues, please contact your supervisor for help.
barcode_scanner.warning.not_supported = Barcode scanner is not supported by this browser.
birth_date = Birth date
branding = Branding
branding.favicon.field = Small icon
Expand Down
3 changes: 3 additions & 0 deletions api/resources/translations/messages-es.properties
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,9 @@ 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, comuníquese con su supervisor para obtener ayuda.
barcode_scanner.warning.no_barcode_detected = No se puede detectar el código de barras, inténtelo de nuevo. Si el problema continúa, comuníquese con su supervisor para obtener ayuda.
barcode_scanner.warning.not_supported = El escáner de código de barras no es compatible con este navegador.
birth_date = Fecha del parto
branding =
branding.favicon.field =
Expand Down
3 changes: 3 additions & 0 deletions api/resources/translations/messages-fr.properties
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,9 @@ 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, contactez votre superviseur pour obtenir de l'aide.
barcode_scanner.warning.no_barcode_detected = Impossible de détecter le code-barres, réessayez. Si le problème persiste, veuillez contacter votre superviseur pour obtenir de l'aide.
barcode_scanner.warning.not_supported = Le scanner de codes-barres n'est pas pris en charge par ce navigateur.
birth_date = Date de naissance
branding =
branding.favicon.field =
Expand Down
3 changes: 3 additions & 0 deletions api/resources/translations/messages-id.properties
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,9 @@ 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 = Tidak dapat membaca barcode, hubungi supervisor Anda untuk mendapatkan bantuan.
barcode_scanner.warning.no_barcode_detected = Tidak dapat mendeteksi barcode, coba lagi. Jika masalah terus berlanjut, harap hubungi supervisor Anda untuk mendapatkan bantuan.
barcode_scanner.warning.not_supported = Pemindai barcode tidak didukung oleh browser ini.
birth_date = Tanggal Lahir
branding =
branding.favicon.field =
Expand Down
3 changes: 3 additions & 0 deletions api/resources/translations/messages-sw.properties
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,9 @@ 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 = Haiwezi kusoma msimbo pau, wasiliana na msimamizi wako kwa usaidizi.
barcode_scanner.warning.no_barcode_detected = Haiwezi kutambua msimbo pau, jaribu tena. Tatizo likiendelea, tafadhali wasiliana na msimamizi wako kwa usaidizi.
barcode_scanner.warning.not_supported = Kichanganuzi cha msimbo pau hakitumiki na kivinjari hiki.
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', async () => await this.scanBarcode(imageHolder));

const input = this.windowRef.document.getElementById('barcode-scanner-input');
input?.addEventListener('click', async () => await this.telemetryService.record(`${this.TELEMETRY_PREFIX}:open`));
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 = this.translateService.instant('barcode_scanner.warning.not_supported');
this.globalAction.setSnackbarContent(message);
console.error(message);
this.feedbackService.submit(message);
this.telemetryService.record(`${this.TELEMETRY_PREFIX}:not_supported`);
return false;
}

return true;
}

private async scanBarcode(imageHolder) {
try {
this.telemetryService.record(`${this.TELEMETRY_PREFIX}:scan`);
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('barcode_scanner.warning.no_barcode_detected');
this.globalAction.setSnackbarContent(message);
this.telemetryService.record(`${this.TELEMETRY_PREFIX}:code_no_detected`);

} catch (error) {
const message = this.translateService.instant('barcode_scanner.error.cannot_read_barcode');
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';
}
}