diff --git a/app/src/main/java/com/example/money2/data/local/AppDatabase.kt b/app/src/main/java/com/example/money2/data/local/AppDatabase.kt index ef396e3..e540b8d 100644 --- a/app/src/main/java/com/example/money2/data/local/AppDatabase.kt +++ b/app/src/main/java/com/example/money2/data/local/AppDatabase.kt @@ -3,18 +3,15 @@ package com.example.money2.data.local import androidx.room.Database import androidx.room.RoomDatabase import com.example.money2.data.local.dao.HoldingDao -import com.example.money2.data.local.dao.TransactionDao import com.example.money2.data.local.entity.HoldingEntity import com.example.money2.data.local.entity.HoldingTransactionEntity -import com.example.money2.data.local.entity.TransactionEntity @Database( - entities = [TransactionEntity::class, HoldingEntity::class, HoldingTransactionEntity::class], + entities = [HoldingEntity::class, HoldingTransactionEntity::class], version = 2, exportSchema = false ) abstract class AppDatabase : RoomDatabase() { - abstract fun transactionDao(): TransactionDao abstract fun holdingDao(): HoldingDao companion object { diff --git a/app/src/main/java/com/example/money2/data/local/dao/TransactionDao.kt b/app/src/main/java/com/example/money2/data/local/dao/TransactionDao.kt deleted file mode 100644 index 4492012..0000000 --- a/app/src/main/java/com/example/money2/data/local/dao/TransactionDao.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.money2.data.local.dao - -import androidx.room.* -import com.example.money2.data.local.entity.TransactionEntity -import kotlinx.coroutines.flow.Flow - -@Dao -interface TransactionDao { - @Query("SELECT * FROM transactions ORDER BY date DESC") - fun getAllTransactions(): Flow> - - @Query("SELECT * FROM transactions WHERE type = :type ORDER BY date DESC") - fun getTransactionsByType(type: String): Flow> - - @Query("SELECT SUM(amount) FROM transactions WHERE type = 'INCOME'") - fun getTotalIncome(): Flow - - @Query("SELECT SUM(ABS(amount)) FROM transactions WHERE type = 'EXPENSE'") - fun getTotalExpense(): Flow - - @Query("SELECT * FROM transactions WHERE date BETWEEN :startDate AND :endDate ORDER BY date DESC") - fun getTransactionsByDateRange(startDate: Long, endDate: Long): Flow> - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertTransaction(transaction: TransactionEntity): Long - - @Update - suspend fun updateTransaction(transaction: TransactionEntity) - - @Delete - suspend fun deleteTransaction(transaction: TransactionEntity) - - @Query("DELETE FROM transactions WHERE id = :id") - suspend fun deleteTransactionById(id: Long) -} diff --git a/app/src/main/java/com/example/money2/data/local/entity/TransactionEntity.kt b/app/src/main/java/com/example/money2/data/local/entity/TransactionEntity.kt deleted file mode 100644 index 51fb969..0000000 --- a/app/src/main/java/com/example/money2/data/local/entity/TransactionEntity.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.money2.data.local.entity - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = "transactions") -data class TransactionEntity( - @PrimaryKey(autoGenerate = true) - val id: Long = 0, - val title: String, - val amount: Double, - val category: String, - val type: String, // "INCOME" | "EXPENSE" - val date: Long, - val note: String = "" -) diff --git a/app/src/main/java/com/example/money2/data/local/mapper/TransactionMapper.kt b/app/src/main/java/com/example/money2/data/local/mapper/TransactionMapper.kt deleted file mode 100644 index 0aa00f6..0000000 --- a/app/src/main/java/com/example/money2/data/local/mapper/TransactionMapper.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.money2.data.local.mapper - -import com.example.money2.data.local.entity.TransactionEntity -import com.example.money2.domain.model.Transaction -import com.example.money2.domain.model.TransactionType - -fun TransactionEntity.toDomain() = Transaction( - id = id, title = title, amount = amount, - category = category, - type = TransactionType.valueOf(type), - date = date, note = note -) - -fun Transaction.toEntity() = TransactionEntity( - id = id, title = title, amount = amount, - category = category, type = type.name, - date = date, note = note -) diff --git a/app/src/main/java/com/example/money2/data/repository/TransactionRepositoryImpl.kt b/app/src/main/java/com/example/money2/data/repository/TransactionRepositoryImpl.kt deleted file mode 100644 index 2d25f40..0000000 --- a/app/src/main/java/com/example/money2/data/repository/TransactionRepositoryImpl.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.example.money2.data.repository - -import com.example.money2.data.local.dao.TransactionDao -import com.example.money2.data.local.mapper.toDomain -import com.example.money2.data.local.mapper.toEntity -import com.example.money2.domain.model.Transaction -import com.example.money2.domain.model.TransactionType -import com.example.money2.domain.repository.TransactionRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -class TransactionRepositoryImpl( - private val dao: TransactionDao -) : TransactionRepository { - - override fun getAllTransactions(): Flow> = - dao.getAllTransactions().map { list -> list.map { it.toDomain() } } - - override fun getTransactionsByType(type: TransactionType): Flow> = - dao.getTransactionsByType(type.name).map { list -> list.map { it.toDomain() } } - - override fun getTotalIncome(): Flow = - dao.getTotalIncome().map { it ?: 0.0 } - - override fun getTotalExpense(): Flow = - dao.getTotalExpense().map { it ?: 0.0 } - - override fun getTransactionsByDateRange(startDate: Long, endDate: Long): Flow> = - dao.getTransactionsByDateRange(startDate, endDate).map { list -> list.map { it.toDomain() } } - - override suspend fun insertTransaction(transaction: Transaction): Long = - dao.insertTransaction(transaction.toEntity()) - - override suspend fun updateTransaction(transaction: Transaction) = - dao.updateTransaction(transaction.toEntity()) - - override suspend fun deleteTransaction(transaction: Transaction) = - dao.deleteTransaction(transaction.toEntity()) -} diff --git a/app/src/main/java/com/example/money2/di/AppModule.kt b/app/src/main/java/com/example/money2/di/AppModule.kt index 7448280..feae952 100644 --- a/app/src/main/java/com/example/money2/di/AppModule.kt +++ b/app/src/main/java/com/example/money2/di/AppModule.kt @@ -7,10 +7,8 @@ import com.example.money2.data.local.prefs.EncryptedPrefs import com.example.money2.data.remote.api.MarketApi import com.example.money2.data.repository.HoldingRepositoryImpl import com.example.money2.data.repository.MarketRepositoryImpl -import com.example.money2.data.repository.TransactionRepositoryImpl import com.example.money2.domain.repository.HoldingRepository import com.example.money2.domain.repository.MarketRepository -import com.example.money2.domain.repository.TransactionRepository import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import org.koin.android.ext.koin.androidContext @@ -19,11 +17,8 @@ import org.koin.dsl.module import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import com.example.money2.domain.usecase.AddHoldingUseCase -import com.example.money2.domain.usecase.AddTransactionUseCase -import com.example.money2.domain.usecase.DeleteTransactionUseCase import com.example.money2.domain.usecase.GetDashboardStatsUseCase import com.example.money2.domain.usecase.GetHoldingsUseCase -import com.example.money2.domain.usecase.GetTransactionsUseCase import com.example.money2.presentation.dashboard.DashboardViewModel val databaseModule = module { @@ -35,12 +30,10 @@ val databaseModule = module { ).fallbackToDestructiveMigration() .build() } - single { get().transactionDao() } single { get().holdingDao() } } val repositoryModule = module { - single { TransactionRepositoryImpl(get()) } single { HoldingRepositoryImpl(get()) } } @@ -73,9 +66,6 @@ val networkModule = module { } val useCaseModule = module { - single { GetTransactionsUseCase(get()) } - single { AddTransactionUseCase(get()) } - single { DeleteTransactionUseCase(get()) } single { GetDashboardStatsUseCase(get(), get().selectedCurrencyFlow, get().exchangeRateFlow) } single { GetHoldingsUseCase(get()) } @@ -84,7 +74,6 @@ val useCaseModule = module { val viewModelModule = module { viewModel { DashboardViewModel(get(), get(), get(), get(), get()) } - viewModel { com.example.money2.presentation.transactions.TransactionsViewModel(get(), get(), get()) } viewModel { com.example.money2.presentation.holdings.HoldingsViewModel(get(), get(), get(), get()) } viewModel { com.example.money2.presentation.holdings.detail.HoldingDetailViewModel(get(), get()) } viewModel { com.example.money2.presentation.settings.SettingsViewModel(get(), get()) } diff --git a/app/src/main/java/com/example/money2/domain/model/Transaction.kt b/app/src/main/java/com/example/money2/domain/model/Transaction.kt deleted file mode 100644 index e6b6e05..0000000 --- a/app/src/main/java/com/example/money2/domain/model/Transaction.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.money2.domain.model - -data class Transaction( - val id: Long = 0, - val title: String, - val amount: Double, // 正數=收入, 負數=支出 - val category: String, - val type: TransactionType, - val date: Long, // timestamp in millis - val note: String = "" -) - -enum class TransactionType { INCOME, EXPENSE } diff --git a/app/src/main/java/com/example/money2/domain/repository/TransactionRepository.kt b/app/src/main/java/com/example/money2/domain/repository/TransactionRepository.kt deleted file mode 100644 index c0ecfae..0000000 --- a/app/src/main/java/com/example/money2/domain/repository/TransactionRepository.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.money2.domain.repository - -import com.example.money2.domain.model.Transaction -import com.example.money2.domain.model.TransactionType -import kotlinx.coroutines.flow.Flow - -interface TransactionRepository { - fun getAllTransactions(): Flow> - fun getTransactionsByType(type: TransactionType): Flow> - fun getTotalIncome(): Flow - fun getTotalExpense(): Flow - fun getTransactionsByDateRange(startDate: Long, endDate: Long): Flow> - suspend fun insertTransaction(transaction: Transaction): Long - suspend fun updateTransaction(transaction: Transaction) - suspend fun deleteTransaction(transaction: Transaction) -} diff --git a/app/src/main/java/com/example/money2/domain/usecase/AddTransactionUseCase.kt b/app/src/main/java/com/example/money2/domain/usecase/AddTransactionUseCase.kt deleted file mode 100644 index 4e611e0..0000000 --- a/app/src/main/java/com/example/money2/domain/usecase/AddTransactionUseCase.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.money2.domain.usecase - -import com.example.money2.domain.model.Transaction -import com.example.money2.domain.repository.TransactionRepository - -class AddTransactionUseCase(private val repository: TransactionRepository) { - suspend operator fun invoke(transaction: Transaction) { - if (transaction.title.isBlank()) { - throw IllegalArgumentException("Title cannot be blank") - } - if (transaction.amount <= 0) { - throw IllegalArgumentException("Amount must be greater than zero") - } - repository.insertTransaction(transaction) - } -} diff --git a/app/src/main/java/com/example/money2/domain/usecase/DeleteTransactionUseCase.kt b/app/src/main/java/com/example/money2/domain/usecase/DeleteTransactionUseCase.kt deleted file mode 100644 index 6888fa0..0000000 --- a/app/src/main/java/com/example/money2/domain/usecase/DeleteTransactionUseCase.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.example.money2.domain.usecase - -import com.example.money2.domain.model.Transaction -import com.example.money2.domain.repository.TransactionRepository - -class DeleteTransactionUseCase(private val repository: TransactionRepository) { - suspend operator fun invoke(transaction: Transaction) { - repository.deleteTransaction(transaction) - } -} diff --git a/app/src/main/java/com/example/money2/domain/usecase/GetTransactionsUseCase.kt b/app/src/main/java/com/example/money2/domain/usecase/GetTransactionsUseCase.kt deleted file mode 100644 index 2f04855..0000000 --- a/app/src/main/java/com/example/money2/domain/usecase/GetTransactionsUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.money2.domain.usecase - -import com.example.money2.domain.model.Transaction -import com.example.money2.domain.repository.TransactionRepository -import kotlinx.coroutines.flow.Flow - -class GetTransactionsUseCase(private val repository: TransactionRepository) { - operator fun invoke(): Flow> { - return repository.getAllTransactions() - } -} diff --git a/app/src/main/java/com/example/money2/presentation/navigation/NavRoutes.kt b/app/src/main/java/com/example/money2/presentation/navigation/NavRoutes.kt index 65e5dc3..cd9ce84 100644 --- a/app/src/main/java/com/example/money2/presentation/navigation/NavRoutes.kt +++ b/app/src/main/java/com/example/money2/presentation/navigation/NavRoutes.kt @@ -2,10 +2,8 @@ package com.example.money2.presentation.navigation sealed class NavRoutes(val route: String) { data object Dashboard : NavRoutes("dashboard") - data object Transactions : NavRoutes("transactions") data object Holdings : NavRoutes("holdings") data object Settings : NavRoutes("settings") - data object AddTransaction : NavRoutes("add_transaction") data object AddHolding : NavRoutes("add_holding") data object HoldingDetail : NavRoutes("holding_detail/{symbol}") { diff --git a/app/src/main/java/com/example/money2/presentation/transactions/AddTransactionBottomSheet.kt b/app/src/main/java/com/example/money2/presentation/transactions/AddTransactionBottomSheet.kt deleted file mode 100644 index 92d19a7..0000000 --- a/app/src/main/java/com/example/money2/presentation/transactions/AddTransactionBottomSheet.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.example.money2.presentation.transactions - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp -import com.example.money2.domain.model.TransactionType -import androidx.compose.ui.res.stringResource -import com.example.money2.R - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AddTransactionBottomSheet( - onDismiss: () -> Unit, - onSave: (title: String, amount: Double, category: String, type: TransactionType) -> Unit -) { - var title by remember { mutableStateOf("") } - var amount by remember { mutableStateOf("") } - var category by remember { mutableStateOf("") } - var type by remember { mutableStateOf(TransactionType.EXPENSE) } - - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - - ModalBottomSheet( - onDismissRequest = onDismiss, - sheetState = sheetState - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .padding(bottom = 32.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Text( - text = stringResource(R.string.add_transaction_title), - style = MaterialTheme.typography.titleLarge - ) - - Row(verticalAlignment = Alignment.CenterVertically) { - RadioButton( - selected = type == TransactionType.EXPENSE, - onClick = { type = TransactionType.EXPENSE } - ) - Text(text = stringResource(R.string.expense), modifier = Modifier.padding(end = 16.dp)) - - RadioButton( - selected = type == TransactionType.INCOME, - onClick = { type = TransactionType.INCOME } - ) - Text(text = stringResource(R.string.income)) - } - - OutlinedTextField( - value = title, - onValueChange = { title = it }, - label = { Text(stringResource(R.string.transaction_title)) }, - modifier = Modifier.fillMaxWidth(), - singleLine = true - ) - - OutlinedTextField( - value = amount, - onValueChange = { amount = it }, - label = { Text(stringResource(R.string.transaction_amount)) }, - modifier = Modifier.fillMaxWidth(), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), - singleLine = true - ) - - OutlinedTextField( - value = category, - onValueChange = { category = it }, - label = { Text(stringResource(R.string.transaction_category)) }, - modifier = Modifier.fillMaxWidth(), - singleLine = true - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically - ) { - TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel)) - } - Spacer(modifier = Modifier.width(8.dp)) - Button( - onClick = { - val amountValue = amount.toDoubleOrNull() ?: 0.0 - onSave(title, amountValue, category, type) - }, - enabled = title.isNotBlank() && amount.isNotBlank() && category.isNotBlank() - ) { - Text(stringResource(R.string.save)) - } - } - } - } -} diff --git a/app/src/main/java/com/example/money2/presentation/transactions/TransactionsScreen.kt b/app/src/main/java/com/example/money2/presentation/transactions/TransactionsScreen.kt deleted file mode 100644 index ca9300c..0000000 --- a/app/src/main/java/com/example/money2/presentation/transactions/TransactionsScreen.kt +++ /dev/null @@ -1,145 +0,0 @@ -package com.example.money2.presentation.transactions - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Edit -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import com.example.money2.domain.model.Transaction -import com.example.money2.domain.model.TransactionType -import org.koin.androidx.compose.koinViewModel -import kotlin.math.abs - -import androidx.compose.ui.res.stringResource -import com.example.money2.R - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun TransactionsScreen( - navController: NavController, - viewModel: TransactionsViewModel = koinViewModel() -) { - val transactions by viewModel.transactions.collectAsState() - var showAddDialog by remember { mutableStateOf(false) } - - Scaffold( - containerColor = androidx.compose.ui.graphics.Color.Transparent, - topBar = { - TopAppBar( - title = { Text(stringResource(R.string.transactions_title)) }, - navigationIcon = { - IconButton(onClick = { navController.navigateUp() }) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer - ) - ) - }, - floatingActionButton = { - FloatingActionButton(onClick = { showAddDialog = true }) { - Icon(Icons.Default.Add, contentDescription = "Add Transaction") - } - } - ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - ) { - if (transactions.isEmpty()) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(stringResource(R.string.no_transactions), style = MaterialTheme.typography.bodyLarge) - } - } else { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(transactions, key = { it.id }) { transaction -> - TransactionItem( - transaction = transaction, - onEdit = { /* TODO: Implement edit logic */ }, - onDelete = { viewModel.deleteTransaction(it) } - ) - } - } - } - } - } - - if (showAddDialog) { - AddTransactionBottomSheet( - onDismiss = { showAddDialog = false }, - onSave = { title, amount, category, type -> - val finalAmount = if (type == TransactionType.EXPENSE) -abs(amount) else abs(amount) - viewModel.addTransaction( - Transaction( - title = title, - amount = finalAmount, - category = category, - type = type, - date = System.currentTimeMillis() - ) - ) - showAddDialog = false - } - ) - } -} - -@Composable -fun TransactionItem( - transaction: Transaction, - onEdit: (Transaction) -> Unit, - onDelete: (Transaction) -> Unit -) { - ElevatedCard( - modifier = Modifier.fillMaxWidth() - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { - Text(transaction.title, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium) - Text(transaction.category, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) - } - - Row(verticalAlignment = Alignment.CenterVertically) { - val isIncome = transaction.type == TransactionType.INCOME - val amountColor = if (isIncome) Color(0xFF4CAF50) else MaterialTheme.colorScheme.error - val amountPrefix = if (isIncome) "+" else "" - Text( - text = "$amountPrefix${transaction.amount}", - color = amountColor, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(end = 8.dp) - ) - IconButton(onClick = { onEdit(transaction) }, modifier = Modifier.size(36.dp)) { - Icon(Icons.Default.Edit, contentDescription = "Edit", tint = MaterialTheme.colorScheme.primary) - } - IconButton(onClick = { onDelete(transaction) }, modifier = Modifier.size(36.dp)) { - Icon(Icons.Default.Delete, contentDescription = "Delete", tint = MaterialTheme.colorScheme.error) - } - } - } - } -} diff --git a/app/src/main/java/com/example/money2/presentation/transactions/TransactionsViewModel.kt b/app/src/main/java/com/example/money2/presentation/transactions/TransactionsViewModel.kt deleted file mode 100644 index 9445b0b..0000000 --- a/app/src/main/java/com/example/money2/presentation/transactions/TransactionsViewModel.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.example.money2.presentation.transactions - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.example.money2.domain.model.Transaction -import com.example.money2.domain.usecase.AddTransactionUseCase -import com.example.money2.domain.usecase.DeleteTransactionUseCase -import com.example.money2.domain.usecase.GetTransactionsUseCase -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch - -class TransactionsViewModel( - private val getTransactionsUseCase: GetTransactionsUseCase, - private val addTransactionUseCase: AddTransactionUseCase, - private val deleteTransactionUseCase: DeleteTransactionUseCase -) : ViewModel() { - - private val _transactions = MutableStateFlow>(emptyList()) - val transactions: StateFlow> = _transactions.asStateFlow() - - init { - loadTransactions() - } - - private fun loadTransactions() { - getTransactionsUseCase() - .onEach { list -> - _transactions.value = list - } - .launchIn(viewModelScope) - } - - fun addTransaction(transaction: Transaction) { - viewModelScope.launch { - addTransactionUseCase(transaction) - } - } - - fun deleteTransaction(transaction: Transaction) { - viewModelScope.launch { - deleteTransactionUseCase(transaction) - } - } -}