diff --git a/.idea/misc.xml b/.idea/misc.xml index 74dd639..b2c751a 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/src/main/java/com/yovinchen/bookkeeping/data/BookkeepingDao.kt b/app/src/main/java/com/yovinchen/bookkeeping/data/BookkeepingDao.kt index 749ab2c..0e33bae 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/data/BookkeepingDao.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/data/BookkeepingDao.kt @@ -3,6 +3,7 @@ package com.yovinchen.bookkeeping.data import androidx.room.* import com.yovinchen.bookkeeping.model.BookkeepingRecord import com.yovinchen.bookkeeping.model.Category +import com.yovinchen.bookkeeping.model.CategoryStat import com.yovinchen.bookkeeping.model.TransactionType import kotlinx.coroutines.flow.Flow import java.util.Date @@ -49,6 +50,37 @@ interface BookkeepingDao { yearMonth: String ): Flow> + @Query(""" + SELECT m.name as category, + SUM(r.amount) as amount, + COUNT(*) as count, + (SUM(r.amount) * 100.0 / ( + SELECT SUM(amount) + FROM bookkeeping_records + WHERE category = :category + AND strftime('%Y-%m', datetime(date/1000, 'unixepoch')) = :yearMonth + )) as percentage + FROM bookkeeping_records r + JOIN members m ON r.memberId = m.id + WHERE r.category = :category + AND strftime('%Y-%m', datetime(r.date/1000, 'unixepoch')) = :yearMonth + GROUP BY m.name + ORDER BY amount DESC + """) + fun getMemberStatsByCategory( + category: String, + yearMonth: String + ): Flow> + + @Query(""" + SELECT * FROM bookkeeping_records + WHERE category = :category + ORDER BY date DESC + """) + fun getRecordsByCategory( + category: String + ): Flow> + @Insert suspend fun insertRecord(record: BookkeepingRecord): Long @@ -75,4 +107,18 @@ interface BookkeepingDao { @Query("UPDATE bookkeeping_records SET category = :newName WHERE category = :oldName") suspend fun updateRecordCategories(oldName: String, newName: String) + + @Query(""" + SELECT * FROM bookkeeping_records + WHERE memberId IN (SELECT id FROM members WHERE name = :memberName) + AND category = :category + AND date BETWEEN :startDate AND :endDate + ORDER BY date DESC + """) + suspend fun getRecordsByMemberAndCategory( + memberName: String, + category: String, + startDate: Date, + endDate: Date + ): List } diff --git a/app/src/main/java/com/yovinchen/bookkeeping/data/Converters.kt b/app/src/main/java/com/yovinchen/bookkeeping/data/Converters.kt index 84c5bea..b23692a 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/data/Converters.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/data/Converters.kt @@ -3,6 +3,7 @@ package com.yovinchen.bookkeeping.data import androidx.room.TypeConverter import java.time.LocalDateTime import java.time.format.DateTimeFormatter +import java.util.* class Converters { private val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME @@ -18,4 +19,14 @@ class Converters { fun dateToTimestamp(date: LocalDateTime?): String? { return date?.format(formatter) } + + @TypeConverter + fun fromDate(value: Date?): String? { + return value?.time?.toString() + } + + @TypeConverter + fun toDate(timestamp: String?): Date? { + return timestamp?.let { Date(it.toLong()) } + } } diff --git a/app/src/main/java/com/yovinchen/bookkeeping/ui/navigation/MainNavigation.kt b/app/src/main/java/com/yovinchen/bookkeeping/ui/navigation/MainNavigation.kt index ea0bee9..fd7fe2b 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/ui/navigation/MainNavigation.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/navigation/MainNavigation.kt @@ -2,11 +2,9 @@ package com.yovinchen.bookkeeping.ui.navigation import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Home -import androidx.compose.material.icons.filled.List +import androidx.compose.material.icons.automirrored.filled.List +import androidx.compose.material.icons.filled.Analytics import androidx.compose.material.icons.filled.Settings -import androidx.compose.material.icons.outlined.Analytics -import androidx.compose.material.icons.outlined.Home import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar @@ -31,27 +29,25 @@ import java.time.format.DateTimeFormatter sealed class Screen( val route: String, - val icon: ImageVector? = null, - val label: String? = null + val title: String, + val icon: ImageVector? = null ) { - data object Home : Screen("home", Icons.Outlined.Home, "首页") - data object Analysis : Screen("analysis", Icons.Outlined.Analytics, "分析") - data object Settings : Screen("settings", Icons.Default.Settings, "设置") - data object CategoryDetail : Screen( - "category/{category}/{yearMonth}", - Icons.Default.List, - "分类详情" - ) { - fun createRoute(category: String, yearMonth: YearMonth): String = - "category/$category/${yearMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}" + object Home : Screen("home", "记账", Icons.AutoMirrored.Filled.List) + object Analysis : Screen("analysis", "分析", Icons.Default.Analytics) + object Settings : Screen("settings", "设置", Icons.Default.Settings) + object CategoryDetail : Screen("category_detail/{category}/{yearMonth}", "分类详情") { + fun createRoute(category: String, yearMonth: YearMonth): String { + return "category_detail/$category/${yearMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}" + } } - data object MemberDetail : Screen( - "member/{memberName}/{yearMonth}", - Icons.Default.List, - "成员详情" - ) { - fun createRoute(memberName: String, yearMonth: YearMonth): String = - "member/$memberName/${yearMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}" + object MemberDetail : Screen("member_detail/{memberName}/{category}/{yearMonth}", "成员详情") { + fun createRoute(memberName: String, category: String, yearMonth: YearMonth): String { + return "member_detail/$memberName/$category/${yearMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}" + } + } + + companion object { + fun bottomNavigationItems() = listOf(Home, Analysis, Settings) } } @@ -69,14 +65,10 @@ fun MainNavigation( val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route - listOf( - Screen.Home, - Screen.Analysis, - Screen.Settings - ).forEach { screen -> + Screen.bottomNavigationItems().forEach { screen -> NavigationBarItem( - icon = { Icon(screen.icon!!, contentDescription = screen.label) }, - label = { Text(screen.label!!) }, + icon = { Icon(screen.icon!!, contentDescription = screen.title) }, + label = { Text(screen.title) }, selected = currentRoute == screen.route, onClick = { navController.navigate(screen.route) { @@ -105,7 +97,9 @@ fun MainNavigation( navController.navigate(Screen.CategoryDetail.createRoute(category, yearMonth)) }, onNavigateToMemberDetail = { memberName, yearMonth -> - navController.navigate(Screen.MemberDetail.createRoute(memberName, yearMonth)) + // 在这里我们暂时使用一个默认分类,你需要根据实际情况修改这里的逻辑 + val defaultCategory = "默认" + navController.navigate(Screen.MemberDetail.createRoute(memberName, defaultCategory, yearMonth)) } ) } @@ -126,11 +120,15 @@ fun MainNavigation( ) { backStackEntry -> val category = backStackEntry.arguments?.getString("category") ?: return@composable val yearMonthStr = backStackEntry.arguments?.getString("yearMonth") ?: return@composable - val yearMonth = YearMonth.parse(yearMonthStr, DateTimeFormatter.ofPattern("yyyy-MM")) + val yearMonth = YearMonth.parse(yearMonthStr) + CategoryDetailScreen( category = category, - month = yearMonth, - onBack = { navController.popBackStack() } + yearMonth = yearMonth, + onNavigateBack = { navController.popBackStack() }, + onNavigateToMemberDetail = { memberName -> + navController.navigate(Screen.MemberDetail.createRoute(memberName, category, yearMonth)) + } ) } @@ -138,14 +136,18 @@ fun MainNavigation( route = Screen.MemberDetail.route, arguments = listOf( navArgument("memberName") { type = NavType.StringType }, + navArgument("category") { type = NavType.StringType }, navArgument("yearMonth") { type = NavType.StringType } ) ) { backStackEntry -> val memberName = backStackEntry.arguments?.getString("memberName") ?: return@composable + val category = backStackEntry.arguments?.getString("category") ?: return@composable val yearMonthStr = backStackEntry.arguments?.getString("yearMonth") ?: return@composable - val yearMonth = YearMonth.parse(yearMonthStr, DateTimeFormatter.ofPattern("yyyy-MM")) + val yearMonth = YearMonth.parse(yearMonthStr) + MemberDetailScreen( memberName = memberName, + category = category, yearMonth = yearMonth, onNavigateBack = { navController.popBackStack() } ) diff --git a/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/CategoryDetailScreen.kt b/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/CategoryDetailScreen.kt index 5432d81..9400144 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/CategoryDetailScreen.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/CategoryDetailScreen.kt @@ -4,138 +4,131 @@ 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.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.yovinchen.bookkeeping.data.BookkeepingDatabase import com.yovinchen.bookkeeping.model.BookkeepingRecord +import com.yovinchen.bookkeeping.ui.components.CategoryPieChart import com.yovinchen.bookkeeping.ui.components.RecordItem import com.yovinchen.bookkeeping.viewmodel.CategoryDetailViewModel import com.yovinchen.bookkeeping.viewmodel.CategoryDetailViewModelFactory +import java.text.NumberFormat import java.text.SimpleDateFormat import java.time.YearMonth import java.time.format.DateTimeFormatter -import java.util.Locale +import java.util.* @OptIn(ExperimentalMaterial3Api::class) @Composable fun CategoryDetailScreen( category: String, - month: YearMonth, - onBack: () -> Unit + yearMonth: YearMonth, + onNavigateBack: () -> Unit, + onNavigateToMemberDetail: (String) -> Unit, + modifier: Modifier = Modifier ) { val context = LocalContext.current val database = remember { BookkeepingDatabase.getDatabase(context) } val viewModel: CategoryDetailViewModel = viewModel( - factory = CategoryDetailViewModelFactory(database, category, month) + factory = CategoryDetailViewModelFactory(database, category, yearMonth) ) val records by viewModel.records.collectAsState() + val memberStats by viewModel.memberStats.collectAsState() val total by viewModel.total.collectAsState() - val members by viewModel.members.collectAsState() - val groupedRecords = remember(records) { - records.groupBy { record -> - SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(record.date) - } - } Scaffold( topBar = { TopAppBar( - title = { Text("$category - ${month.format(DateTimeFormatter.ofPattern("yyyy年MM月"))}") }, + title = { Text(category) }, navigationIcon = { - IconButton(onClick = onBack) { - Icon(Icons.Default.ArrowBack, contentDescription = "返回") + IconButton(onClick = onNavigateBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回") } } ) } ) { padding -> - Column( - modifier = Modifier + LazyColumn( + modifier = modifier .fillMaxSize() - .padding(padding) + .padding(padding), + horizontalAlignment = Alignment.CenterHorizontally ) { - // 总金额显示 - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "总金额", - style = MaterialTheme.typography.titleMedium - ) - Text( - text = String.format("%.2f", total), - style = MaterialTheme.typography.titleLarge - ) - } + item { + Text( + text = NumberFormat.getCurrencyInstance(Locale.CHINA).format(total), + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(16.dp) + ) } - // 记录列表 - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - groupedRecords.forEach { (date, dayRecords) -> - item { - Card( + item { + CategoryPieChart( + categoryData = memberStats.map { Pair(it.category, it.percentage.toFloat()) }, + memberData = emptyList(), + currentViewMode = false, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + onCategoryClick = { memberName -> onNavigateToMemberDetail(memberName) } + ) + } + + // 按日期分组记录 + val groupedRecords = records.groupBy { record -> + SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(record.date) + }.toSortedMap(compareByDescending { it }) + + groupedRecords.forEach { (date, dayRecords) -> + item { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Column( modifier = Modifier .fillMaxWidth() - .padding(vertical = 4.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + .padding(16.dp) ) { - Column( + // 日期标题和总金额 + Row( modifier = Modifier .fillMaxWidth() - .padding(16.dp) + .padding(bottom = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween ) { - // 日期标签 Text( - text = SimpleDateFormat( - "yyyy年MM月dd日 E", - Locale.CHINESE - ).format(dayRecords.first().date), + text = date, style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + fontWeight = FontWeight.Bold ) + Text( + text = NumberFormat.getCurrencyInstance(Locale.CHINA) + .format(dayRecords.sumOf { it.amount }), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.error + ) + } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) - // 当天的记录 - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - dayRecords.forEachIndexed { index, record -> - RecordItem( - record = record, - onClick = {}, - members = members - ) - - if (index < dayRecords.size - 1) { - HorizontalDivider( - modifier = Modifier.padding(vertical = 4.dp), - color = MaterialTheme.colorScheme.surfaceVariant, - thickness = 0.5.dp - ) - } - } + // 当天的记录列表 + dayRecords.forEach { record -> + RecordItem(record = record) + if (record != dayRecords.last()) { + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp), + color = MaterialTheme.colorScheme.outlineVariant + ) } } } @@ -145,3 +138,43 @@ fun CategoryDetailScreen( } } } + +@Composable +private fun RecordItem( + record: BookkeepingRecord, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = record.memberId.toString(), // 暂时显示 memberId,后续可以通过 MemberDao 获取成员名称 + style = MaterialTheme.typography.titleMedium + ) + if (record.description.isNotBlank()) { + Text( + text = record.description, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Text( + text = SimpleDateFormat("HH:mm", Locale.getDefault()).format(record.date), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Text( + text = NumberFormat.getCurrencyInstance(Locale.CHINA).format(record.amount), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.error + ) + } +} diff --git a/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/MemberDetailScreen.kt b/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/MemberDetailScreen.kt index 99f6129..8237601 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/MemberDetailScreen.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/MemberDetailScreen.kt @@ -2,7 +2,6 @@ package com.yovinchen.bookkeeping.ui.screen 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.filled.ArrowBack import androidx.compose.material3.* @@ -12,7 +11,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.yovinchen.bookkeeping.model.BookkeepingRecord +import com.yovinchen.bookkeeping.data.Record +import com.yovinchen.bookkeeping.ui.components.RecordItem import com.yovinchen.bookkeeping.viewmodel.MemberDetailViewModel import java.text.NumberFormat import java.text.SimpleDateFormat @@ -24,6 +24,7 @@ import java.util.* @Composable fun MemberDetailScreen( memberName: String, + category: String, yearMonth: YearMonth, onNavigateBack: () -> Unit, viewModel: MemberDetailViewModel = viewModel() @@ -31,8 +32,8 @@ fun MemberDetailScreen( val records by viewModel.memberRecords.collectAsState(initial = emptyList()) val totalAmount by viewModel.totalAmount.collectAsState(initial = 0.0) - LaunchedEffect(memberName, yearMonth) { - viewModel.loadMemberRecords(memberName, yearMonth) + LaunchedEffect(memberName, category, yearMonth) { + viewModel.loadMemberRecords(memberName, category, yearMonth) } val groupedRecords = remember(records) { @@ -45,7 +46,7 @@ fun MemberDetailScreen( topBar = { TopAppBar( title = { - Text("$memberName - ${yearMonth.format(DateTimeFormatter.ofPattern("yyyy年MM月"))}") + Text("$category - $memberName") }, navigationIcon = { IconButton(onClick = onNavigateBack) { @@ -55,50 +56,72 @@ fun MemberDetailScreen( ) } ) { padding -> - Column( + LazyColumn( modifier = Modifier .fillMaxSize() .padding(padding) ) { - // 总金额显示 - Card( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Column( + // 第一层:总金额卡片 + item { + Card( modifier = Modifier - .padding(16.dp) + .fillMaxWidth() + .padding(16.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) ) { - Text( - text = "总支出", - style = MaterialTheme.typography.titleMedium - ) - Text( - text = NumberFormat.getCurrencyInstance(Locale.CHINA) - .format(totalAmount), - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold - ) + Column( + modifier = Modifier + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "当前分类总支出", + style = MaterialTheme.typography.titleMedium + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = NumberFormat.getCurrencyInstance(Locale.CHINA) + .format(totalAmount), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold + ) + } } } - // 按日期分组的记录列表 - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - groupedRecords.forEach { (date, dayRecords) -> - item { - Text( - text = date, - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(vertical = 8.dp) - ) - } - items(dayRecords.sortedByDescending { it.date }) { record -> - RecordItem(record = record) + // 第二层:按日期分组的记录列表 + groupedRecords.forEach { (date, dayRecords) -> + item { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = date, + style = MaterialTheme.typography.titleMedium + ) + Text( + text = NumberFormat.getCurrencyInstance(Locale.CHINA) + .format(dayRecords.sumOf { it.amount }), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + } + dayRecords.forEach { record -> + RecordItem(record = record) + } + } } } } @@ -106,44 +129,34 @@ fun MemberDetailScreen( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable -private fun RecordItem(record: BookkeepingRecord) { - Card( - modifier = Modifier.fillMaxWidth() +private fun RecordItem(record: Record) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier.weight(1f) - ) { + Column { + if (record.description.isNotBlank()) { Text( - text = record.category, - style = MaterialTheme.typography.titleMedium - ) - if (record.description.isNotBlank()) { - Text( - text = record.description, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - Text( - text = SimpleDateFormat("HH:mm", Locale.getDefault()).format(record.date), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = record.description, + style = MaterialTheme.typography.bodyMedium ) } Text( - text = NumberFormat.getCurrencyInstance(Locale.CHINA).format(record.amount), - style = MaterialTheme.typography.titleMedium, - color = if (record.amount < 0) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary + text = SimpleDateFormat("HH:mm", Locale.getDefault()) + .format(record.dateTime), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } + Text( + text = NumberFormat.getCurrencyInstance(Locale.CHINA) + .format(record.amount), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary + ) } } diff --git a/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModel.kt b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModel.kt index 1379818..ada1d52 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModel.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModel.kt @@ -1,14 +1,12 @@ package com.yovinchen.bookkeeping.viewmodel import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.yovinchen.bookkeeping.data.BookkeepingDatabase import com.yovinchen.bookkeeping.model.BookkeepingRecord -import com.yovinchen.bookkeeping.model.Member -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch +import com.yovinchen.bookkeeping.model.CategoryStat +import kotlinx.coroutines.flow.* import java.time.YearMonth import java.time.format.DateTimeFormatter @@ -17,36 +15,40 @@ class CategoryDetailViewModel( private val category: String, private val month: YearMonth ) : ViewModel() { + private val recordDao = database.bookkeepingDao() + private val yearMonthStr = month.format(DateTimeFormatter.ofPattern("yyyy-MM")) + private val _records = MutableStateFlow>(emptyList()) - val records: StateFlow> = _records + val records: StateFlow> = _records.asStateFlow() - private val _total = MutableStateFlow(0.0) - val total: StateFlow = _total + private val _memberStats = MutableStateFlow>(emptyList()) + val memberStats: StateFlow> = _memberStats.asStateFlow() - private val _members = MutableStateFlow>(emptyList()) - val members: StateFlow> = _members + val total: StateFlow = records + .map { records -> records.sumOf { it.amount } } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = 0.0 + ) init { - loadRecords() - loadMembers() - } - - private fun loadRecords() { - viewModelScope.launch { - val monthStr = month.format(DateTimeFormatter.ofPattern("yyyy-MM")) - database.bookkeepingDao().getRecordsByCategoryAndMonth(category, monthStr) - .collect { records -> - _records.value = records - _total.value = records.sumOf { it.amount } + recordDao.getRecordsByCategory(category) + .onEach { records -> + _records.value = records.filter { record -> + val recordMonth = YearMonth.from( + DateTimeFormatter.ofPattern("yyyy-MM") + .parse(yearMonthStr) + ) + YearMonth.from(record.date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime()) == recordMonth } - } - } - - private fun loadMembers() { - viewModelScope.launch { - database.memberDao().getAllMembers().collect { members -> - _members.value = members } - } + .launchIn(viewModelScope) + + recordDao.getMemberStatsByCategory(category, yearMonthStr) + .onEach { stats -> + _memberStats.value = stats + } + .launchIn(viewModelScope) } } diff --git a/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/MemberDetailViewModel.kt b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/MemberDetailViewModel.kt index 2e56cf7..939168b 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/MemberDetailViewModel.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/MemberDetailViewModel.kt @@ -5,30 +5,43 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.yovinchen.bookkeeping.data.BookkeepingDatabase import com.yovinchen.bookkeeping.model.BookkeepingRecord -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import java.time.YearMonth -import java.time.format.DateTimeFormatter +import java.time.ZoneId +import java.util.Date class MemberDetailViewModel(application: Application) : AndroidViewModel(application) { - private val recordDao = BookkeepingDatabase.getDatabase(application).bookkeepingDao() + private val database = BookkeepingDatabase.getDatabase(application) + private val recordDao = database.bookkeepingDao() private val _memberRecords = MutableStateFlow>(emptyList()) - val memberRecords: StateFlow> = _memberRecords.asStateFlow() + val memberRecords: StateFlow> = _memberRecords - val totalAmount: StateFlow = _memberRecords - .map { records -> records.sumOf { it.amount } } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = 0.0 - ) + private val _totalAmount = MutableStateFlow(0.0) + val totalAmount: StateFlow = _totalAmount - fun loadMemberRecords(memberName: String, yearMonth: YearMonth) { - recordDao.getRecordsByMemberAndMonth( - memberName, - yearMonth.format(DateTimeFormatter.ofPattern("yyyy-MM")) - ).onEach { records -> + fun loadMemberRecords(memberName: String, category: String, yearMonth: YearMonth) { + viewModelScope.launch { + val startDate = yearMonth.atDay(1).atStartOfDay() + .atZone(ZoneId.systemDefault()) + .toInstant() + .let { Date.from(it) } + + val endDate = yearMonth.atEndOfMonth().atTime(23, 59, 59) + .atZone(ZoneId.systemDefault()) + .toInstant() + .let { Date.from(it) } + + val records = recordDao.getRecordsByMemberAndCategory( + memberName = memberName, + category = category, + startDate = startDate, + endDate = endDate + ) _memberRecords.value = records - }.launchIn(viewModelScope) + _totalAmount.value = records.sumOf { it.amount } + } } }