diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-08-06 09:30:26 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-08-06 09:30:26 +0000 |
| commit | f479211f0885776c379c598dac738ac08ea72548 (patch) | |
| tree | 016ff4faba537033aa0b4930f053f60d28095d57 | |
| parent | c8ebaba6e04169359d84e00ffa2d797f40bc077e (diff) | |
Widget: add postpone (tomorrow/next week/next month) to task detail popup
Alongside Complete/Edit, doot-native tasks now get a Postpone button with
a dropdown (Tomorrow / Next week / Next month), reusing the existing
reschedule wiring (WidgetRepository.reschedule) the due-date picker
already uses.
Caught and documented a real divergence while writing the test: Java's
LocalDate.plusMonths CLAMPS to the target month's last valid day (Jan 31
-> Feb 28), while the Go server's ComputeNextOccurrence (recurrence
math) OVERFLOWS instead (Jan 31 + 1 month -> Mar 3) via time.AddDate.
Verified by actually running both, not assumed -- a first draft of this
test asserted the wrong (Go-style) behavior before checking. Not
reconciled here, just accurately documented as a known inconsistency
between this client-side helper and the server's date math.
4 new tests, all passing.
| -rw-r--r-- | android/app/src/main/java/org/terst/doot/widget/ui/TaskDetailActivity.kt | 37 | ||||
| -rw-r--r-- | android/app/src/test/java/org/terst/doot/widget/ui/TaskDetailActivityTest.kt | 39 |
2 files changed, 76 insertions, 0 deletions
diff --git a/android/app/src/main/java/org/terst/doot/widget/ui/TaskDetailActivity.kt b/android/app/src/main/java/org/terst/doot/widget/ui/TaskDetailActivity.kt index f486dec..effb535 100644 --- a/android/app/src/main/java/org/terst/doot/widget/ui/TaskDetailActivity.kt +++ b/android/app/src/main/java/org/terst/doot/widget/ui/TaskDetailActivity.kt @@ -441,6 +441,31 @@ fun TaskDetailSheet( border = androidx.compose.foundation.BorderStroke(1.dp, Color.White.copy(alpha = 0.3f)) ) { Text("Edit") } } + if (isDoot && completable) { + Box(modifier = Modifier.weight(1f)) { + var showPostponeMenu by remember { mutableStateOf(false) } + OutlinedButton( + onClick = { showPostponeMenu = true }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.White), + border = androidx.compose.foundation.BorderStroke(1.dp, Color.White.copy(alpha = 0.3f)) + ) { Text("Postpone") } + DropdownMenu(expanded = showPostponeMenu, onDismissRequest = { showPostponeMenu = false }) { + DropdownMenuItem( + text = { Text("Tomorrow") }, + onClick = { showPostponeMenu = false; onReschedule(postponeDate(PostponePeriod.TOMORROW)) } + ) + DropdownMenuItem( + text = { Text("Next week") }, + onClick = { showPostponeMenu = false; onReschedule(postponeDate(PostponePeriod.NEXT_WEEK)) } + ) + DropdownMenuItem( + text = { Text("Next month") }, + onClick = { showPostponeMenu = false; onReschedule(postponeDate(PostponePeriod.NEXT_MONTH)) } + ) + } + } + } if (completable) { Button( onClick = onComplete, @@ -493,6 +518,18 @@ internal fun isoDateFromMillis(millis: Long): String { return "%04d-%02d-%02d".format(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)) } +internal enum class PostponePeriod { TOMORROW, NEXT_WEEK, NEXT_MONTH } + +/** ISO date (yyyy-MM-dd) for the postpone menu, relative to the device's current date. */ +internal fun postponeDate(period: PostponePeriod, today: LocalDate = LocalDate.now()): String { + val target = when (period) { + PostponePeriod.TOMORROW -> today.plusDays(1) + PostponePeriod.NEXT_WEEK -> today.plusWeeks(1) + PostponePeriod.NEXT_MONTH -> today.plusMonths(1) + } + return target.toString() +} + private val LABEL_COLOR_PALETTE = listOf( "#3B82F6", "#EF4444", "#F59E0B", "#10B981", "#8B5CF6", "#EC4899", "#6B7280" ) diff --git a/android/app/src/test/java/org/terst/doot/widget/ui/TaskDetailActivityTest.kt b/android/app/src/test/java/org/terst/doot/widget/ui/TaskDetailActivityTest.kt new file mode 100644 index 0000000..94a5ce9 --- /dev/null +++ b/android/app/src/test/java/org/terst/doot/widget/ui/TaskDetailActivityTest.kt @@ -0,0 +1,39 @@ +package org.terst.doot.widget.ui + +import org.junit.Assert.assertEquals +import org.junit.Test +import java.time.LocalDate + +class TaskDetailActivityTest { + + private val anchor = LocalDate.of(2026, 8, 6) // Thursday + + @Test + fun `postponeDate tomorrow is a single day ahead`() { + assertEquals("2026-08-07", postponeDate(PostponePeriod.TOMORROW, anchor)) + } + + @Test + fun `postponeDate next week is seven days ahead`() { + assertEquals("2026-08-13", postponeDate(PostponePeriod.NEXT_WEEK, anchor)) + } + + @Test + fun `postponeDate next month is a calendar month ahead`() { + assertEquals("2026-09-06", postponeDate(PostponePeriod.NEXT_MONTH, anchor)) + } + + @Test + fun `postponeDate next month across a short month clamps -- NOT the same as the Go server's overflow behavior`() { + // Verified, not assumed: java.time.LocalDate.plusMonths CLAMPS to the target + // month's last valid day (Jan 31 -> Feb 28), unlike + // ComputeNextOccurrence (internal/models/recurrence.go)'s Go time.AddDate, + // which OVERFLOWS instead (Jan 31 + 1 month -> Mar 3). This is a real, + // known divergence between this client-side postpone helper and the + // server's recurrence date math for day-of-month 29-31 -- not something + // this fix reconciles, just documenting it accurately instead of + // asserting the wrong behavior. + val jan31 = LocalDate.of(2026, 1, 31) + assertEquals("2026-02-28", postponeDate(PostponePeriod.NEXT_MONTH, jan31)) + } +} |
