Compare commits
2 Commits
76d0286883
...
94fc7b2a7e
Author | SHA1 | Date | |
---|---|---|---|
94fc7b2a7e | |||
380fdd5589 |
@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
|
@ -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
|
||||
@ -38,6 +39,48 @@ interface BookkeepingDao {
|
||||
yearMonth: String
|
||||
): Flow<List<BookkeepingRecord>>
|
||||
|
||||
@Query("""
|
||||
SELECT * FROM bookkeeping_records
|
||||
WHERE memberId IN (SELECT id FROM members WHERE name = :memberName)
|
||||
AND strftime('%Y-%m', datetime(date/1000, 'unixepoch')) = :yearMonth
|
||||
ORDER BY date DESC
|
||||
""")
|
||||
fun getRecordsByMemberAndMonth(
|
||||
memberName: String,
|
||||
yearMonth: String
|
||||
): Flow<List<BookkeepingRecord>>
|
||||
|
||||
@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<List<CategoryStat>>
|
||||
|
||||
@Query("""
|
||||
SELECT * FROM bookkeeping_records
|
||||
WHERE category = :category
|
||||
ORDER BY date DESC
|
||||
""")
|
||||
fun getRecordsByCategory(
|
||||
category: String
|
||||
): Flow<List<BookkeepingRecord>>
|
||||
|
||||
@Insert
|
||||
suspend fun insertRecord(record: BookkeepingRecord): Long
|
||||
|
||||
@ -64,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<BookkeepingRecord>
|
||||
}
|
||||
|
@ -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()) }
|
||||
}
|
||||
}
|
||||
|
@ -12,5 +12,6 @@ data class Record(
|
||||
val category: String,
|
||||
val description: String,
|
||||
val dateTime: LocalDateTime = LocalDateTime.now(),
|
||||
val isExpense: Boolean = true
|
||||
val isExpense: Boolean = true,
|
||||
val member: String = "Default"
|
||||
)
|
||||
|
@ -2,16 +2,14 @@ package com.yovinchen.bookkeeping.ui.components
|
||||
|
||||
import android.graphics.Color as AndroidColor
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import com.github.mikephil.charting.charts.PieChart
|
||||
import com.github.mikephil.charting.components.Legend
|
||||
import com.github.mikephil.charting.data.Entry
|
||||
import com.github.mikephil.charting.data.PieData
|
||||
import com.github.mikephil.charting.data.PieDataSet
|
||||
@ -24,11 +22,13 @@ import com.github.mikephil.charting.utils.ColorTemplate
|
||||
@Composable
|
||||
fun CategoryPieChart(
|
||||
categoryData: List<Pair<String, Float>>,
|
||||
memberData: List<Pair<String, Float>>,
|
||||
currentViewMode: Boolean = false, // false 为分类视图,true 为成员视图
|
||||
modifier: Modifier = Modifier,
|
||||
onCategoryClick: (String) -> Unit = {}
|
||||
) {
|
||||
isSystemInDarkTheme()
|
||||
val textColor = MaterialTheme.colorScheme.onSurface.toArgb()
|
||||
val data = if (currentViewMode) memberData else categoryData
|
||||
|
||||
AndroidView(
|
||||
modifier = modifier
|
||||
@ -39,23 +39,15 @@ fun CategoryPieChart(
|
||||
description.isEnabled = false
|
||||
setUsePercentValues(true)
|
||||
setDrawEntryLabels(true)
|
||||
|
||||
// 禁用图例显示
|
||||
legend.isEnabled = false
|
||||
|
||||
isDrawHoleEnabled = true
|
||||
holeRadius = 40f
|
||||
setHoleColor(AndroidColor.TRANSPARENT)
|
||||
setTransparentCircleRadius(45f)
|
||||
|
||||
// 设置标签文字颜色
|
||||
setEntryLabelColor(textColor)
|
||||
setEntryLabelTextSize(12f)
|
||||
|
||||
// 设置中心文字颜色跟随主题
|
||||
setCenterTextColor(textColor)
|
||||
|
||||
// 添加点击事件监听器
|
||||
setOnChartValueSelectedListener(object : OnChartValueSelectedListener {
|
||||
override fun onValueSelected(e: Entry?, h: Highlight?) {
|
||||
e?.let {
|
||||
@ -65,18 +57,16 @@ fun CategoryPieChart(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNothingSelected() {
|
||||
// 不需要处理
|
||||
}
|
||||
override fun onNothingSelected() {}
|
||||
})
|
||||
}
|
||||
},
|
||||
update = { chart ->
|
||||
val entries = categoryData.map { (category, amount) ->
|
||||
PieEntry(amount, category)
|
||||
val entries = data.map { (label, amount) ->
|
||||
PieEntry(amount, label)
|
||||
}
|
||||
|
||||
val dataSet = PieDataSet(entries, "").apply { // 将标题设为空字符串
|
||||
val dataSet = PieDataSet(entries, "").apply {
|
||||
colors = ColorTemplate.MATERIAL_COLORS.toList()
|
||||
valueTextSize = 14f
|
||||
valueFormatter = PercentFormatter(chart)
|
||||
|
@ -2,10 +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.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
@ -24,28 +23,31 @@ import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.yovinchen.bookkeeping.model.ThemeMode
|
||||
import com.yovinchen.bookkeeping.ui.screen.AnalysisScreen
|
||||
import com.yovinchen.bookkeeping.ui.screen.CategoryDetailScreen
|
||||
import com.yovinchen.bookkeeping.ui.screen.HomeScreen
|
||||
import com.yovinchen.bookkeeping.ui.screen.SettingsScreen
|
||||
import com.yovinchen.bookkeeping.ui.screen.*
|
||||
import java.time.YearMonth
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
sealed class Screen(
|
||||
val route: String,
|
||||
val icon: ImageVector,
|
||||
val label: String
|
||||
val title: String,
|
||||
val icon: ImageVector? = null
|
||||
) {
|
||||
data object Home : Screen("home", Icons.Default.Home, "主页")
|
||||
data object Analysis : Screen("analysis", Icons.Outlined.Analytics, "分析")
|
||||
data object Settings : Screen("settings", Icons.Default.Settings, "设置")
|
||||
data object CategoryDetail : Screen(
|
||||
"category_detail/{category}/{yearMonth}",
|
||||
Icons.Default.List,
|
||||
"分类详情"
|
||||
) {
|
||||
fun createRoute(category: String, yearMonth: String) =
|
||||
"category_detail/$category/$yearMonth"
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,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) {
|
||||
@ -92,14 +90,20 @@ fun MainNavigation(
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
) {
|
||||
composable(Screen.Home.route) { HomeScreen() }
|
||||
|
||||
composable(Screen.Analysis.route) {
|
||||
AnalysisScreen(
|
||||
onNavigateToCategoryDetail = { category, month ->
|
||||
val monthStr = month.format(DateTimeFormatter.ofPattern("yyyy-MM"))
|
||||
navController.navigate(Screen.CategoryDetail.createRoute(category, monthStr))
|
||||
onNavigateToCategoryDetail = { category, yearMonth ->
|
||||
navController.navigate(Screen.CategoryDetail.createRoute(category, yearMonth))
|
||||
},
|
||||
onNavigateToMemberDetail = { memberName, yearMonth ->
|
||||
// 在这里我们暂时使用一个默认分类,你需要根据实际情况修改这里的逻辑
|
||||
val defaultCategory = "默认"
|
||||
navController.navigate(Screen.MemberDetail.createRoute(memberName, defaultCategory, yearMonth))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(Screen.Settings.route) {
|
||||
SettingsScreen(
|
||||
currentTheme = currentTheme,
|
||||
@ -115,13 +119,37 @@ fun MainNavigation(
|
||||
)
|
||||
) { backStackEntry ->
|
||||
val category = backStackEntry.arguments?.getString("category") ?: return@composable
|
||||
val yearMonth = YearMonth.parse(
|
||||
backStackEntry.arguments?.getString("yearMonth") ?: return@composable
|
||||
)
|
||||
val yearMonthStr = backStackEntry.arguments?.getString("yearMonth") ?: return@composable
|
||||
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))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
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)
|
||||
|
||||
MemberDetailScreen(
|
||||
memberName = memberName,
|
||||
category = category,
|
||||
yearMonth = yearMonth,
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,11 @@ 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.ArrowDropDown
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@ -18,17 +21,25 @@ import com.yovinchen.bookkeeping.viewmodel.AnalysisViewModel
|
||||
import java.time.YearMonth
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
enum class ViewMode {
|
||||
CATEGORY, MEMBER
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AnalysisScreen(
|
||||
onNavigateToCategoryDetail: (String, YearMonth) -> Unit
|
||||
onNavigateToCategoryDetail: (String, YearMonth) -> Unit,
|
||||
onNavigateToMemberDetail: (String, YearMonth) -> Unit
|
||||
) {
|
||||
val viewModel: AnalysisViewModel = viewModel()
|
||||
val selectedMonth by viewModel.selectedMonth.collectAsState()
|
||||
val selectedAnalysisType by viewModel.selectedAnalysisType.collectAsState()
|
||||
val categoryStats by viewModel.categoryStats.collectAsState()
|
||||
val memberStats by viewModel.memberStats.collectAsState()
|
||||
|
||||
var showMonthPicker by remember { mutableStateOf(false) }
|
||||
var showViewModeMenu by remember { mutableStateOf(false) }
|
||||
var currentViewMode by rememberSaveable { mutableStateOf(ViewMode.CATEGORY) }
|
||||
|
||||
Scaffold { padding ->
|
||||
Column(
|
||||
@ -36,17 +47,54 @@ fun AnalysisScreen(
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
// 月份选择器和类型切换
|
||||
// 时间选择按钮行
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Button(onClick = { showMonthPicker = true }) {
|
||||
Text(selectedMonth.format(DateTimeFormatter.ofPattern("yyyy年MM月")))
|
||||
}
|
||||
}
|
||||
|
||||
// 分析类型和视图模式选择行
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 月份选择按钮
|
||||
Button(onClick = { showMonthPicker = true }) {
|
||||
Text(selectedMonth.format(DateTimeFormatter.ofPattern("yyyy年MM月")))
|
||||
// 分类/成员切换下拉菜单
|
||||
Box {
|
||||
Button(
|
||||
onClick = { showViewModeMenu = true }
|
||||
) {
|
||||
Text(if (currentViewMode == ViewMode.CATEGORY) "分类" else "成员")
|
||||
Icon(Icons.Default.ArrowDropDown, "切换视图")
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showViewModeMenu,
|
||||
onDismissRequest = { showViewModeMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("分类") },
|
||||
onClick = {
|
||||
currentViewMode = ViewMode.CATEGORY
|
||||
showViewModeMenu = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("成员") },
|
||||
onClick = {
|
||||
currentViewMode = ViewMode.MEMBER
|
||||
showViewModeMenu = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 类型切换
|
||||
@ -80,37 +128,48 @@ fun AnalysisScreen(
|
||||
item {
|
||||
CategoryPieChart(
|
||||
categoryData = categoryStats.map { Pair(it.category, it.percentage.toFloat()) },
|
||||
memberData = memberStats.map { Pair(it.category, it.percentage.toFloat()) },
|
||||
currentViewMode = currentViewMode == ViewMode.MEMBER,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp)
|
||||
.padding(bottom = 16.dp),
|
||||
onCategoryClick = { category ->
|
||||
if (currentViewMode == ViewMode.CATEGORY) {
|
||||
onNavigateToCategoryDetail(category, selectedMonth)
|
||||
} else {
|
||||
onNavigateToMemberDetail(category, selectedMonth)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加分类统计列表项目
|
||||
items(categoryStats) { stat ->
|
||||
// 添加统计列表项目
|
||||
items(if (currentViewMode == ViewMode.CATEGORY) categoryStats else memberStats) { stat ->
|
||||
CategoryStatItem(
|
||||
stat = stat,
|
||||
onClick = { onNavigateToCategoryDetail(stat.category, selectedMonth) }
|
||||
onClick = {
|
||||
if (currentViewMode == ViewMode.CATEGORY) {
|
||||
onNavigateToCategoryDetail(stat.category, selectedMonth)
|
||||
} else {
|
||||
onNavigateToMemberDetail(stat.category, selectedMonth)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 月份选择器对话框
|
||||
if (showMonthPicker) {
|
||||
MonthYearPicker(
|
||||
selectedMonth = selectedMonth,
|
||||
onMonthSelected = {
|
||||
viewModel.setSelectedMonth(it)
|
||||
onMonthSelected = { month ->
|
||||
viewModel.setSelectedMonth(month)
|
||||
showMonthPicker = false
|
||||
},
|
||||
onDismiss = { showMonthPicker = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,135 +4,130 @@ 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)
|
||||
) {
|
||||
// 总金额显示
|
||||
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
|
||||
.padding(padding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = "总金额",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
text = NumberFormat.getCurrencyInstance(Locale.CHINA).format(total),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = String.format("%.2f", total),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 记录列表
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
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(vertical = 4.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// 日期标签
|
||||
// 日期标题和总金额
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.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))
|
||||
|
||||
// 当天的记录
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
dayRecords.forEachIndexed { index, record ->
|
||||
RecordItem(
|
||||
record = record,
|
||||
onClick = {},
|
||||
members = members
|
||||
)
|
||||
|
||||
if (index < dayRecords.size - 1) {
|
||||
// 当天的记录列表
|
||||
dayRecords.forEach { record ->
|
||||
RecordItem(record = record)
|
||||
if (record != dayRecords.last()) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
thickness = 0.5.dp
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -142,6 +137,44 @@ 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,162 @@
|
||||
package com.yovinchen.bookkeeping.ui.screen
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
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.data.Record
|
||||
import com.yovinchen.bookkeeping.ui.components.RecordItem
|
||||
import com.yovinchen.bookkeeping.viewmodel.MemberDetailViewModel
|
||||
import java.text.NumberFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.YearMonth
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MemberDetailScreen(
|
||||
memberName: String,
|
||||
category: String,
|
||||
yearMonth: YearMonth,
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: MemberDetailViewModel = viewModel()
|
||||
) {
|
||||
val records by viewModel.memberRecords.collectAsState(initial = emptyList())
|
||||
val totalAmount by viewModel.totalAmount.collectAsState(initial = 0.0)
|
||||
|
||||
LaunchedEffect(memberName, category, yearMonth) {
|
||||
viewModel.loadMemberRecords(memberName, category, yearMonth)
|
||||
}
|
||||
|
||||
val groupedRecords = remember(records) {
|
||||
records.groupBy { record ->
|
||||
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(record.date)
|
||||
}.toSortedMap(reverseOrder())
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text("$category - $memberName")
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, "返回")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
// 第一层:总金额卡片
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||
) {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 第二层:按日期分组的记录列表
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecordItem(record: Record) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
if (record.description.isNotBlank()) {
|
||||
Text(
|
||||
text = record.description,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
Text(
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
@ -11,9 +11,11 @@ import kotlinx.coroutines.flow.*
|
||||
import java.time.LocalDateTime
|
||||
import java.time.YearMonth
|
||||
import java.time.ZoneId
|
||||
import java.util.Date
|
||||
|
||||
class AnalysisViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val recordDao = BookkeepingDatabase.getDatabase(application).bookkeepingDao()
|
||||
private val memberDao = BookkeepingDatabase.getDatabase(application).memberDao()
|
||||
|
||||
private val _selectedMonth = MutableStateFlow(YearMonth.now())
|
||||
val selectedMonth = _selectedMonth.asStateFlow()
|
||||
@ -21,11 +23,50 @@ class AnalysisViewModel(application: Application) : AndroidViewModel(application
|
||||
private val _selectedAnalysisType = MutableStateFlow(AnalysisType.EXPENSE)
|
||||
val selectedAnalysisType = _selectedAnalysisType.asStateFlow()
|
||||
|
||||
private val members = memberDao.getAllMembers()
|
||||
|
||||
val memberStats = combine(selectedMonth, selectedAnalysisType, members) { month, type, membersList ->
|
||||
val records = recordDao.getAllRecords().first()
|
||||
val monthRecords = records.filter {
|
||||
val recordDate = Date(it.date.time)
|
||||
val localDateTime = LocalDateTime.ofInstant(recordDate.toInstant(), ZoneId.systemDefault())
|
||||
YearMonth.from(localDateTime) == month && it.type == when(type) {
|
||||
AnalysisType.EXPENSE -> TransactionType.EXPENSE
|
||||
AnalysisType.INCOME -> TransactionType.INCOME
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
// 按成员统计
|
||||
val memberMap = monthRecords.groupBy { record ->
|
||||
membersList.find { it.id == record.memberId }?.name ?: "未分配"
|
||||
}
|
||||
|
||||
val stats = memberMap.map { (memberName, records) ->
|
||||
CategoryStat(
|
||||
category = memberName,
|
||||
amount = records.sumOf { it.amount },
|
||||
count = records.size
|
||||
)
|
||||
}.sortedByDescending { it.amount }
|
||||
|
||||
// 计算总额
|
||||
val total = stats.sumOf { it.amount }
|
||||
|
||||
// 计算百分比
|
||||
stats.map { it.copy(percentage = if (total > 0) it.amount / total * 100 else 0.0) }
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5000),
|
||||
initialValue = emptyList()
|
||||
)
|
||||
|
||||
val categoryStats = combine(selectedMonth, selectedAnalysisType) { month, type ->
|
||||
val records = recordDao.getAllRecords().first()
|
||||
val monthRecords = records.filter {
|
||||
val recordDate = LocalDateTime.ofInstant(it.date.toInstant(), ZoneId.systemDefault())
|
||||
YearMonth.from(recordDate) == month && it.type == when(type) {
|
||||
val recordDate = Date(it.date.time)
|
||||
val localDateTime = LocalDateTime.ofInstant(recordDate.toInstant(), ZoneId.systemDefault())
|
||||
YearMonth.from(localDateTime) == month && it.type == when(type) {
|
||||
AnalysisType.EXPENSE -> TransactionType.EXPENSE
|
||||
AnalysisType.INCOME -> TransactionType.INCOME
|
||||
else -> null
|
||||
|
@ -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<List<BookkeepingRecord>>(emptyList())
|
||||
val records: StateFlow<List<BookkeepingRecord>> = _records
|
||||
val records: StateFlow<List<BookkeepingRecord>> = _records.asStateFlow()
|
||||
|
||||
private val _total = MutableStateFlow(0.0)
|
||||
val total: StateFlow<Double> = _total
|
||||
private val _memberStats = MutableStateFlow<List<CategoryStat>>(emptyList())
|
||||
val memberStats: StateFlow<List<CategoryStat>> = _memberStats.asStateFlow()
|
||||
|
||||
private val _members = MutableStateFlow<List<Member>>(emptyList())
|
||||
val members: StateFlow<List<Member>> = _members
|
||||
val total: StateFlow<Double> = records
|
||||
.map { records -> records.sumOf { it.amount } }
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5000),
|
||||
initialValue = 0.0
|
||||
)
|
||||
|
||||
init {
|
||||
loadRecords()
|
||||
loadMembers()
|
||||
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
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadMembers() {
|
||||
viewModelScope.launch {
|
||||
database.memberDao().getAllMembers().collect { members ->
|
||||
_members.value = members
|
||||
}
|
||||
recordDao.getMemberStatsByCategory(category, yearMonthStr)
|
||||
.onEach { stats ->
|
||||
_memberStats.value = stats
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,47 @@
|
||||
package com.yovinchen.bookkeeping.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.yovinchen.bookkeeping.data.BookkeepingDatabase
|
||||
import com.yovinchen.bookkeeping.model.BookkeepingRecord
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.YearMonth
|
||||
import java.time.ZoneId
|
||||
import java.util.Date
|
||||
|
||||
class MemberDetailViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val database = BookkeepingDatabase.getDatabase(application)
|
||||
private val recordDao = database.bookkeepingDao()
|
||||
|
||||
private val _memberRecords = MutableStateFlow<List<BookkeepingRecord>>(emptyList())
|
||||
val memberRecords: StateFlow<List<BookkeepingRecord>> = _memberRecords
|
||||
|
||||
private val _totalAmount = MutableStateFlow(0.0)
|
||||
val totalAmount: StateFlow<Double> = _totalAmount
|
||||
|
||||
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
|
||||
_totalAmount.value = records.sumOf { it.amount }
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user