Skip to content

Commit

Permalink
Merge pull request #594 from hpi-dhc/issue/567-show-affected-medicati…
Browse files Browse the repository at this point in the history
…ons-for-gene

Show affected medications for gene
  • Loading branch information
jannis-baum authored Apr 14, 2023
2 parents 9b263d6 + 186368e commit 22cde6e
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 170 deletions.
18 changes: 9 additions & 9 deletions app/integration_test/search_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:mocktail/mocktail.dart';

class MockSearchCubit extends MockCubit<SearchState> implements SearchCubit {
class MockDrugListCubit extends MockCubit<DrugListState> implements DrugListCubit {
@override
FilterState get filter => FilterState.initial();
}

void main() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
final mockSearchCubit = MockSearchCubit();
final mockDrugListCubit = MockDrugListCubit();

binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.onlyPumps;
final loadedDrugs = [
Expand Down Expand Up @@ -50,12 +50,12 @@ void main() {
];
group('integration test for the search page', () {
testWidgets('test search page in loading state', (tester) async {
when(() => mockSearchCubit.state).thenReturn(SearchState.loading());
when(() => mockDrugListCubit.state).thenReturn(DrugListState.loading());
await tester.pumpWidget(BlocProvider.value(
value: mockSearchCubit,
value: mockDrugListCubit,
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: SearchPage(cubit: mockSearchCubit),
home: SearchPage(cubit: mockDrugListCubit),
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
Expand All @@ -70,14 +70,14 @@ void main() {
});

testWidgets('test search page in loaded state', (tester) async {
when(() => mockSearchCubit.state)
.thenReturn(SearchState.loaded(loadedDrugs, FilterState.initial()));
when(() => mockDrugListCubit.state)
.thenReturn(DrugListState.loaded(loadedDrugs, FilterState.initial()));

await tester.pumpWidget(BlocProvider.value(
value: mockSearchCubit,
value: mockDrugListCubit,
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: SearchPage(cubit: mockSearchCubit),
home: SearchPage(cubit: mockDrugListCubit),
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
Expand Down
4 changes: 1 addition & 3 deletions app/lib/common/routing/router.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';

import '../../common/module.dart';
import '../../faq/module.dart';
import '../../login/module.dart';
import '../../report/module.dart';
Expand Down
3 changes: 3 additions & 0 deletions app/lib/common/utilities/genome_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ Future<void> fetchAndSaveLookups() async {
matchingLookups[diplotype.gene] = lookup;
}

// uncomment to make user have CYP2D6 lookupkey 0.0
// matchingLookups['CYP2D6'] = lookupsHashMap['CYP2D6__*100/*100']!;

UserData.instance.lookups = matchingLookups;
await UserData.save();

Expand Down
100 changes: 100 additions & 0 deletions app/lib/common/widgets/drug_list/builder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import '../../module.dart';

List<Widget> buildDrugList(
BuildContext context,
DrugListState state, {
String? noDrugsMessage,
}) =>
state.when(
initial: () => [Container()],
error: () => [errorIndicator(context.l10n.err_generic)],
loaded: (drugs, filter) => _buildDrugCards(
context,
drugs,
filter,
noDrugsMessage: noDrugsMessage,
),
loading: () => [loadingIndicator()],
);

List<Widget> _buildDrugCards(
BuildContext context,
List<Drug> drugs,
FilterState filter, {
String? noDrugsMessage,
}) {
final filteredDrugs = filter.filter(drugs);
if (filteredDrugs.isEmpty && noDrugsMessage != null) {
return [errorIndicator(noDrugsMessage)];
}
return [
SizedBox(height: 8),
...filteredDrugs.map((drug) => Column(children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: DrugCard(
onTap: () => context.router
.push(DrugRoute(drug: drug))
.then((_) => context.read<DrugListCubit>().search()),
drug: drug)),
SizedBox(height: 12)
]))
];
}

class DrugCard extends StatelessWidget {
const DrugCard({
required this.onTap,
required this.drug,
});

final VoidCallback onTap;
final Drug drug;

@override
Widget build(BuildContext context) {
final warningLevel = drug.userGuideline()?.annotations.warningLevel;

return RoundedCard(
onTap: onTap,
padding: EdgeInsets.all(8),
radius: 16,
color: warningLevel?.color ?? PharMeTheme.onSurfaceColor,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Icon(warningLevel?.icon ?? Icons.help_outline_rounded),
SizedBox(width: 4),
Text(
drug.name.capitalize(),
style: PharMeTheme.textTheme.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
),
]),
SizedBox(height: 4),
if (drug.annotations.brandNames.isNotEmpty) ...[
SizedBox(width: 4),
Text(
'(${drug.annotations.brandNames.join(', ')})',
style: PharMeTheme.textTheme.titleMedium,
),
],
SizedBox(height: 8),
Text(
drug.annotations.drugclass,
style: PharMeTheme.textTheme.titleSmall,
),
],
),
),
Icon(Icons.chevron_right_rounded),
],
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import 'dart:async';

import 'package:freezed_annotation/freezed_annotation.dart';

import '../../common/models/drug/cached_drugs.dart';
import '../../common/module.dart';
import '../../models/drug/cached_drugs.dart';
import '../../module.dart';

part 'cubit.freezed.dart';

class SearchCubit extends Cubit<SearchState> {
SearchCubit() : super(SearchState.initial()) {
loadDrugs();
class DrugListCubit extends Cubit<DrugListState> {
DrugListCubit({
FilterState? initialFilter,
}) : super(DrugListState.initial()) {
loadDrugs(filter: initialFilter);
}

Timer? searchTimeout;
Expand All @@ -24,7 +26,7 @@ class SearchCubit extends Cubit<SearchState> {
state.whenOrNull(
initial: loadDrugs,
loaded: (allDrugs, filter) => emit(
SearchState.loaded(
DrugListState.loaded(
allDrugs,
FilterState.from(
filter,
Expand All @@ -39,27 +41,32 @@ class SearchCubit extends Cubit<SearchState> {
}

Future<void> loadDrugs({
FilterState? filter,
bool updateIfNull = true,
bool useCache = true,
}) async {
filter = filter ??
state.whenOrNull(loaded: (_, filter) => filter) ??
FilterState.initial();

if (useCache) {
final drugs = CachedDrugs.instance.drugs;
if (drugs != null) {
emit(SearchState.loaded(drugs, FilterState.initial()));
emit(DrugListState.loaded(drugs, filter));
return;
}
if (!updateIfNull) {
emit(SearchState.error());
emit(DrugListState.error());
return;
}
}

emit(SearchState.loading());
emit(DrugListState.loading());
try {
await updateCachedDrugs();
await loadDrugs(updateIfNull: false);
await loadDrugs(updateIfNull: false, filter: filter);
} catch (error) {
emit(SearchState.error());
emit(DrugListState.error());
}
}

Expand All @@ -71,48 +78,71 @@ class FilterState {
required this.query,
required this.showInactive,
required this.showWarningLevel,
required this.gene,
});

FilterState.initial()
: this(query: '', showInactive: true, showWarningLevel: {
for (var level in WarningLevel.values) level: true
});
: this(
query: '',
showInactive: true,
showWarningLevel: {
for (var level in WarningLevel.values) level: true
},
gene: '',
);

FilterState.from(
FilterState other, {
String? query,
bool? showInactive,
Map<WarningLevel, bool>? showWarningLevel,
String? gene,
}) : this(
query: query ?? other.query,
showInactive: showInactive ?? other.showInactive,
showWarningLevel: {
for (var level in WarningLevel.values)
level: showWarningLevel?[level] ?? other.showWarningLevel[level]!
},
gene: gene ?? other.gene,
);

FilterState.forGene(String gene)
: this(
query: '',
showInactive: true,
showWarningLevel: {
for (var level in WarningLevel.values)
level: level != WarningLevel.none
},
gene: gene,
);

final String query;
final bool showInactive;
final Map<WarningLevel, bool> showWarningLevel;
final String gene;

bool isAccepted(Drug drug) {
final warningLevel = drug.userGuideline()?.annotations.warningLevel;
final guideline = drug.userGuideline();
final warningLevel =
guideline?.annotations.warningLevel ?? WarningLevel.none;
return drug.matches(query: query) &&
(drug.isActive() || showInactive) &&
(showWarningLevel[warningLevel] ?? true);
(showWarningLevel[warningLevel] ?? true) &&
(gene.isBlank || (guideline?.lookupkey.keys.contains(gene) ?? false));
}

List<Drug> filter(List<Drug> drugs) => drugs.filter(isAccepted).toList();
}

@freezed
class SearchState with _$SearchState {
const factory SearchState.initial() = _InitialState;
const factory SearchState.loading() = _LoadingState;
const factory SearchState.loaded(
class DrugListState with _$DrugListState {
const factory DrugListState.initial() = _InitialState;
const factory DrugListState.loading() = _LoadingState;
const factory DrugListState.loaded(
List<Drug> allDrugs,
FilterState filter,
) = _LoadedState;
const factory SearchState.error() = _ErrorState;
const factory DrugListState.error() = _ErrorState;
}
2 changes: 2 additions & 0 deletions app/lib/common/widgets/module.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export 'app.dart';
export 'context_menu.dart';
export 'drug_list/builder.dart';
export 'drug_list/cubit.dart';
export 'headings.dart';
export 'indicators.dart';
export 'page_scaffold.dart';
Expand Down
3 changes: 3 additions & 0 deletions app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@
"gene_page_genotype_tooltip": "The genotype describes your personal variant of this gene.",
"gene_page_phenotype": "Phenotype",
"gene_page_phenotype_tooltip": "The phenotype describes the effect that your personal variant of this gene has on the enzyme it encodes.",
"gene_page_affected_drugs": "Affected drugs",
"gene_page_affected_drugs_tooltip": "The drugs listed here are affected by your variant of this gene.",
"gene_page_no_affected_drugs": "Your variant of this gene has no known effect on any drug.",

"nav_report": "Report",
"tab_report": "Gene report",
Expand Down
2 changes: 2 additions & 0 deletions app/lib/report/module.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import '../common/module.dart';
import '../search/module.dart';
import 'pages/gene.dart';
import 'pages/report.dart';

Expand All @@ -12,5 +13,6 @@ const reportRoutes = AutoRoute(
children: [
AutoRoute(path: '', page: ReportPage),
AutoRoute(page: GenePage),
AutoRoute(page: DrugPage)
],
);
Loading

0 comments on commit 22cde6e

Please sign in to comment.