Skip to content

Commit

Permalink
feat: adapt phase calculation logic, style todos
Browse files Browse the repository at this point in the history
  • Loading branch information
johannesvedder committed Jul 10, 2024
1 parent 4385b35 commit 46ad2d1
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 145 deletions.
19 changes: 18 additions & 1 deletion designer_v2/lib/common_views/utils.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:studyu_designer_v2/theme.dart';
import 'package:studyu_designer_v2/utils/extensions.dart';

typedef WidgetDecorator = Widget Function(Widget widget);

Expand Down Expand Up @@ -43,3 +45,18 @@ extension ColorX on Color {
return withAlpha((alphaScaleFactor * alpha).round());
}
}

Widget interventionPrefix(int rowIdx, BuildContext context) {
final theme = Theme.of(context);
return Row(
children: [
Text(
''.alphabetLetterFrom(rowIdx).toUpperCase(),
style: TextStyle(
color: ThemeConfig.dropdownMenuItemTheme(theme).iconTheme!.color,
),
),
const SizedBox(width: 16.0),
],
);
}
5 changes: 3 additions & 2 deletions designer_v2/lib/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class Config {
static const minSplashTime = 0;

static const formAutosaveDebounce = 1000;

static const participantDropoutDuration = 5;
static const participantInactiveDuration = 3;
}

const kPathSeparator = ' / ';
Expand Down Expand Up @@ -46,5 +49,3 @@ const String signupRouteName = 'signup';
const String forgotPasswordRouteName = 'forgotPassword';
const String recoverPasswordRouteName = 'recoverPassword';
const String errorRouteName = 'error';
const participantDropoutDuration = 5;
const participantInactiveDuration = 3;
132 changes: 58 additions & 74 deletions designer_v2/lib/domain/study_monitoring.dart
Original file line number Diff line number Diff line change
@@ -1,39 +1,12 @@
import 'dart:math';

import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:studyu_core/core.dart';
import 'package:studyu_designer_v2/constants.dart';

class StudyMonitorData {
/// Number of participants who are currently active in the study
/// Active means that the the study has not ended yet and the participant did not drop out
final int activeParticipants;

// Number of participants who are currently inactive in the study for more than 3 days in a row
final int inactiveParticipants;

/// Number of participants who dropped out of the study before the study ended
/// Hint: The is_deleted flag in the study_subject database table marks a participant as dropped out
/// Note: If the participant's last activity exceeds 7 days, they will also be counted as a dropout
final int dropoutParticipants;

/// Number of participants who completed the study
/// Completed means that the participant has reached the end of the study
final int completedParticipants;

/// List of all participants with their monitoring data
final List<StudyMonitorItem> items;

const StudyMonitorData({
required this.activeParticipants,
required this.inactiveParticipants,
required this.dropoutParticipants,
required this.completedParticipants,
required this.items,
});
}

class StudyMonitorItem extends Equatable {
final StudySubject studySubject;
final String participantId;
final String? inviteCode;
final DateTime startedAt;
Expand All @@ -49,6 +22,7 @@ class StudyMonitorItem extends Equatable {
final List<Set<String>> completedTasksPerDay;

const StudyMonitorItem({
required this.studySubject,
required this.participantId,
required this.inviteCode,
required this.startedAt,
Expand All @@ -70,7 +44,7 @@ class StudyMonitorItem extends Equatable {
}

extension StudyMonitoringX on Study {
StudyMonitorData get monitorData {
List<StudyMonitorItem> get monitorData {
final List<StudyMonitorItem> items = [];

final participants = this.participants ?? [];
Expand Down Expand Up @@ -158,6 +132,7 @@ extension StudyMonitoringX on Study {

items.add(
StudyMonitorItem(
studySubject: participant,
participantId: participant.id,
inviteCode: participant.inviteCode,
startedAt: participant.startedAt!,
Expand All @@ -175,50 +150,59 @@ extension StudyMonitoringX on Study {
);
}

int activeParticipants = 0;
int inactiveParticipants = 0;
int dropoutParticipants = 0;
int completedParticipants = 0;

final participantInactiveDays = DateTime.now()
.subtract(const Duration(days: participantInactiveDuration));
final participantDropoutDays = DateTime.now()
.subtract(const Duration(days: participantDropoutDuration));

for (final item in items) {
if (!item.droppedOut) {
if (item.currentDayOfStudy < item.studyDurationInDays) {
if (item.lastActivityAt.isAfter(participantInactiveDays)) {
activeParticipants += 1; // Active
} else {
if (item.lastActivityAt.isBefore(participantDropoutDays)) {
dropoutParticipants += 1; //dropout
} else {
inactiveParticipants += 1; // Inactive
}
}
} else {
completedParticipants += 1; // Completed
}
} else {
dropoutParticipants += 1; // Dropout
}
}
final participantCategories = items.activeParticipants.toList() +
items.inactiveParticipants.toList() +
items.dropoutParticipants.toList() +
items.completedParticipants.toList();
final deepEq = const DeepCollectionEquality.unordered().equals;
assert(deepEq(items, participantCategories));

assert(
activeParticipants +
inactiveParticipants +
dropoutParticipants +
completedParticipants ==
items.length,
);

return StudyMonitorData(
activeParticipants: activeParticipants,
inactiveParticipants: inactiveParticipants,
dropoutParticipants: dropoutParticipants,
completedParticipants: completedParticipants,
items: items,
);
return items;
}
}

extension ListX on List<StudyMonitorItem> {
static final inactiveDate = DateTime.now()
.subtract(const Duration(days: Config.participantInactiveDuration));
static final dropoutDate = DateTime.now()
.subtract(const Duration(days: Config.participantDropoutDuration));

static bool Function(StudyMonitorItem p) get studyStillRunning =>
(StudyMonitorItem p) => p.currentDayOfStudy < p.studyDurationInDays;

static bool Function(StudyMonitorItem p) get inactive => (p) =>
p.lastActivityAt.isBefore(inactiveDate) &&
p.lastActivityAt.isAfter(dropoutDate);

static bool Function(StudyMonitorItem p) get dropout =>
(p) => p.droppedOut || dropoutByDuration(p) && studyStillRunning(p);

static bool Function(StudyMonitorItem p) get dropoutByDuration =>
(p) => p.lastActivityAt.isBefore(dropoutDate);

/// Number of participants who are currently active in the study
/// Active means that the the study has not ended yet and the participant
/// did not drop out
Iterable<StudyMonitorItem> get activeParticipants =>
where((p) => !dropout(p) && !inactive(p) && studyStillRunning(p));

/// Number of participants who are currently inactive in the study for more
/// than [participantDropoutDuration] days in a row
Iterable<StudyMonitorItem> get inactiveParticipants => where(
(p) => !dropout(p) && inactive(p) && studyStillRunning(p),
);

/// Number of participants who dropped out of the study before the study ended
/// Hint: The is_deleted flag in the study_subject database table marks a
/// participant as dropped out
/// Note: If the participant's last activity exceeds
/// [participantDropoutDuration] days, they will also be counted as a dropout
Iterable<StudyMonitorItem> get dropoutParticipants => where(
(p) => dropout(p),
);

/// Number of participants who completed the study
/// Completed means that the participant has reached the end of the study
Iterable<StudyMonitorItem> get completedParticipants =>
where((p) => !dropout(p) && !studyStillRunning(p));
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:studyu_core/core.dart';
import 'package:studyu_designer_v2/common_views/async_value_widget.dart';
import 'package:studyu_designer_v2/common_views/text_hyperlink.dart';
import 'package:studyu_designer_v2/common_views/text_paragraph.dart';
import 'package:studyu_designer_v2/common_views/utils.dart';
import 'package:studyu_designer_v2/features/design/interventions/intervention_form_controller.dart';
import 'package:studyu_designer_v2/features/design/interventions/study_schedule_form_view.dart';
import 'package:studyu_designer_v2/features/design/study_design_page_view.dart';
Expand All @@ -13,16 +14,13 @@ import 'package:studyu_designer_v2/features/forms/form_array_table.dart';
import 'package:studyu_designer_v2/features/study/study_controller.dart';
import 'package:studyu_designer_v2/localization/app_translation.dart';
import 'package:studyu_designer_v2/theme.dart';
import 'package:studyu_designer_v2/utils/extensions.dart';

class StudyDesignInterventionsFormView extends StudyDesignPageWidget {
const StudyDesignInterventionsFormView(super.studyId, {super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(studyControllerProvider(studyId));
final theme = Theme.of(context);

return AsyncValueWidget<Study>(
value: state.study,
data: (study) {
Expand Down Expand Up @@ -68,22 +66,8 @@ class StudyDesignInterventionsFormView extends StudyDesignPageWidget {
emptyDescription:
tr.form_array_interventions_empty_description,
hideLeadingTrailingWhenEmpty: true,
rowPrefix: (context, viewModel, rowIdx) {
return Row(
children: [
Text(
''.alphabetLetterFrom(rowIdx).toUpperCase(),
style: TextStyle(
color:
ThemeConfig.dropdownMenuItemTheme(theme)
.iconTheme!
.color,
),
),
const SizedBox(width: 16.0),
],
);
},
rowPrefix: (context, viewModel, rowIdx) =>
interventionPrefix(rowIdx, context),
);
},
);
Expand Down
Loading

0 comments on commit 46ad2d1

Please sign in to comment.