Skip to content

Commit

Permalink
feat(#6669): search contact using a barcode scanner (#8684)
Browse files Browse the repository at this point in the history
  • Loading branch information
latin-panda committed Nov 16, 2023
1 parent 54cb3cf commit 59a1dbd
Show file tree
Hide file tree
Showing 17 changed files with 455 additions and 20 deletions.
1 change: 1 addition & 0 deletions api/resources/translations/messages-en.properties
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 barre. Veuillez réessayer.
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": []
},
"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
18 changes: 17 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,27 @@ mm-search-bar {
.fa {
font-size: @font-extra-large;
color: @filter-icon-color;
vertical-align: top;
&.fa-qrcode {
font-size: @font-extra-extra-large;
}

&.fa-sliders {
vertical-align: baseline;
}

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

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

.barcode-scanner-input {
display: none;
}
}
}

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 class="fa fa-qrcode" *ngIf="isBarcodeScannerAvailable && showSearchIcon()" (click)="onBarcodeOpen()">
<input type="file" class="barcode-scanner-input" data-type-xml="binary" accept="image/*" (change)="processBarcodeFile($event)"/>
</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
115 changes: 113 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 @@ -24,16 +35,24 @@ export class SearchBarComponent implements AfterContentInit, AfterViewInit, OnDe
@Input() disabled;
@Input() showFilter;
@Input() showSort;
@Input() showBarcodeScanner;
@Input() sortDirection;
@Input() lastVisitedDateExtras;
@Output() sort: EventEmitter<any> = new EventEmitter();
@Output() toggleFilter: EventEmitter<any> = new EventEmitter();
@Output() search: EventEmitter<any> = new EventEmitter();

private readonly TELEMETRY_PREFIX = 'search_by_barcode';
private globalAction: GlobalActions;
private barcodeDetector;
private filters;
private barcodeTypes;
private barcodeImageElement;
windowRef;
subscription: Subscription = new Subscription();
activeFilters: number = 0;
openSearch = false;
isBarcodeScannerAvailable = false;

@ViewChild(FreetextFilterComponent)
freetextFilter: FreetextFilterComponent;
Expand All @@ -42,14 +61,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.canShowBarcodeScanner();
this.searchFiltersService.init(this.freetextFilter);
await this.initBarcodeScanner();
}

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

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

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

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

onBarcodeOpen() {
this.telemetryService.record(`${this.TELEMETRY_PREFIX}:open`);
}

processBarcodeFile($event) {
const input = $event.target;
if (!input.files) {
return;
}
const reader = new FileReader();
reader.addEventListener('load', event => this.barcodeImageElement.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 +159,58 @@ export class SearchBarComponent implements AfterContentInit, AfterViewInit, OnDe
return this.openSearch || !!this.filters?.search;
}

private async canShowBarcodeScanner() {
if (!this.showBarcodeScanner) {
return false;
}

const canUseBarcodeScanner = !this.sessionService.isAdmin() && await this.authService.has(CAN_USE_BARCODE_SCANNER);
if (!canUseBarcodeScanner) {
return false;
}

this.barcodeTypes = await this.windowRef.BarcodeDetector?.getSupportedFormats();

if (
!('BarcodeDetector' in this.windowRef)
|| !this.barcodeTypes?.length
|| this.browserDetectorService.isDesktopUserAgent() // But we won't support it in desktop's browser.
) {
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
1 change: 1 addition & 0 deletions webapp/src/ts/modules/contacts/contacts.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<mm-search-bar
*ngIf="useSearchNewDesign"
[showSort]="true"
[showBarcodeScanner]="true"
[sortDirection]="sortDirection"
[lastVisitedDateExtras]="lastVisitedDateExtras"
(sort)="sort($event)"
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';
}
}

0 comments on commit 59a1dbd

Please sign in to comment.