feat: 增强时间范围筛选和成员统计显示
1. 更新 BookkeepingDao 支持时间范围筛选 2. 重构 CategoryDetailViewModel 及其工厂类 3. 为 MemberStat 添加 Room 注解 4. 改进 CategoryDetailScreen,结合饼状图和列表视图 5. 优化数据库查询和状态管理
This commit is contained in:
parent
c92cc18dde
commit
3296f6d154
@ -3,7 +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.MemberStat
|
||||
import com.yovinchen.bookkeeping.model.TransactionType
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.util.Date
|
||||
@ -51,15 +51,11 @@ interface BookkeepingDao {
|
||||
): Flow<List<BookkeepingRecord>>
|
||||
|
||||
@Query("""
|
||||
SELECT m.name as category,
|
||||
SELECT
|
||||
m.name as member,
|
||||
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
|
||||
(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
|
||||
@ -70,7 +66,7 @@ interface BookkeepingDao {
|
||||
fun getMemberStatsByCategory(
|
||||
category: String,
|
||||
yearMonth: String
|
||||
): Flow<List<CategoryStat>>
|
||||
): Flow<List<MemberStat>>
|
||||
|
||||
@Query("""
|
||||
SELECT * FROM bookkeeping_records
|
||||
@ -81,6 +77,67 @@ interface BookkeepingDao {
|
||||
category: String
|
||||
): Flow<List<BookkeepingRecord>>
|
||||
|
||||
@Query("""
|
||||
SELECT * FROM bookkeeping_records
|
||||
WHERE category = :category
|
||||
AND date BETWEEN :startDate AND :endDate
|
||||
ORDER BY date DESC
|
||||
""")
|
||||
fun getRecordsByCategoryAndDateRange(
|
||||
category: String,
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
): Flow<List<BookkeepingRecord>>
|
||||
|
||||
@Query("""
|
||||
SELECT * FROM bookkeeping_records
|
||||
WHERE memberId IN (SELECT id FROM members WHERE name = :memberName)
|
||||
AND date BETWEEN :startDate AND :endDate
|
||||
AND (:transactionType IS NULL OR type = :transactionType)
|
||||
ORDER BY date DESC
|
||||
""")
|
||||
fun getRecordsByMemberAndDateRange(
|
||||
memberName: String,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
transactionType: TransactionType?
|
||||
): Flow<List<BookkeepingRecord>>
|
||||
|
||||
@Query("""
|
||||
SELECT * FROM bookkeeping_records
|
||||
WHERE memberId IN (SELECT id FROM members WHERE name = :memberName)
|
||||
AND category = :category
|
||||
AND date BETWEEN :startDate AND :endDate
|
||||
AND (:transactionType IS NULL OR type = :transactionType)
|
||||
ORDER BY date DESC
|
||||
""")
|
||||
fun getRecordsByMemberCategoryAndDateRange(
|
||||
memberName: String,
|
||||
category: String,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
transactionType: TransactionType?
|
||||
): Flow<List<BookkeepingRecord>>
|
||||
|
||||
@Query("""
|
||||
SELECT
|
||||
m.name as member,
|
||||
SUM(r.amount) as amount,
|
||||
COUNT(*) as count,
|
||||
(SUM(r.amount) * 100.0 / (SELECT SUM(amount) FROM bookkeeping_records WHERE category = :category AND date BETWEEN :startDate AND :endDate)) as percentage
|
||||
FROM bookkeeping_records r
|
||||
JOIN members m ON r.memberId = m.id
|
||||
WHERE r.category = :category
|
||||
AND r.date BETWEEN :startDate AND :endDate
|
||||
GROUP BY m.name
|
||||
ORDER BY amount DESC
|
||||
""")
|
||||
fun getMemberStatsByCategoryAndDateRange(
|
||||
category: String,
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
): Flow<List<MemberStat>>
|
||||
|
||||
@Insert
|
||||
suspend fun insertRecord(record: BookkeepingRecord): Long
|
||||
|
||||
|
@ -1,8 +1,17 @@
|
||||
package com.yovinchen.bookkeeping.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
|
||||
data class MemberStat(
|
||||
@ColumnInfo(name = "member")
|
||||
val member: String,
|
||||
|
||||
@ColumnInfo(name = "amount")
|
||||
val amount: Double,
|
||||
|
||||
@ColumnInfo(name = "count")
|
||||
val count: Int,
|
||||
|
||||
@ColumnInfo(name = "percentage")
|
||||
val percentage: Double = 0.0
|
||||
)
|
||||
|
@ -36,14 +36,30 @@ sealed class Screen(
|
||||
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"))}"
|
||||
object CategoryDetail : Screen(
|
||||
"category_detail/{category}/{startMonth}/{endMonth}",
|
||||
"分类详情"
|
||||
) {
|
||||
fun createRoute(
|
||||
category: String,
|
||||
startMonth: YearMonth,
|
||||
endMonth: YearMonth
|
||||
): String {
|
||||
return "category_detail/$category/${startMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}/${endMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}"
|
||||
}
|
||||
}
|
||||
object MemberDetail : Screen("member_detail/{memberName}/{category}/{yearMonth}?type={type}", "成员详情") {
|
||||
fun createRoute(memberName: String, category: String, yearMonth: YearMonth, type: AnalysisType): String {
|
||||
return "member_detail/$memberName/$category/${yearMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}?type=${type.name}"
|
||||
object MemberDetail : Screen(
|
||||
"member_detail/{memberName}/{category}/{startMonth}/{endMonth}?type={type}",
|
||||
"成员详情"
|
||||
) {
|
||||
fun createRoute(
|
||||
memberName: String,
|
||||
category: String,
|
||||
startMonth: YearMonth,
|
||||
endMonth: YearMonth,
|
||||
type: AnalysisType
|
||||
): String {
|
||||
return "member_detail/$memberName/$category/${startMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}/${endMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}?type=${type.name}"
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,11 +110,11 @@ fun MainNavigation(
|
||||
|
||||
composable(Screen.Analysis.route) {
|
||||
AnalysisScreen(
|
||||
onNavigateToCategoryDetail = { category, yearMonth ->
|
||||
navController.navigate(Screen.CategoryDetail.createRoute(category, yearMonth))
|
||||
onNavigateToCategoryDetail = { category, startMonth, endMonth ->
|
||||
navController.navigate(Screen.CategoryDetail.createRoute(category, startMonth, endMonth))
|
||||
},
|
||||
onNavigateToMemberDetail = { memberName, yearMonth, analysisType ->
|
||||
navController.navigate(Screen.MemberDetail.createRoute(memberName, "", yearMonth, analysisType))
|
||||
onNavigateToMemberDetail = { memberName, startMonth, endMonth, analysisType ->
|
||||
navController.navigate(Screen.MemberDetail.createRoute(memberName, "", startMonth, endMonth, analysisType))
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -114,51 +130,68 @@ fun MainNavigation(
|
||||
route = Screen.CategoryDetail.route,
|
||||
arguments = listOf(
|
||||
navArgument("category") { type = NavType.StringType },
|
||||
navArgument("yearMonth") { type = NavType.StringType }
|
||||
navArgument("startMonth") { type = NavType.StringType },
|
||||
navArgument("endMonth") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
val category = backStackEntry.arguments?.getString("category") ?: return@composable
|
||||
val yearMonthStr = backStackEntry.arguments?.getString("yearMonth") ?: return@composable
|
||||
val yearMonth = YearMonth.parse(yearMonthStr)
|
||||
|
||||
val category = backStackEntry.arguments?.getString("category") ?: ""
|
||||
val startMonth = YearMonth.parse(
|
||||
backStackEntry.arguments?.getString("startMonth") ?: "",
|
||||
DateTimeFormatter.ofPattern("yyyy-MM")
|
||||
)
|
||||
val endMonth = YearMonth.parse(
|
||||
backStackEntry.arguments?.getString("endMonth") ?: "",
|
||||
DateTimeFormatter.ofPattern("yyyy-MM")
|
||||
)
|
||||
CategoryDetailScreen(
|
||||
category = category,
|
||||
yearMonth = yearMonth,
|
||||
startMonth = startMonth,
|
||||
endMonth = endMonth,
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToMemberDetail = { memberName ->
|
||||
navController.navigate(Screen.MemberDetail.createRoute(memberName, category, yearMonth, AnalysisType.EXPENSE))
|
||||
navController.navigate(
|
||||
Screen.MemberDetail.createRoute(
|
||||
memberName = memberName,
|
||||
category = category,
|
||||
startMonth = startMonth,
|
||||
endMonth = endMonth,
|
||||
type = AnalysisType.EXPENSE
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = Screen.MemberDetail.route,
|
||||
arguments = listOf(
|
||||
navArgument("memberName") { type = NavType.StringType },
|
||||
navArgument("category") { type = NavType.StringType },
|
||||
navArgument("yearMonth") { type = NavType.StringType },
|
||||
navArgument("startMonth") { type = NavType.StringType },
|
||||
navArgument("endMonth") { type = NavType.StringType },
|
||||
navArgument("type") {
|
||||
type = NavType.StringType
|
||||
defaultValue = AnalysisType.EXPENSE.name
|
||||
}
|
||||
)
|
||||
) { 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)
|
||||
val type = backStackEntry.arguments?.getString("type")?.let {
|
||||
try {
|
||||
AnalysisType.valueOf(it)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
AnalysisType.EXPENSE
|
||||
}
|
||||
} ?: AnalysisType.EXPENSE
|
||||
|
||||
val memberName = backStackEntry.arguments?.getString("memberName") ?: ""
|
||||
val category = backStackEntry.arguments?.getString("category") ?: ""
|
||||
val startMonth = YearMonth.parse(
|
||||
backStackEntry.arguments?.getString("startMonth") ?: "",
|
||||
DateTimeFormatter.ofPattern("yyyy-MM")
|
||||
)
|
||||
val endMonth = YearMonth.parse(
|
||||
backStackEntry.arguments?.getString("endMonth") ?: "",
|
||||
DateTimeFormatter.ofPattern("yyyy-MM")
|
||||
)
|
||||
val type = AnalysisType.valueOf(
|
||||
backStackEntry.arguments?.getString("type") ?: AnalysisType.EXPENSE.name
|
||||
)
|
||||
MemberDetailScreen(
|
||||
memberName = memberName,
|
||||
yearMonth = yearMonth,
|
||||
category = category,
|
||||
startMonth = startMonth,
|
||||
endMonth = endMonth,
|
||||
analysisType = type,
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
)
|
||||
|
@ -48,8 +48,9 @@ enum class ViewMode {
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AnalysisScreen(
|
||||
onNavigateToCategoryDetail: (String, YearMonth) -> Unit,
|
||||
onNavigateToMemberDetail: (String, YearMonth, AnalysisType) -> Unit
|
||||
onNavigateToCategoryDetail: (String, YearMonth, YearMonth) -> Unit,
|
||||
onNavigateToMemberDetail: (String, YearMonth, YearMonth, AnalysisType) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val viewModel: AnalysisViewModel = viewModel()
|
||||
val startMonth by viewModel.startMonth.collectAsState()
|
||||
@ -61,11 +62,13 @@ fun AnalysisScreen(
|
||||
var showViewModeMenu by remember { mutableStateOf(false) }
|
||||
var currentViewMode by rememberSaveable { mutableStateOf(ViewMode.CATEGORY) }
|
||||
|
||||
Scaffold { padding ->
|
||||
Scaffold(
|
||||
modifier = modifier.fillMaxSize()
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
// 时间区间选择
|
||||
DateRangePicker(
|
||||
@ -151,9 +154,9 @@ fun AnalysisScreen(
|
||||
.padding(bottom = 16.dp),
|
||||
onCategoryClick = { category ->
|
||||
if (currentViewMode == ViewMode.CATEGORY) {
|
||||
onNavigateToCategoryDetail(category, startMonth)
|
||||
onNavigateToCategoryDetail(category, startMonth, endMonth)
|
||||
} else {
|
||||
onNavigateToMemberDetail(category, startMonth, selectedAnalysisType)
|
||||
onNavigateToMemberDetail(category, startMonth, endMonth, selectedAnalysisType)
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -169,9 +172,9 @@ fun AnalysisScreen(
|
||||
stat = stat,
|
||||
onClick = {
|
||||
if (currentViewMode == ViewMode.CATEGORY && category != null) {
|
||||
onNavigateToCategoryDetail(category, startMonth)
|
||||
onNavigateToCategoryDetail(category, startMonth, endMonth)
|
||||
} else if (currentViewMode == ViewMode.MEMBER && member != null) {
|
||||
onNavigateToMemberDetail(member, startMonth, selectedAnalysisType)
|
||||
onNavigateToMemberDetail(member, startMonth, endMonth, selectedAnalysisType)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.yovinchen.bookkeeping.ui.screen
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@ -17,6 +18,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
@ -33,6 +35,7 @@ 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.model.MemberStat
|
||||
import com.yovinchen.bookkeeping.model.TransactionType
|
||||
import com.yovinchen.bookkeeping.ui.components.CategoryPieChart
|
||||
import com.yovinchen.bookkeeping.ui.components.RecordItem
|
||||
@ -47,17 +50,20 @@ import java.util.Locale
|
||||
@Composable
|
||||
fun CategoryDetailScreen(
|
||||
category: String,
|
||||
yearMonth: YearMonth,
|
||||
startMonth: YearMonth,
|
||||
endMonth: YearMonth,
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToMemberDetail: (String) -> Unit,
|
||||
viewModel: CategoryDetailViewModel = viewModel(
|
||||
factory = CategoryDetailViewModelFactory(
|
||||
database = BookkeepingDatabase.getDatabase(LocalContext.current),
|
||||
category = category,
|
||||
startMonth = startMonth,
|
||||
endMonth = endMonth
|
||||
)
|
||||
),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val database = remember { BookkeepingDatabase.getDatabase(context) }
|
||||
val viewModel: CategoryDetailViewModel = viewModel(
|
||||
factory = CategoryDetailViewModelFactory(database, category, yearMonth)
|
||||
)
|
||||
|
||||
val records by viewModel.records.collectAsState()
|
||||
val memberStats by viewModel.memberStats.collectAsState()
|
||||
val total by viewModel.total.collectAsState()
|
||||
@ -107,16 +113,16 @@ fun CategoryDetailScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// 第二部分:扇形图
|
||||
// 第二部分:成员统计
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
@ -125,19 +131,33 @@ fun CategoryDetailScreen(
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
// 饼状图
|
||||
CategoryPieChart(
|
||||
categoryData = memberStats.map { Pair(it.category, it.percentage.toFloat()) },
|
||||
memberData = emptyList(),
|
||||
currentViewMode = false,
|
||||
categoryData = emptyList(),
|
||||
memberData = memberStats.map { Pair(it.member, it.percentage.toFloat()) },
|
||||
currentViewMode = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp),
|
||||
onCategoryClick = { memberName ->
|
||||
if (records.isNotEmpty() && records.first().type == TransactionType.EXPENSE) {
|
||||
onNavigateToMemberDetail(memberName)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 成员列表
|
||||
Column {
|
||||
memberStats.forEach { stat ->
|
||||
MemberStatItem(
|
||||
stat = stat,
|
||||
onClick = { onNavigateToMemberDetail(stat.member) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -208,6 +228,29 @@ fun CategoryDetailScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MemberStatItem(
|
||||
stat: MemberStat,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stat.member) },
|
||||
supportingContent = {
|
||||
Text(
|
||||
buildString {
|
||||
append("金额: ¥%.2f".format(stat.amount))
|
||||
append(" | ")
|
||||
append("次数: ${stat.count}")
|
||||
append(" | ")
|
||||
append("占比: %.1f%%".format(stat.percentage))
|
||||
}
|
||||
)
|
||||
},
|
||||
modifier = modifier.clickable(onClick = onClick)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecordItem(
|
||||
record: BookkeepingRecord,
|
||||
|
@ -44,7 +44,8 @@ import java.util.Locale
|
||||
@Composable
|
||||
fun MemberDetailScreen(
|
||||
memberName: String,
|
||||
yearMonth: YearMonth,
|
||||
startMonth: YearMonth,
|
||||
endMonth: YearMonth,
|
||||
category: String = "",
|
||||
analysisType: AnalysisType = AnalysisType.EXPENSE,
|
||||
onNavigateBack: () -> Unit,
|
||||
@ -53,8 +54,14 @@ fun MemberDetailScreen(
|
||||
val records by viewModel.memberRecords.collectAsState(initial = emptyList())
|
||||
val totalAmount by viewModel.totalAmount.collectAsState(initial = 0.0)
|
||||
|
||||
LaunchedEffect(memberName, category, yearMonth, analysisType) {
|
||||
viewModel.loadMemberRecords(memberName, category, yearMonth, analysisType)
|
||||
LaunchedEffect(memberName, category, startMonth, endMonth, analysisType) {
|
||||
viewModel.loadMemberRecords(
|
||||
memberName = memberName,
|
||||
category = category,
|
||||
startMonth = startMonth,
|
||||
endMonth = endMonth,
|
||||
analysisType = analysisType
|
||||
)
|
||||
}
|
||||
|
||||
val groupedRecords = remember(records) {
|
||||
|
@ -5,24 +5,25 @@ 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.CategoryStat
|
||||
import com.yovinchen.bookkeeping.model.MemberStat
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.time.YearMonth
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.ZoneId
|
||||
import java.util.Date
|
||||
|
||||
class CategoryDetailViewModel(
|
||||
private val database: BookkeepingDatabase,
|
||||
private val category: String,
|
||||
private val month: YearMonth
|
||||
private val startMonth: YearMonth,
|
||||
private val endMonth: YearMonth
|
||||
) : ViewModel() {
|
||||
private val recordDao = database.bookkeepingDao()
|
||||
private val yearMonthStr = month.format(DateTimeFormatter.ofPattern("yyyy-MM"))
|
||||
|
||||
private val _records = MutableStateFlow<List<BookkeepingRecord>>(emptyList())
|
||||
val records: StateFlow<List<BookkeepingRecord>> = _records.asStateFlow()
|
||||
|
||||
private val _memberStats = MutableStateFlow<List<CategoryStat>>(emptyList())
|
||||
val memberStats: StateFlow<List<CategoryStat>> = _memberStats.asStateFlow()
|
||||
private val _memberStats = MutableStateFlow<List<MemberStat>>(emptyList())
|
||||
val memberStats: StateFlow<List<MemberStat>> = _memberStats.asStateFlow()
|
||||
|
||||
val total: StateFlow<Double> = records
|
||||
.map { records -> records.sumOf { it.amount } }
|
||||
@ -33,22 +34,30 @@ class CategoryDetailViewModel(
|
||||
)
|
||||
|
||||
init {
|
||||
recordDao.getRecordsByCategory(category)
|
||||
.onEach { records ->
|
||||
_records.value = records.filter { record ->
|
||||
val recordMonth = YearMonth.from(
|
||||
DateTimeFormatter.ofPattern("yyyy-MM")
|
||||
.parse(yearMonthStr)
|
||||
val startDate = startMonth.atDay(1).atStartOfDay()
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toInstant()
|
||||
.let { Date.from(it) }
|
||||
|
||||
val endDate = endMonth.atEndOfMonth().atTime(23, 59, 59)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toInstant()
|
||||
.let { Date.from(it) }
|
||||
|
||||
recordDao.getRecordsByCategoryAndDateRange(
|
||||
category = category,
|
||||
startDate = startDate,
|
||||
endDate = endDate
|
||||
)
|
||||
YearMonth.from(record.date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime()) == recordMonth
|
||||
}
|
||||
}
|
||||
.onEach { records -> _records.value = records }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
recordDao.getMemberStatsByCategory(category, yearMonthStr)
|
||||
.onEach { stats ->
|
||||
_memberStats.value = stats
|
||||
}
|
||||
recordDao.getMemberStatsByCategoryAndDateRange(
|
||||
category = category,
|
||||
startDate = startDate,
|
||||
endDate = endDate
|
||||
)
|
||||
.onEach { stats -> _memberStats.value = stats }
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
}
|
||||
|
@ -8,12 +8,13 @@ import java.time.YearMonth
|
||||
class CategoryDetailViewModelFactory(
|
||||
private val database: BookkeepingDatabase,
|
||||
private val category: String,
|
||||
private val month: YearMonth
|
||||
private val startMonth: YearMonth,
|
||||
private val endMonth: YearMonth
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(CategoryDetailViewModel::class.java)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return CategoryDetailViewModel(database, category, month) as T
|
||||
return CategoryDetailViewModel(database, category, startMonth, endMonth) as T
|
||||
}
|
||||
throw IllegalArgumentException("Unknown ViewModel class")
|
||||
}
|
||||
|
@ -7,9 +7,7 @@ import com.yovinchen.bookkeeping.data.BookkeepingDatabase
|
||||
import com.yovinchen.bookkeeping.model.BookkeepingRecord
|
||||
import com.yovinchen.bookkeeping.model.AnalysisType
|
||||
import com.yovinchen.bookkeeping.model.TransactionType
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.time.YearMonth
|
||||
import java.time.ZoneId
|
||||
import java.util.Date
|
||||
@ -19,19 +17,24 @@ class MemberDetailViewModel(application: Application) : AndroidViewModel(applica
|
||||
private val recordDao = database.bookkeepingDao()
|
||||
|
||||
private val _memberRecords = MutableStateFlow<List<BookkeepingRecord>>(emptyList())
|
||||
val memberRecords: StateFlow<List<BookkeepingRecord>> = _memberRecords
|
||||
val memberRecords: StateFlow<List<BookkeepingRecord>> = _memberRecords.asStateFlow()
|
||||
|
||||
private val _totalAmount = MutableStateFlow(0.0)
|
||||
val totalAmount: StateFlow<Double> = _totalAmount
|
||||
val totalAmount: StateFlow<Double> = _totalAmount.asStateFlow()
|
||||
|
||||
fun loadMemberRecords(memberName: String, category: String, yearMonth: YearMonth, analysisType: AnalysisType) {
|
||||
viewModelScope.launch {
|
||||
val startDate = yearMonth.atDay(1).atStartOfDay()
|
||||
fun loadMemberRecords(
|
||||
memberName: String,
|
||||
category: String,
|
||||
startMonth: YearMonth,
|
||||
endMonth: YearMonth,
|
||||
analysisType: AnalysisType
|
||||
) {
|
||||
val startDate = startMonth.atDay(1).atStartOfDay()
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toInstant()
|
||||
.let { Date.from(it) }
|
||||
|
||||
val endDate = yearMonth.atEndOfMonth().atTime(23, 59, 59)
|
||||
val endDate = endMonth.atEndOfMonth().atTime(23, 59, 59)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toInstant()
|
||||
.let { Date.from(it) }
|
||||
@ -42,15 +45,15 @@ class MemberDetailViewModel(application: Application) : AndroidViewModel(applica
|
||||
else -> null
|
||||
}
|
||||
|
||||
val records = if (category.isEmpty()) {
|
||||
recordDao.getRecordsByMember(
|
||||
val recordsFlow = if (category.isEmpty()) {
|
||||
recordDao.getRecordsByMemberAndDateRange(
|
||||
memberName = memberName,
|
||||
startDate = startDate,
|
||||
endDate = endDate,
|
||||
transactionType = transactionType
|
||||
)
|
||||
} else {
|
||||
recordDao.getRecordsByMemberAndCategory(
|
||||
recordDao.getRecordsByMemberCategoryAndDateRange(
|
||||
memberName = memberName,
|
||||
category = category,
|
||||
startDate = startDate,
|
||||
@ -58,8 +61,12 @@ class MemberDetailViewModel(application: Application) : AndroidViewModel(applica
|
||||
transactionType = transactionType
|
||||
)
|
||||
}
|
||||
|
||||
recordsFlow
|
||||
.onEach { records ->
|
||||
_memberRecords.value = records
|
||||
_totalAmount.value = records.sumOf { it.amount }
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user