Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
63fa6bd
Phase 2: group data model (migration 23 to 24) with colour inheritance
claude Aug 25, 2026
6265680
Phase 3: reusable component library for the logging redesign
claude Aug 25, 2026
cd074f3
Merge origin/main (Phase 2 merged as #179, release v0.54.0-beta.1)
claude Aug 25, 2026
d93476e
Phase 4: MetricInput facade + Yes/No and Time input types
claude Aug 25, 2026
ad12e98
Extract shared period-day logic into PeriodDaySync
claude Aug 25, 2026
ed027ad
Add unified LogScreen(date): one screen logs a day, period is a state
claude Aug 25, 2026
b2cf50d
Phase 5 bookkeeping: progress log, map drift note, changelog, lesson
claude Aug 25, 2026
94fb3f8
Fix unresolved semantics property references in the component library
claude Aug 25, 2026
9c4c6ee
Merge branch 'claude/logging-redesign-phase-3' into claude/logging-re…
claude Aug 25, 2026
e6353b0
Merge phase-3 semantics-import fix; fix the same missing import in Ti…
claude Aug 25, 2026
4113e39
Merge branch 'claude/logging-redesign-phase-4' into claude/logging-re…
claude Aug 25, 2026
e76e398
Phase 6: redesign What You Track home with first-class groups
claude Aug 25, 2026
a844719
Phase 7: 2-step category create/edit flow, scale settings step, alarm…
claude Aug 25, 2026
007f11a
Merge remote-tracking branch 'origin/main' into claude/logging-redesi…
claude Aug 26, 2026
1121e0c
Merge branch 'claude/logging-redesign-phase-4' into claude/logging-re…
claude Aug 26, 2026
eede9e1
Merge branch 'claude/logging-redesign-phase-5' into claude/logging-re…
claude Aug 26, 2026
2c723ed
Merge branch 'claude/logging-redesign-phase-6' into claude/logging-re…
claude Aug 26, 2026
790830b
Merge remote-tracking branch 'origin/main' into claude/logging-redesi…
claude Aug 26, 2026
cf515a6
Merge branch 'claude/logging-redesign-phase-5' into claude/logging-re…
claude Aug 26, 2026
e53c2e3
Merge branch 'claude/logging-redesign-phase-6' into claude/logging-re…
claude Aug 26, 2026
86e3233
Merge remote-tracking branch 'origin/main' into claude/logging-redesi…
claude Aug 26, 2026
aabc701
Merge branch 'claude/logging-redesign-phase-6' into claude/logging-re…
claude Aug 26, 2026
e056d8d
Merge remote-tracking branch 'origin/main' into claude/logging-redesi…
claude Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions LESSONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ When implementing the merge itself: don't reach for a `deleteLogsForPeriod`-styl
**Derive range entities from per-unit facts; keep the derived rows persistent for consumers**
When a domain concept is really a series of per-unit facts (a period is days of menstruation, a streak is days of activity), storing it as a single start/end record forces every attribute to be range-global (one flow level for a five-day period) and breeds edge-case bugs at the boundaries (extend, bridge, fragment, reopen). Store the per-unit facts as the source of truth and derive the range records by grouping under a tolerance rule, rebuilding after every mutation. Keep the derived rows persisted in the old table with stable ids (match old rows to new groups by span overlap, earliest survivor wins) so every existing consumer — stats, widgets, export, reminders — keeps reading the same schema. "Ongoing" then stops being a fragile nullable-end guess: a range is open while its last unit is within the tolerance window of today, and a scheduled reconcile closes it at its true last unit otherwise.

**Gate a storage-representation field on dependent data existing, not on a blanket "immutable after creation" rule**
When a field decides how dependent rows are encoded (a category's input type deciding whether logs hold labels, counts, or numbers), "immutable after creation" is stricter than the real invariant and blocks harmless fixes (user picked the wrong type before ever logging). The true constraint is "no dependent data may be reinterpreted": compute it live with a COUNT over the dependent table at edit time, lock the control only when rows exist, and say why in the UI in one plain sentence. Enforce the same guard again in the save/repository path (the UI check can be stale or bypassed), and don't cache the lock in a stored flag — a count is always current, survives deletion of the dependent rows, and needs no migration.

**Insert/upsert flags need a separate edit-by-ID path**
A flag like `allowMultiple` controls whether saving a log upserts an existing row (keyed by date + category) or always inserts a new one. Neither branch handles "update this specific existing row by ID." Routing an edit through `allowMultiple = false` works only when the existing row is uniquely keyed by the natural key; using `allowMultiple = true` creates a duplicate instead. The correct pattern is a dedicated `updateInPlace(existingLog, …)` method. Callers check `existingLog != null` and take this path directly, bypassing the insert/upsert decision entirely.

Expand Down
50 changes: 50 additions & 0 deletions app/src/main/java/com/mapgie/goflo/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ import com.mapgie.goflo.ui.screens.auth.LockViewModel
import com.mapgie.goflo.ui.screens.auth.PinSetupScreen
import com.mapgie.goflo.ui.screens.auth.PinSetupViewModel
import com.mapgie.goflo.ui.screens.disclaimer.DisclaimerScreen
import com.mapgie.goflo.ui.screens.categories.CategoryEditScreen
import com.mapgie.goflo.ui.screens.categories.CategoryEditViewModel
import com.mapgie.goflo.ui.screens.categories.ManageCategoriesScreen
import com.mapgie.goflo.ui.screens.categories.ManageCategoriesViewModel
import com.mapgie.goflo.ui.screens.categories.ManageCategoryValuesScreen
Expand Down Expand Up @@ -535,10 +537,55 @@ private fun MainNavHost(app: GoFloApplication, currentTheme: AppTheme, pendingCa
onNavigateBack = { navController.popBackStack() },
onNavigateToCategory = { categoryId ->
navController.navigate(Screen.ManageCategoryValues.forCategory(categoryId))
},
onNavigateToCreateCategory = { groupId ->
navController.navigate(
if (groupId != null) Screen.CategoryEdit.newInGroup(groupId)
else Screen.CategoryEdit.newCategory
)
}
)
}

// ── Category create/edit flow (logging redesign Phase 7) ──────────────

composable(
route = Screen.CategoryEdit.route,
arguments = listOf(
navArgument("categoryId") { type = NavType.LongType; defaultValue = -1L },
navArgument("groupId") { type = NavType.LongType; defaultValue = -1L },
)
) { backStack ->
val categoryId = backStack.arguments?.getLong("categoryId") ?: -1L
val groupId = backStack.arguments?.getLong("groupId") ?: -1L
val vm: CategoryEditViewModel = viewModel(
key = "category_edit_${categoryId}_$groupId",
factory = CategoryEditViewModel.Factory(
categoryId, groupId,
app.trackingRepository, app.customAlarmRepository, app.applicationContext
)
)
CategoryEditScreen(
viewModel = vm,
onNavigateBack = { navController.popBackStack() },
onCreated = { newId, categoryType ->
navController.popBackStack()
// Default (list-of-values) categories continue to the values
// screen so the user can add their options; every other type
// is fully configured by the create flow already.
if (categoryType == "default") {
navController.navigate(Screen.ManageCategoryValues.forCategory(newId))
}
},
onNavigateToNewAlarm = {
navController.navigate(Screen.EditAlarm.newForCategory(categoryId))
},
onNavigateToEditAlarm = { alarmId ->
navController.navigate(Screen.EditAlarm.forAlarm(alarmId))
},
)
}

composable(
route = Screen.ManageCategoryValues.route,
arguments = listOf(navArgument("categoryId") { type = NavType.LongType })
Expand All @@ -559,6 +606,9 @@ private fun MainNavHost(app: GoFloApplication, currentTheme: AppTheme, pendingCa
onNavigateToEditAlarm = { alarmId ->
navController.navigate(Screen.EditAlarm.forAlarm(alarmId))
},
onNavigateToEditCategory = {
navController.navigate(Screen.CategoryEdit.forCategory(categoryId))
},
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ interface TrackingLogDao {
@Query("SELECT * FROM tracking_logs WHERE date = :date AND categoryId = :categoryId ORDER BY loggedAt ASC, id ASC")
suspend fun getLogsForDateAndCategory(date: String, categoryId: Long): List<TrackingLog>

/** Number of tracking log rows recorded for [categoryId], across all dates. */
@Query("SELECT COUNT(*) FROM tracking_logs WHERE categoryId = :categoryId")
suspend fun countLogsForCategory(categoryId: Long): Int

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertLog(log: TrackingLog): Long

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,17 @@ class TrackingRepository(
return newCount
}

/**
* True when at least one tracking log exists for the category.
*
* Used by the category edit flow (logging redesign Phase 7) to decide
* whether the input type is still editable: the owner-decided rule is
* "the type is fixed once logged" — computed here from the presence of
* dependent data rather than stored as a flag.
*/
suspend fun hasLogs(categoryId: Long): Boolean =
logDao.countLogsForCategory(categoryId) > 0

/** Returns the existing log (with values) for a specific (date, category), or null. */
suspend fun getExistingLog(date: LocalDate, categoryId: Long): TrackingLogWithValues? {
val log = logDao.getLogForDateAndCategory(date.toString(), categoryId) ?: return null
Expand Down
12 changes: 12 additions & 0 deletions app/src/main/java/com/mapgie/goflo/ui/navigation/Screen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ sealed class Screen(val route: String) {
fun forCategory(id: Long) = "manage_category_values/$id"
}

/**
* The 2-step category create/edit flow (logging redesign Phase 7).
* - [categoryId] `> 0` edits an existing category; `-1` creates a new one.
* - [groupId] `> 0` (create only) files the new category into that group on
* save and pre-selects the group's default input type.
*/
data object CategoryEdit : Screen("category_edit?categoryId={categoryId}&groupId={groupId}") {
val newCategory = "category_edit?categoryId=-1&groupId=-1"
fun newInGroup(groupId: Long) = "category_edit?categoryId=-1&groupId=$groupId"
fun forCategory(categoryId: Long) = "category_edit?categoryId=$categoryId&groupId=-1"
}

// ── Custom alarms ──────────────────────────────────────────────────────────

data object CustomAlarms : Screen("custom_alarms")
Expand Down
Loading
Loading