From ab8748c2e81a3eeac50011031754eacea2eca0f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:41:09 +0000 Subject: [PATCH 1/3] Initial plan From f6e1068df1329a6ad763512b20c0511e0d7b7272 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:55:03 +0000 Subject: [PATCH 2/3] Make duration optional for ongoing medications Co-authored-by: waseefakhtar <4093820+waseefakhtar@users.noreply.github.com> --- .../doseapp/analytics/AnalyticsHelper.kt | 4 +- .../doseapp/data/MedicationDatabase.kt | 7 +- .../doseapp/data/entity/MedicationEntity.kt | 2 +- .../doseapp/domain/model/Medication.kt | 2 +- .../addmedication/AddMedicationRoute.kt | 135 +++++++++++++----- .../addmedication/StartDatePickerDialog.kt | 81 +++++++++++ .../viewmodel/AddMedicationViewModel.kt | 17 ++- .../MedicationConfirmRoute.kt | 18 ++- .../waseefakhtar/doseapp/util/TimeUtils.kt | 4 +- app/src/main/res/values/strings.xml | 9 ++ 10 files changed, 233 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/StartDatePickerDialog.kt diff --git a/app/src/main/java/com/waseefakhtar/doseapp/analytics/AnalyticsHelper.kt b/app/src/main/java/com/waseefakhtar/doseapp/analytics/AnalyticsHelper.kt index 502135f4..752acfaf 100644 --- a/app/src/main/java/com/waseefakhtar/doseapp/analytics/AnalyticsHelper.kt +++ b/app/src/main/java/com/waseefakhtar/doseapp/analytics/AnalyticsHelper.kt @@ -20,7 +20,7 @@ class AnalyticsHelper( fun trackNotificationShown(medication: Medication) { val params = bundleOf( MEDICATION_TIME to medication.medicationTime.toFormattedDateString(), - MEDICATION_END_DATE to medication.endDate.toFormattedDateString(), + MEDICATION_END_DATE to (medication.endDate?.toFormattedDateString() ?: "ongoing"), NOTIFICATION_TIME to Date().toFormattedDateString() ) logEvent(AnalyticsEvents.MEDICATION_NOTIFICATION_SHOWN, params) @@ -29,7 +29,7 @@ class AnalyticsHelper( fun trackNotificationScheduled(medication: Medication) { val params = bundleOf( MEDICATION_TIME to medication.medicationTime.toFormattedDateString(), - MEDICATION_END_DATE to medication.endDate.toFormattedDateString(), + MEDICATION_END_DATE to (medication.endDate?.toFormattedDateString() ?: "ongoing"), NOTIFICATION_TIME to Date().toFormattedDateString() ) logEvent(AnalyticsEvents.MEDICATION_NOTIFICATION_SCHEDULED, params) diff --git a/app/src/main/java/com/waseefakhtar/doseapp/data/MedicationDatabase.kt b/app/src/main/java/com/waseefakhtar/doseapp/data/MedicationDatabase.kt index a741fd0f..d5a13893 100644 --- a/app/src/main/java/com/waseefakhtar/doseapp/data/MedicationDatabase.kt +++ b/app/src/main/java/com/waseefakhtar/doseapp/data/MedicationDatabase.kt @@ -11,9 +11,10 @@ import com.waseefakhtar.doseapp.data.entity.MedicationEntity @Database( entities = [MedicationEntity::class], - version = 4, + version = 5, autoMigrations = [ - AutoMigration(from = 3, to = 4, spec = MedicationDatabase.AutoMigration::class) + AutoMigration(from = 3, to = 4, spec = MedicationDatabase.AutoMigration3To4::class), + AutoMigration(from = 4, to = 5) ] ) @TypeConverters(Converters::class) @@ -22,5 +23,5 @@ abstract class MedicationDatabase : RoomDatabase() { abstract val dao: MedicationDao @DeleteColumn(tableName = "MedicationEntity", columnName = "timesOfDay") @RenameColumn(tableName = "MedicationEntity", fromColumnName = "date", toColumnName = "medicationTime") - class AutoMigration : AutoMigrationSpec + class AutoMigration3To4 : AutoMigrationSpec } diff --git a/app/src/main/java/com/waseefakhtar/doseapp/data/entity/MedicationEntity.kt b/app/src/main/java/com/waseefakhtar/doseapp/data/entity/MedicationEntity.kt index b702bc9b..e217bad4 100644 --- a/app/src/main/java/com/waseefakhtar/doseapp/data/entity/MedicationEntity.kt +++ b/app/src/main/java/com/waseefakhtar/doseapp/data/entity/MedicationEntity.kt @@ -14,7 +14,7 @@ data class MedicationEntity( val dosage: Int, val recurrence: String, val startDate: Date?, - val endDate: Date, + val endDate: Date?, val medicationTaken: Boolean, val medicationTime: Date, @ColumnInfo(defaultValue = "TABLET") diff --git a/app/src/main/java/com/waseefakhtar/doseapp/domain/model/Medication.kt b/app/src/main/java/com/waseefakhtar/doseapp/domain/model/Medication.kt index fdefea78..3f17ec8f 100644 --- a/app/src/main/java/com/waseefakhtar/doseapp/domain/model/Medication.kt +++ b/app/src/main/java/com/waseefakhtar/doseapp/domain/model/Medication.kt @@ -12,7 +12,7 @@ data class Medication( val dosage: Int, val frequency: String, val startDate: Date, - val endDate: Date, + val endDate: Date?, val medicationTaken: Boolean, val medicationTime: Date, val type: MedicationType = MedicationType.getDefault() diff --git a/app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/AddMedicationRoute.kt b/app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/AddMedicationRoute.kt index 209cddac..0dc27a62 100644 --- a/app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/AddMedicationRoute.kt +++ b/app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/AddMedicationRoute.kt @@ -32,6 +32,7 @@ import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.DateRange import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox @@ -107,6 +108,7 @@ fun AddMedicationScreen( var frequency by rememberSaveable { mutableStateOf(Frequency.EVERYDAY.name) } var startDate by rememberSaveable { mutableLongStateOf(0L) } var endDate by rememberSaveable { mutableLongStateOf(0L) } + var isOngoing by rememberSaveable { mutableStateOf(false) } var showDatePicker by remember { mutableStateOf(false) } val selectedTimes = rememberSaveable( @@ -169,6 +171,7 @@ fun AddMedicationScreen( frequency = frequency, startDate = startDate, endDate = endDate, + isOngoing = isOngoing, selectedTimes = selectedTimes, type = medicationType, onInvalidate = { @@ -227,39 +230,86 @@ fun AddMedicationScreen( Column( modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - TextField( + Row( modifier = Modifier.fillMaxWidth(), - readOnly = true, - value = buildDateRangeText(startDate, endDate), - onValueChange = {}, - label = { Text(stringResource(R.string.duration)) }, - placeholder = { Text(stringResource(R.string.select_duration)) }, - trailingIcon = { Icon(Icons.Default.DateRange, contentDescription = null) }, - interactionSource = remember { MutableInteractionSource() }.also { interactionSource -> - LaunchedEffect(interactionSource) { - interactionSource.interactions.collect { - if (it is PressInteraction.Release) { - showDatePicker = true + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox( + checked = isOngoing, + onCheckedChange = { isOngoing = it }, + ) + Text( + text = stringResource(R.string.ongoing_medication), + modifier = Modifier.clickable { isOngoing = !isOngoing }, + style = MaterialTheme.typography.bodyMedium, + ) + } + + if (isOngoing) { + TextField( + modifier = Modifier.fillMaxWidth(), + readOnly = true, + value = buildStartDateText(startDate), + onValueChange = {}, + label = { Text(stringResource(R.string.start_date)) }, + placeholder = { Text(stringResource(R.string.select_start_date)) }, + trailingIcon = { Icon(Icons.Default.DateRange, contentDescription = null) }, + interactionSource = remember { MutableInteractionSource() }.also { interactionSource -> + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { + if (it is PressInteraction.Release) { + showDatePicker = true + } } } - } + }, + ) + } else { + TextField( + modifier = Modifier.fillMaxWidth(), + readOnly = true, + value = buildDateRangeText(startDate, endDate), + onValueChange = {}, + label = { Text(stringResource(R.string.duration)) }, + placeholder = { Text(stringResource(R.string.select_duration)) }, + trailingIcon = { Icon(Icons.Default.DateRange, contentDescription = null) }, + interactionSource = remember { MutableInteractionSource() }.also { interactionSource -> + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { + if (it is PressInteraction.Release) { + showDatePicker = true + } + } + } + }, + ) + } + } + + if (isOngoing) { + StartDatePickerDialog( + showDialog = showDatePicker, + selectedDate = startDate, + onDismiss = { showDatePicker = false }, + onDateSelected = { start -> + startDate = start + }, + ) + } else { + DateRangePickerDialog( + showDialog = showDatePicker, + startDate = startDate, + endDate = endDate, + onDismiss = { showDatePicker = false }, + onDateSelected = { start, end -> + startDate = start + endDate = end }, ) } - DateRangePickerDialog( - showDialog = showDatePicker, - startDate = startDate, - endDate = endDate, - onDismiss = { showDatePicker = false }, - onDateSelected = { start, end -> - startDate = start - endDate = end - }, - ) - Spacer(modifier = Modifier.padding(4.dp)) Text( text = stringResource(R.string.schedule), @@ -358,6 +408,7 @@ private fun validateMedication( frequency: String, startDate: Long, endDate: Long, + isOngoing: Boolean, selectedTimes: List, type: MedicationType, onInvalidate: (Int) -> Unit, @@ -374,14 +425,22 @@ private fun validateMedication( return } - if (startDate == 0L || endDate == 0L) { - onInvalidate(R.string.duration) - return - } + // For ongoing medications, only startDate is required + if (isOngoing) { + if (startDate == 0L) { + onInvalidate(R.string.start_date) + return + } + } else { + if (startDate == 0L || endDate == 0L) { + onInvalidate(R.string.duration) + return + } - if (startDate >= endDate) { - onInvalidate(R.string.duration) - return + if (startDate >= endDate) { + onInvalidate(R.string.duration) + return + } } if (selectedTimes.isEmpty()) { @@ -395,7 +454,7 @@ private fun validateMedication( dosage = dosage, frequency = frequency, startDate = Date(startDate), - endDate = Date(endDate), + endDate = if (isOngoing) null else Date(endDate), medicationTimes = selectedTimes, type = type ) @@ -526,6 +585,16 @@ private fun buildDateRangeText( "${Date(startDate).toFormattedMonthDateString()} - ${Date(endDate).toFormattedMonthDateString()}" } +@Composable +private fun buildStartDateText( + startDate: Long, +): String = + if (startDate == 0L) { + "" + } else { + Date(startDate).toFormattedMonthDateString() + } + @Composable private fun MedicationTypeBox( type: MedicationType, diff --git a/app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/StartDatePickerDialog.kt b/app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/StartDatePickerDialog.kt new file mode 100644 index 00000000..0439a75f --- /dev/null +++ b/app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/StartDatePickerDialog.kt @@ -0,0 +1,81 @@ +package com.waseefakhtar.doseapp.feature.addmedication + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.waseefakhtar.doseapp.R +import com.waseefakhtar.doseapp.extension.toFormattedDateString +import java.util.Calendar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun StartDatePickerDialog( + showDialog: Boolean, + selectedDate: Long?, + onDismiss: () -> Unit, + onDateSelected: (startDate: Long) -> Unit, +) { + if (showDialog) { + val today = Calendar.getInstance().timeInMillis + val datePickerState = rememberDatePickerState( + initialSelectedDateMillis = if (selectedDate == 0L) today else selectedDate, + ) + + DatePickerDialog( + onDismissRequest = onDismiss, + confirmButton = { + TextButton( + enabled = datePickerState.selectedDateMillis != null, + onClick = { + datePickerState.selectedDateMillis?.let { start -> + onDateSelected(start) + } + onDismiss() + }, + ) { + Text(stringResource(R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.cancel)) + } + }, + ) { + DatePicker( + state = datePickerState, + showModeToggle = false, + title = { + Text( + text = stringResource(R.string.select_start_date), + modifier = Modifier.padding( + start = 24.dp, + end = 12.dp, + top = 16.dp, + ), + ) + }, + headline = { + datePickerState.selectedDateMillis?.toFormattedDateString()?.let { + Text( + modifier = Modifier.padding( + start = 24.dp, + end = 12.dp, + bottom = 12.dp, + ), + text = it + ) + } + }, + ) + } + } +} diff --git a/app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/viewmodel/AddMedicationViewModel.kt b/app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/viewmodel/AddMedicationViewModel.kt index 6bbc0303..d45e3a9e 100644 --- a/app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/viewmodel/AddMedicationViewModel.kt +++ b/app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/viewmodel/AddMedicationViewModel.kt @@ -19,12 +19,16 @@ class AddMedicationViewModel @Inject constructor( @ApplicationContext private val context: Context ) : ViewModel() { + companion object { + private const val DEFAULT_ONGOING_DURATION_DAYS = 365 // 1 year for ongoing medications + } + fun createMedications( name: String, dosage: Int, frequency: String, startDate: Date, - endDate: Date, + endDate: Date?, medicationTimes: List, type: MedicationType, ): List { @@ -36,7 +40,16 @@ class AddMedicationViewModel @Inject constructor( } val oneDayInMillis = 86400 * 1000 // Number of milliseconds in one day - val durationInDays = ((endDate.time + oneDayInMillis - startDate.time) / oneDayInMillis).toInt() + + // For ongoing medications (null endDate), use a default duration + val effectiveEndDate = endDate ?: run { + val calendar = Calendar.getInstance() + calendar.time = startDate + calendar.add(Calendar.DAY_OF_YEAR, DEFAULT_ONGOING_DURATION_DAYS) + calendar.time + } + + val durationInDays = ((effectiveEndDate.time + oneDayInMillis - startDate.time) / oneDayInMillis).toInt() // Always create at least one occurrence if we have a valid duration val numOccurrences = if (durationInDays > 0) maxOf(1, durationInDays / interval) else 0 diff --git a/app/src/main/java/com/waseefakhtar/doseapp/feature/medicationconfirm/MedicationConfirmRoute.kt b/app/src/main/java/com/waseefakhtar/doseapp/feature/medicationconfirm/MedicationConfirmRoute.kt index 2097d2c5..6b0fc853 100644 --- a/app/src/main/java/com/waseefakhtar/doseapp/feature/medicationconfirm/MedicationConfirmRoute.kt +++ b/app/src/main/java/com/waseefakhtar/doseapp/feature/medicationconfirm/MedicationConfirmRoute.kt @@ -108,15 +108,27 @@ fun MedicationConfirmScreen( ) val medication = medications.first() - Text( - text = pluralStringResource( + val confirmationText = if (medication.endDate != null) { + pluralStringResource( id = R.plurals.all_set, count = medications.size, medication.name, medications.size, medication.frequency.lowercase(), medication.endDate.toFormattedDateString() - ), + ) + } else { + pluralStringResource( + id = R.plurals.all_set_ongoing, + count = medications.size, + medication.name, + medications.size, + medication.frequency.lowercase(), + stringResource(R.string.ongoing) + ) + } + Text( + text = confirmationText, style = MaterialTheme.typography.titleMedium ) } diff --git a/app/src/main/java/com/waseefakhtar/doseapp/util/TimeUtils.kt b/app/src/main/java/com/waseefakhtar/doseapp/util/TimeUtils.kt index 82ffc4d7..a6de2741 100644 --- a/app/src/main/java/com/waseefakhtar/doseapp/util/TimeUtils.kt +++ b/app/src/main/java/com/waseefakhtar/doseapp/util/TimeUtils.kt @@ -17,7 +17,9 @@ fun getTimeRemaining(medication: Medication): String { val timeDiff = abs(currentTime.time - dateBefore.time) // If the medication is scheduled for a future date, display days remaining - if (medication.medicationTime.toFormattedDateString() != medication.endDate.toFormattedDateString()) { + // For ongoing medications (null endDate), use medication time for comparison + val endDateString = medication.endDate?.toFormattedDateString() + if (endDateString != null && medication.medicationTime.toFormattedDateString() != endDateString) { val daysRemaining = TimeUnit.DAYS.convert(timeDiff, TimeUnit.MILLISECONDS) + 1L return stringResource(id = R.string.time_remaining, daysRemaining, stringResource(id = R.string.days)) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bf268cb4..442bd6ee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -70,6 +70,15 @@ Monthly Duration Select Duration + Ongoing (no end date) + Start Date + Select Start Date + ongoing + + Your %1$s reminders are ready, with %2$s alert %3$s, %4$s. + Your %1$s reminders are ready, with %2$s alerts %3$s, %4$s. + Your %1$s reminders are ready, with %2$s alerts %3$s, %4$s. + %d day %d days From d1a71dd5a5d18296dc09590dd83024a2861a7963 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:58:05 +0000 Subject: [PATCH 3/3] Address code review comments: improve accessibility and documentation Co-authored-by: waseefakhtar <4093820+waseefakhtar@users.noreply.github.com> --- .../feature/addmedication/AddMedicationRoute.kt | 15 ++++++++++++--- .../viewmodel/AddMedicationViewModel.kt | 8 +++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/AddMedicationRoute.kt b/app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/AddMedicationRoute.kt index 0dc27a62..df549b89 100644 --- a/app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/AddMedicationRoute.kt +++ b/app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/AddMedicationRoute.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions @@ -61,6 +62,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign @@ -233,16 +235,23 @@ fun AddMedicationScreen( verticalArrangement = Arrangement.spacedBy(8.dp), ) { Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .toggleable( + value = isOngoing, + onValueChange = { isOngoing = it }, + role = Role.Checkbox, + ) + .padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { Checkbox( checked = isOngoing, - onCheckedChange = { isOngoing = it }, + onCheckedChange = null, ) Text( text = stringResource(R.string.ongoing_medication), - modifier = Modifier.clickable { isOngoing = !isOngoing }, + modifier = Modifier.padding(start = 8.dp), style = MaterialTheme.typography.bodyMedium, ) } diff --git a/app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/viewmodel/AddMedicationViewModel.kt b/app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/viewmodel/AddMedicationViewModel.kt index d45e3a9e..80d544cd 100644 --- a/app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/viewmodel/AddMedicationViewModel.kt +++ b/app/src/main/java/com/waseefakhtar/doseapp/feature/addmedication/viewmodel/AddMedicationViewModel.kt @@ -20,7 +20,13 @@ class AddMedicationViewModel @Inject constructor( ) : ViewModel() { companion object { - private const val DEFAULT_ONGOING_DURATION_DAYS = 365 // 1 year for ongoing medications + /** + * Default duration for ongoing medications (no end date). + * We create 1 year of medication reminders initially for ongoing medications. + * This allows users to have a practical number of reminders while the app + * can be enhanced in the future to automatically extend this duration. + */ + private const val DEFAULT_ONGOING_DURATION_DAYS = 365 } fun createMedications(