diff --git a/app/src/main/java/com/danilkinkin/buckwheat/analytics/Analytics.kt b/app/src/main/java/com/danilkinkin/buckwheat/analytics/Analytics.kt
index f8e5f84..a87bef2 100644
--- a/app/src/main/java/com/danilkinkin/buckwheat/analytics/Analytics.kt
+++ b/app/src/main/java/com/danilkinkin/buckwheat/analytics/Analytics.kt
@@ -62,6 +62,9 @@ fun Analytics(
val spends by spendsViewModel.spends.observeAsState(emptyList())
val wholeBudget = spendsViewModel.budget.value!!
val scrollState = rememberScrollState()
+
+ val finishPeriodActualDate by spendsViewModel.finishPeriodActualDate.observeAsState(null)
+
// Need to hide calendar after migration to transactions,
// because after migration can't restore some transactions like INCOME & SET_DAILY_BUDGET
val afterMigrationToTransactions =
@@ -98,6 +101,7 @@ fun Analytics(
currency = spendsViewModel.currency.value!!,
startDate = spendsViewModel.startPeriodDate.value!!,
finishDate = spendsViewModel.finishPeriodDate.value!!,
+ actualFinishDate = finishPeriodActualDate,
)
Spacer(modifier = Modifier.height(16.dp))
if (spends.isNotEmpty()) {
diff --git a/app/src/main/java/com/danilkinkin/buckwheat/analytics/WholeBudgetCard.kt b/app/src/main/java/com/danilkinkin/buckwheat/analytics/WholeBudgetCard.kt
index fca78cc..f989b56 100644
--- a/app/src/main/java/com/danilkinkin/buckwheat/analytics/WholeBudgetCard.kt
+++ b/app/src/main/java/com/danilkinkin/buckwheat/analytics/WholeBudgetCard.kt
@@ -8,9 +8,12 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.layout.*
import androidx.compose.ui.platform.LocalContext
@@ -21,6 +24,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.*
+import androidx.compose.ui.zIndex
import com.danilkinkin.buckwheat.R
import com.danilkinkin.buckwheat.data.ExtendCurrency
import com.danilkinkin.buckwheat.ui.BuckwheatTheme
@@ -36,12 +40,13 @@ fun WholeBudgetCard(
currency: ExtendCurrency,
startDate: Date,
finishDate: Date?,
+ actualFinishDate: Date? = null,
colors: CardColors = CardDefaults.cardColors(),
bigVariant: Boolean = true,
contentPadding: PaddingValues = PaddingValues(vertical = 16.dp, horizontal = 24.dp),
) {
val context = LocalContext.current
-
+
StatCard(
modifier = modifier.fillMaxWidth(),
contentPadding = contentPadding,
@@ -83,49 +88,89 @@ fun WholeBudgetCard(
.padding(horizontal = if (bigVariant) 16.dp else 8.dp)
.fillMaxHeight()
)
- if (finishDate !== null && bigVariant) {
- Surface(
- modifier = Modifier
+ if (actualFinishDate !== null && bigVariant) {
+ CountDaysChip(
+ Modifier
.align(Alignment.Center)
- .requiredHeight(24.dp),
- shape = CircleShape,
- color = LocalContentColor.current,
- contentColor = MaterialTheme.colorScheme.surface,
+ .offset(6.dp, (-12).dp)
+ .rotate(6f)
+ .zIndex(1f),
+ fromDate = startDate,
+ toDate = actualFinishDate
+ )
+ Cross(
+ modifier = Modifier.align(Alignment.Center)
) {
- val days = countDays(finishDate, startDate)
-
- Box(
- contentAlignment = Alignment.Center,
- ) {
- Text(
- modifier = Modifier.padding(12.dp, 0.dp),
- text = String.format(
- pluralStringResource(R.plurals.days_count, count = days),
- days,
- ),
- style = MaterialTheme.typography.bodyMedium,
- )
- }
+ CountDaysChip(
+ Modifier,
+ fromDate = startDate,
+ toDate = finishDate!!
+ )
}
+ } else if (finishDate !== null && bigVariant) {
+ CountDaysChip(
+ Modifier.align(Alignment.Center),
+ fromDate = startDate,
+ toDate = finishDate
+ )
}
}
Column(horizontalAlignment = Alignment.End) {
- Text(
- text = if (finishDate !== null) {
- prettyDate(
- finishDate,
- pattern = "dd MMM",
- simplifyIfToday = false,
+ Box {
+ if (actualFinishDate !== null) {
+ Text(
+ modifier = Modifier
+ .offset((-4).dp, (-20).dp)
+ .rotate(6f),
+ text = prettyDate(
+ actualFinishDate,
+ pattern = "dd MMM",
+ simplifyIfToday = false,
+ ),
+ softWrap = false,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.bodyMedium,
+ fontSize = if (bigVariant) MaterialTheme.typography.bodySmall.fontSize else MaterialTheme.typography.labelSmall.fontSize,
)
+
+ Cross {
+ Box(Modifier.wrapContentSize()) {
+ Text(
+ text = if (finishDate !== null) {
+ prettyDate(
+ finishDate,
+ pattern = "dd MMM",
+ simplifyIfToday = false,
+ )
+ } else {
+ "-"
+ },
+ softWrap = false,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.bodyMedium,
+ fontSize = if (bigVariant) MaterialTheme.typography.bodySmall.fontSize else MaterialTheme.typography.labelSmall.fontSize,
+ )
+ }
+ }
} else {
- "-"
- },
- softWrap = false,
- overflow = TextOverflow.Ellipsis,
- style = MaterialTheme.typography.bodyMedium,
- fontSize = if (bigVariant) MaterialTheme.typography.bodySmall.fontSize else MaterialTheme.typography.labelSmall.fontSize,
- )
+ Text(
+ text = if (finishDate !== null) {
+ prettyDate(
+ finishDate,
+ pattern = "dd MMM",
+ simplifyIfToday = false,
+ )
+ } else {
+ "-"
+ },
+ softWrap = false,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.bodyMedium,
+ fontSize = if (bigVariant) MaterialTheme.typography.bodySmall.fontSize else MaterialTheme.typography.labelSmall.fontSize,
+ )
+ }
+ }
}
},
)
@@ -133,6 +178,60 @@ fun WholeBudgetCard(
)
}
+@Composable
+fun CountDaysChip(modifier: Modifier = Modifier, fromDate: Date, toDate: Date) {
+ Surface(
+ modifier = modifier
+ .requiredHeight(24.dp),
+ shape = CircleShape,
+ color = LocalContentColor.current,
+ contentColor = MaterialTheme.colorScheme.surface,
+ ) {
+ val days = countDays(toDate, fromDate)
+
+ Box(
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ modifier = Modifier.padding(12.dp, 0.dp),
+ text = String.format(
+ pluralStringResource(
+ R.plurals.days_count,
+ count = days
+ ),
+ days,
+ ),
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+ }
+}
+
+@Composable
+fun Cross(
+ modifier: Modifier = Modifier,
+ tint: Color = MaterialTheme.colorScheme.error,
+ content: @Composable () -> Unit,
+) {
+ Box(modifier = modifier) {
+ content()
+ Canvas(modifier = Modifier.matchParentSize()) {
+ val width = this.size.width
+ val height = this.size.height
+ val offset = Offset(6f, 6f)
+ val thickness = 6f
+
+ drawLine(
+ color = tint,
+ start = Offset(offset.x, height - offset.y),
+ end = Offset(width - offset.x, offset.y),
+ strokeWidth = thickness,
+ cap = StrokeCap.Round
+ )
+ }
+ }
+}
+
@Composable
fun Arrow(
modifier: Modifier = Modifier,
@@ -231,7 +330,18 @@ private fun PreviewChart() {
@Preview
@Composable
-private fun Preview() {
+private fun PreviewCross() {
+ BuckwheatTheme {
+ Cross {
+ Text(text = "Hello")
+ }
+ }
+}
+
+@Preview
+@Preview(name = "Night mode", uiMode = UI_MODE_NIGHT_YES)
+@Composable
+private fun PreviewEarlyFinish() {
BuckwheatTheme {
WholeBudgetCard(
modifier = Modifier.height(IntrinsicSize.Min),
@@ -239,13 +349,15 @@ private fun Preview() {
currency = ExtendCurrency.none(),
startDate = LocalDate.now().minusDays(28).toDate(),
finishDate = Date(),
+ actualFinishDate = LocalDate.now().minusDays(2).toDate(),
)
}
}
+@Preview
@Preview(name = "Night mode", uiMode = UI_MODE_NIGHT_YES)
@Composable
-private fun PreviewNightMode() {
+private fun Preview() {
BuckwheatTheme {
WholeBudgetCard(
modifier = Modifier.height(IntrinsicSize.Min),
diff --git a/app/src/main/java/com/danilkinkin/buckwheat/data/SpendsViewModel.kt b/app/src/main/java/com/danilkinkin/buckwheat/data/SpendsViewModel.kt
index 38a5e91..90de9f6 100644
--- a/app/src/main/java/com/danilkinkin/buckwheat/data/SpendsViewModel.kt
+++ b/app/src/main/java/com/danilkinkin/buckwheat/data/SpendsViewModel.kt
@@ -34,10 +34,12 @@ class SpendsViewModel @Inject constructor(
var spentFromDailyBudget = spendsRepository.getSpentFromDailyBudget().asLiveData()
var startPeriodDate = spendsRepository.getStartPeriodDate().asLiveData()
var finishPeriodDate = spendsRepository.getFinishPeriodDate().asLiveData()
+ var finishPeriodActualDate = spendsRepository.getFinishPeriodActualDate().asLiveData()
var lastChangeDailyBudgetDate = spendsRepository.getLastChangeDailyBudgetDate().asLiveData()
var currency = spendsRepository.getCurrency().asLiveData()
- var restedBudgetDistributionMethod = spendsRepository.getRestedBudgetDistributionMethod().asLiveData()
+ var restedBudgetDistributionMethod =
+ spendsRepository.getRestedBudgetDistributionMethod().asLiveData()
var hideOverspendingWarn = spendsRepository.getHideOverspendingWarn().asLiveData()
var requireDistributionRestedBudget = MutableLiveData(false)
@@ -61,6 +63,15 @@ class SpendsViewModel @Inject constructor(
}
}
+ fun finishBudget() {
+ viewModelScope.launch {
+ spendsRepository.finishBudget(Date())
+
+ requireSetBudget.value = false
+ periodFinished.value = true
+ }
+ }
+
fun setDailyBudget(newDailyBudget: BigDecimal) {
viewModelScope.launch {
spendsRepository.setDailyBudget(newDailyBudget)
@@ -130,24 +141,42 @@ class SpendsViewModel @Inject constructor(
viewModelScope.launch {
val lastChangeDailyBudgetDate = spendsRepository.getLastChangeDailyBudgetDate().first()
val finishPeriodDate = spendsRepository.getFinishPeriodDate().first()
+ val finishPeriodActualDate = spendsRepository.getFinishPeriodActualDate().first()
val dailyBudget = spendsRepository.getDailyBudget().first()
val spentFromDailyBudget = spendsRepository.getSpentFromDailyBudget().first()
- val restedBudgetDistributionMethod = spendsRepository.getRestedBudgetDistributionMethod().first()
+ val restedBudgetDistributionMethod =
+ spendsRepository.getRestedBudgetDistributionMethod().first()
+
+ val finishDayNotReached = if (finishPeriodActualDate === null) {
+ finishPeriodDate !== null
+ && countDaysToToday(finishPeriodDate) > 0
+ } else {
+ countDaysToToday(finishPeriodActualDate) > 0
+ }
+
+ val finishTimeReached = if (finishPeriodActualDate === null) {
+ finishPeriodDate !== null
+ && finishPeriodDate.time <= Date().time
+ } else {
+ finishPeriodActualDate.time <= Date().time
+ }
when {
lastChangeDailyBudgetDate !== null
- && finishPeriodDate !== null
&& !isToday(lastChangeDailyBudgetDate)
- && countDaysToToday(finishPeriodDate) > 0 -> {
+ && finishDayNotReached -> {
if (dailyBudget - spentFromDailyBudget > BigDecimal.ZERO) {
when (restedBudgetDistributionMethod) {
RestedBudgetDistributionMethod.ASK -> {
requireDistributionRestedBudget.value = true
}
+
RestedBudgetDistributionMethod.REST -> {
- val whatBudgetForDay = spendsRepository.whatBudgetForDay(applyTodaySpends = true)
+ val whatBudgetForDay =
+ spendsRepository.whatBudgetForDay(applyTodaySpends = true)
setDailyBudget(whatBudgetForDay)
}
+
RestedBudgetDistributionMethod.ADD_TODAY -> {
val notSpent = spendsRepository.howMuchNotSpent(
excludeSkippedPart = true,
@@ -157,7 +186,8 @@ class SpendsViewModel @Inject constructor(
}
}
} else {
- val whatBudgetForDay = spendsRepository.whatBudgetForDay(applyTodaySpends = true)
+ val whatBudgetForDay =
+ spendsRepository.whatBudgetForDay(applyTodaySpends = true)
setDailyBudget(whatBudgetForDay)
}
}
@@ -166,7 +196,7 @@ class SpendsViewModel @Inject constructor(
requireSetBudget.value = true
}
- finishPeriodDate !== null && finishPeriodDate.time <= Date().time -> {
+ finishTimeReached -> {
periodFinished.value = true
}
}
diff --git a/app/src/main/java/com/danilkinkin/buckwheat/di/SpendsRepository.kt b/app/src/main/java/com/danilkinkin/buckwheat/di/SpendsRepository.kt
index 20565a2..7933d69 100644
--- a/app/src/main/java/com/danilkinkin/buckwheat/di/SpendsRepository.kt
+++ b/app/src/main/java/com/danilkinkin/buckwheat/di/SpendsRepository.kt
@@ -39,6 +39,7 @@ val spentFromDailyBudgetStoreKey = stringPreferencesKey("spentFromDailyBudget")
val lastChangeDailyBudgetDateStoreKey = longPreferencesKey("lastChangeDailyBudgetDate")
val startPeriodDateStoreKey = longPreferencesKey("startPeriodDate")
val finishPeriodDateStoreKey = longPreferencesKey("finishPeriodDate")
+val finishPeriodActualDateStoreKey = longPreferencesKey("finishPeriodActualDate")
class SpendsRepository @Inject constructor(
@ApplicationContext val context: Context,
@@ -84,6 +85,10 @@ class SpendsRepository @Inject constructor(
it[finishPeriodDateStoreKey]?.let { value -> Date(value) }
}
+ fun getFinishPeriodActualDate() = context.budgetDataStore.data.map {
+ it[finishPeriodActualDateStoreKey]?.let { value -> Date(value) }
+ }
+
fun getLastChangeDailyBudgetDate() = context.budgetDataStore.data.map {
it[lastChangeDailyBudgetDateStoreKey]?.let { value -> Date(value) }
}
@@ -157,6 +162,22 @@ class SpendsRepository @Inject constructor(
hideOverspendingWarn(false)
}
+ suspend fun finishBudget(finishDate: Date) {
+ context.budgetDataStore.edit {
+ it[finishPeriodActualDateStoreKey] = finishDate.time
+
+ Log.d(
+ "SpendsRepository",
+ "Finish budget ["
+ + "budget: ${it[budgetStoreKey]} "
+ + "start date: ${Date(it[startPeriodDateStoreKey]!!)} "
+ + "actual finish date: ${Date(it[finishPeriodActualDateStoreKey]!!)}"
+ + "finish date: ${Date(it[finishPeriodDateStoreKey]!!)}"
+ + "]"
+ )
+ }
+ }
+
suspend fun setDailyBudget(newDailyBudget: BigDecimal) {
context.budgetDataStore.edit {
val spent: BigDecimal = it[spentStoreKey]?.toBigDecimal()!!
diff --git a/app/src/main/java/com/danilkinkin/buckwheat/wallet/CustomCurrencyEditor.kt b/app/src/main/java/com/danilkinkin/buckwheat/wallet/CustomCurrencyEditor.kt
index e24f759..0f35cbd 100644
--- a/app/src/main/java/com/danilkinkin/buckwheat/wallet/CustomCurrencyEditor.kt
+++ b/app/src/main/java/com/danilkinkin/buckwheat/wallet/CustomCurrencyEditor.kt
@@ -174,6 +174,7 @@ fun CustomCurrencyEditor(
}
@Preview(name = "Default")
+@Preview(name = "Night mode", uiMode = UI_MODE_NIGHT_YES)
@Composable
private fun PreviewDefault() {
BuckwheatTheme {
@@ -184,15 +185,3 @@ private fun PreviewDefault() {
)
}
}
-
-@Preview(name = "Night mode", uiMode = UI_MODE_NIGHT_YES)
-@Composable
-private fun PreviewNightMode() {
- BuckwheatTheme {
- CustomCurrencyEditorContent(
- defaultCurrency = "",
- onChange = { },
- onClose = { }
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/danilkinkin/buckwheat/wallet/FinishEarlyConfirm.kt b/app/src/main/java/com/danilkinkin/buckwheat/wallet/FinishEarlyConfirm.kt
new file mode 100644
index 0000000..7d86fb5
--- /dev/null
+++ b/app/src/main/java/com/danilkinkin/buckwheat/wallet/FinishEarlyConfirm.kt
@@ -0,0 +1,123 @@
+package com.danilkinkin.buckwheat.wallet
+
+import OverrideLocalize
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import com.danilkinkin.buckwheat.R
+import com.danilkinkin.buckwheat.base.RenderAdaptivePane
+import com.danilkinkin.buckwheat.ui.BuckwheatTheme
+
+
+@Composable
+fun ConfirmFinishEarly(
+ onConfirm: () -> Unit,
+ onClose: () -> Unit,
+) {
+ Card(
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier
+ .widthIn(max = 440.dp)
+ .padding(36.dp)
+ ) {
+ Column {
+ Box(
+ modifier = Modifier.fillMaxWidth().padding(top = 24.dp, bottom = 16.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_priority_high),
+ tint = LocalContentColor.current.copy(alpha = 0.7f),
+ contentDescription = null,
+ )
+ }
+ Text(
+ text = stringResource(R.string.confirm_finish_budget_title),
+ style = MaterialTheme.typography.titleLarge,
+ modifier = Modifier
+ .padding(horizontal = 24.dp)
+ .padding(bottom = 24.dp)
+ .fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ )
+ Text(
+ text = stringResource(R.string.confirm_finish_budget_description),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(horizontal = 24.dp),
+ )
+ Row(
+ horizontalArrangement = Arrangement.End,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 12.dp, horizontal = 16.dp),
+ ) {
+ Button(
+ onClick = { onClose() },
+ colors = ButtonDefaults.textButtonColors(),
+ contentPadding = ButtonDefaults.TextButtonContentPadding,
+ ) {
+ Text(text = stringResource(R.string.cancel))
+ }
+ Button(
+ onClick = {
+ onConfirm()
+ onClose()
+ },
+ colors = ButtonDefaults.textButtonColors(),
+ contentPadding = ButtonDefaults.TextButtonContentPadding,
+ ) {
+ Text(text = stringResource(R.string.confirm_finish_budget))
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun ConfirmFinishEarlyDialog(
+ onConfirm: () -> Unit,
+ onClose: () -> Unit,
+) {
+ Dialog(
+ onDismissRequest = { onClose() },
+ properties = DialogProperties(usePlatformDefaultWidth = false)
+ ) {
+ OverrideLocalize {
+ RenderAdaptivePane {
+ ConfirmFinishEarly(
+ onConfirm = onConfirm,
+ onClose = onClose
+ )
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun PreviewDefault() {
+ BuckwheatTheme {
+ ConfirmFinishEarly({}, {})
+ }
+}
diff --git a/app/src/main/java/com/danilkinkin/buckwheat/wallet/Wallet.kt b/app/src/main/java/com/danilkinkin/buckwheat/wallet/Wallet.kt
index 210d774..b964e64 100644
--- a/app/src/main/java/com/danilkinkin/buckwheat/wallet/Wallet.kt
+++ b/app/src/main/java/com/danilkinkin/buckwheat/wallet/Wallet.kt
@@ -65,6 +65,7 @@ fun Wallet(
(budgetCache - spent - spentFromDailyBudget)
val openConfirmChangeBudgetDialog = remember { mutableStateOf(false) }
+ val openConfirmFinishBudgetDialog = remember { mutableStateOf(false) }
if (spends === null) return
@@ -265,6 +266,18 @@ fun Wallet(
text = stringResource(R.string.export_to_csv),
onClick = { exportCSVLaunch() }
)
+
+ CompositionLocalProvider(
+ LocalContentColor provides MaterialTheme.colorScheme.error
+ ) {
+ ButtonRow(
+ icon = painterResource(R.drawable.ic_close),
+ text = stringResource(R.string.finish_early),
+ onClick = {
+ openConfirmFinishBudgetDialog.value = true
+ }
+ )
+ }
}
}
AnimatedVisibility(
@@ -344,6 +357,18 @@ fun Wallet(
onClose = { openConfirmChangeBudgetDialog.value = false },
)
}
+
+ if (openConfirmFinishBudgetDialog.value) {
+ ConfirmFinishEarlyDialog(
+ onConfirm = {
+ spendsViewModel.finishBudget()
+
+ onClose()
+ haptic.performHapticFeedback(HapticFeedbackType.LongPress)
+ },
+ onClose = { openConfirmFinishBudgetDialog.value = false },
+ )
+ }
}
@Preview
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 84fcbf5..7381c52 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -149,4 +149,8 @@
History
View analytics
Analytics
+ Finish early
+ Finish budget
+ Your current budget finished
+ Finish budget