mirror of
https://github.com/andwati/ledga.git
synced 2026-10-18 03:12:55 +00:00
v1.3.2 hotfix: migration FK crash + dark-theme insight teaser
MIGRATION_1_2 was using ALTER TABLE to add transactions.accountId, but
SQLite can't add a FOREIGN KEY via ALTER. Room validates schema after
migration and threw IllegalStateException ("Migration didn't properly
handle: transactions") because the FK to mpesa_accounts was missing.
The whole DB stayed at v1, blocking every read/write — which is why
SMS import + SMS receive looked dead too.
Fix: standard SQLite table-swap dance. Create transactions_new with
both FKs declared, INSERT … SELECT old rows in, drop the old table,
rename. All indices recreated. Defensive DROP IF EXISTS in case a
prior failed run left junk.
BentoCard now picks title + overline colors from the tonal background
luminance instead of unconditionally using onSurface. On dark theme
the Insights teaser had near-white text on the (light-theme) pale
green bg — invisible. Now it adapts:
- light tonal bg → dark ink
- dark tonal bg → light ink
HomeScreen Insights teaser switched from hardcoded LedgaAccentSoft to
MaterialTheme.colorScheme.primaryContainer so the bg itself also
adapts to theme.
This commit is contained in:
@@ -27,8 +27,8 @@ android {
|
||||
applicationId = "com.ledga.app"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 4
|
||||
versionName = "1.3.1"
|
||||
versionCode = 5
|
||||
versionName = "1.3.2"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -78,17 +78,68 @@ object Migrations {
|
||||
"ON `goal_contributions` (`transactionId`)"
|
||||
)
|
||||
|
||||
// ---- transactions.accountId + note ----
|
||||
// SQLite ALTER TABLE … ADD COLUMN can't carry a FOREIGN KEY clause —
|
||||
// Room re-asserts the FK constraint via its own validation, but the
|
||||
// raw column add is enough here. The index makes account-scoped
|
||||
// queries cheap (we'll add a lot of them in Phase E).
|
||||
db.execSQL("ALTER TABLE `transactions` ADD COLUMN `accountId` INTEGER")
|
||||
db.execSQL("ALTER TABLE `transactions` ADD COLUMN `note` TEXT")
|
||||
// ---- transactions: add accountId (FK → mpesa_accounts) + note ----
|
||||
// SQLite cannot add a FOREIGN KEY via ALTER TABLE — the constraint
|
||||
// can only be declared at CREATE TABLE time. So we recreate the
|
||||
// table: build the new shape, copy rows, drop the old, rename.
|
||||
// Room runs this migration inside its own transaction, which defers
|
||||
// FK enforcement until commit, so the swap is safe.
|
||||
// Defensive: if a previous failed run left a half-built table, drop it.
|
||||
db.execSQL("DROP TABLE IF EXISTS `transactions_new`")
|
||||
db.execSQL(
|
||||
"CREATE INDEX IF NOT EXISTS `index_transactions_accountId` " +
|
||||
"ON `transactions` (`accountId`)"
|
||||
"""
|
||||
CREATE TABLE `transactions_new` (
|
||||
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`transactionCode` TEXT NOT NULL,
|
||||
`type` TEXT NOT NULL,
|
||||
`amount` REAL NOT NULL,
|
||||
`transactionCost` REAL NOT NULL,
|
||||
`recipientName` TEXT,
|
||||
`recipientPhone` TEXT,
|
||||
`accountNumber` TEXT,
|
||||
`destinationCountry` TEXT,
|
||||
`balance` REAL NOT NULL,
|
||||
`direction` TEXT NOT NULL,
|
||||
`categoryId` INTEGER,
|
||||
`fulizaAmount` REAL,
|
||||
`fulizaOutstanding` REAL,
|
||||
`reversedTransactionCode` TEXT,
|
||||
`rawSms` TEXT NOT NULL,
|
||||
`timestamp` INTEGER NOT NULL,
|
||||
`createdAt` INTEGER NOT NULL,
|
||||
`accountId` INTEGER,
|
||||
`note` TEXT,
|
||||
FOREIGN KEY (`categoryId`) REFERENCES `categories`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL,
|
||||
FOREIGN KEY (`accountId`) REFERENCES `mpesa_accounts`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL
|
||||
)
|
||||
""".trimIndent()
|
||||
)
|
||||
db.execSQL(
|
||||
"""
|
||||
INSERT INTO `transactions_new` (
|
||||
`id`, `transactionCode`, `type`, `amount`, `transactionCost`,
|
||||
`recipientName`, `recipientPhone`, `accountNumber`, `destinationCountry`,
|
||||
`balance`, `direction`, `categoryId`, `fulizaAmount`, `fulizaOutstanding`,
|
||||
`reversedTransactionCode`, `rawSms`, `timestamp`, `createdAt`,
|
||||
`accountId`, `note`
|
||||
)
|
||||
SELECT
|
||||
`id`, `transactionCode`, `type`, `amount`, `transactionCost`,
|
||||
`recipientName`, `recipientPhone`, `accountNumber`, `destinationCountry`,
|
||||
`balance`, `direction`, `categoryId`, `fulizaAmount`, `fulizaOutstanding`,
|
||||
`reversedTransactionCode`, `rawSms`, `timestamp`, `createdAt`,
|
||||
NULL, NULL
|
||||
FROM `transactions`
|
||||
""".trimIndent()
|
||||
)
|
||||
db.execSQL("DROP TABLE `transactions`")
|
||||
db.execSQL("ALTER TABLE `transactions_new` RENAME TO `transactions`")
|
||||
// Indices live with the table — recreate every one the entity declares.
|
||||
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_transactions_transactionCode` ON `transactions` (`transactionCode`)")
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS `index_transactions_categoryId` ON `transactions` (`categoryId`)")
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS `index_transactions_accountId` ON `transactions` (`accountId`)")
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS `index_transactions_timestamp` ON `transactions` (`timestamp`)")
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS `index_transactions_type` ON `transactions` (`type`)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,13 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.ledga.app.ui.theme.LedgaInk
|
||||
import com.ledga.app.ui.theme.LedgaInkDark
|
||||
import com.ledga.app.ui.theme.LedgaMuted
|
||||
import com.ledga.app.ui.theme.LedgaMutedDark
|
||||
import com.ledga.app.ui.theme.LedgaText
|
||||
import com.ledga.app.ui.theme.Radius
|
||||
import com.ledga.app.ui.theme.Space
|
||||
@@ -53,6 +58,13 @@ fun BentoCard(
|
||||
val container = if (tonal) tonalColor else MaterialTheme.colorScheme.surface
|
||||
val border = if (tonal) null else BorderStroke(1.dp, MaterialTheme.colorScheme.outline)
|
||||
|
||||
// For tonal cards we pick text colors that contrast with the tonal bg
|
||||
// rather than the theme's default onSurface — otherwise a light tonal
|
||||
// (e.g. accent-soft) in dark mode renders near-white text on a near-white
|
||||
// bg and disappears.
|
||||
val titleColor = if (tonal) onTonal(tonalColor) else MaterialTheme.colorScheme.onSurface
|
||||
val overlineColor = if (tonal) onTonalMuted(tonalColor) else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
@@ -88,14 +100,14 @@ fun BentoCard(
|
||||
Text(
|
||||
text = overline.uppercase(),
|
||||
style = LedgaText.Overline,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
color = overlineColor,
|
||||
)
|
||||
}
|
||||
if (title != null) {
|
||||
Text(
|
||||
text = title,
|
||||
style = LedgaText.TitleS,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
color = titleColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -107,6 +119,21 @@ fun BentoCard(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a primary text color that has decent contrast against [background].
|
||||
* Hand-rolled rather than relying on theme onSurface because tonal bgs
|
||||
* are explicit colors set by the caller, not picked from MaterialTheme.
|
||||
*
|
||||
* Public so screens can render their own content slot in the matching ink
|
||||
* (e.g. body text inside a tonal card).
|
||||
*/
|
||||
fun onTonal(background: Color): Color =
|
||||
if (background.luminance() > 0.5f) LedgaInk else LedgaInkDark
|
||||
|
||||
/** Muted variant of [onTonal] for captions / overlines. */
|
||||
fun onTonalMuted(background: Color): Color =
|
||||
if (background.luminance() > 0.5f) LedgaMuted else LedgaMutedDark
|
||||
|
||||
/**
|
||||
* Small 40×40 icon tile with rounded corners, category-color tint.
|
||||
* Used in transaction rows, category chips, account avatars.
|
||||
|
||||
@@ -43,6 +43,7 @@ import com.ledga.app.ui.components.v2.LedgaTopBar
|
||||
import com.ledga.app.ui.components.v2.StatCard
|
||||
import com.ledga.app.ui.components.v2.TopBarIconButton
|
||||
import com.ledga.app.ui.components.v2.TransactionRowV2
|
||||
import com.ledga.app.ui.components.v2.onTonal
|
||||
import com.ledga.app.ui.theme.LedgaAccent
|
||||
import com.ledga.app.ui.theme.LedgaAccentDeep
|
||||
import com.ledga.app.ui.theme.LedgaAccentSoft
|
||||
@@ -232,22 +233,26 @@ fun HomeScreen(
|
||||
}
|
||||
|
||||
// ---- Insights teaser ----
|
||||
// Use the theme-adaptive accent-container so the card stays
|
||||
// readable in both light + dark.
|
||||
val insightBg = MaterialTheme.colorScheme.primaryContainer
|
||||
val insightInk = onTonal(insightBg)
|
||||
val topInsight = state.topInsight
|
||||
if (topInsight != null) {
|
||||
BentoCard(
|
||||
overline = topInsight.typeLabel,
|
||||
title = topInsight.headline,
|
||||
icon = Icons.Filled.AutoAwesome,
|
||||
iconTint = LedgaAccentDeep,
|
||||
iconTint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
tonal = true,
|
||||
tonalColor = LedgaAccentSoft,
|
||||
tonalColor = insightBg,
|
||||
onClick = onNavigateToInsights,
|
||||
) {
|
||||
if (!topInsight.body.isNullOrBlank()) {
|
||||
Text(
|
||||
text = topInsight.body,
|
||||
style = LedgaText.BodyM,
|
||||
color = LedgaInk,
|
||||
color = insightInk,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -256,16 +261,16 @@ fun HomeScreen(
|
||||
overline = "Insights",
|
||||
title = "All quiet — nothing unusual",
|
||||
icon = Icons.Filled.AutoAwesome,
|
||||
iconTint = LedgaAccentDeep,
|
||||
iconTint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
tonal = true,
|
||||
tonalColor = LedgaAccentSoft,
|
||||
tonalColor = insightBg,
|
||||
onClick = onNavigateToInsights,
|
||||
) {
|
||||
Text(
|
||||
text = "Ledga watches your activity daily and surfaces anything " +
|
||||
"worth attention. Tap to see how it works.",
|
||||
style = LedgaText.BodyM,
|
||||
color = LedgaInk,
|
||||
color = insightInk,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user