Merge branch 'fix/date-checker-negative-days'
修复日期检查器保质期天数负数 bug: clampExpiryDays 纯函数钳制非负(含 4 单测);两个日期写入路径(选择器确认+行内输入)兜底;DatePicker SelectableDates 禁选早于生产日期(仅 Row);加载清理历史持久化负数旧数据
This commit is contained in:
commit
1cb35d2752
@ -43,6 +43,7 @@ import androidx.compose.material.icons.filled.Add
|
|||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SelectableDates
|
||||||
import androidx.compose.material3.SwipeToDismissBox
|
import androidx.compose.material3.SwipeToDismissBox
|
||||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@ -142,8 +143,9 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
|||||||
var productionDate by remember { mutableStateOf(saved?.first ?: today) }
|
var productionDate by remember { mutableStateOf(saved?.first ?: today) }
|
||||||
var rows by remember {
|
var rows by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
|
// clampExpiryDays 兜底:清理本修复前可能持久化的负数旧数据
|
||||||
(saved?.second ?: defaultRows).mapIndexed { index, days ->
|
(saved?.second ?: defaultRows).mapIndexed { index, days ->
|
||||||
ExpiryRow(index, days)
|
ExpiryRow(index, clampExpiryDays(days))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -365,7 +367,8 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onExpiryDateChange = { newDate ->
|
onExpiryDateChange = { newDate ->
|
||||||
val newDays = productionDate.daysUntil(newDate)
|
val rawDays = productionDate.daysUntil(newDate)
|
||||||
|
val newDays = clampExpiryDays(rawDays)
|
||||||
rows = rows.map {
|
rows = rows.map {
|
||||||
if (it.id == row.id) it.copy(days = newDays) else it
|
if (it.id == row.id) it.copy(days = newDays) else it
|
||||||
}
|
}
|
||||||
@ -452,7 +455,23 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
|||||||
null -> productionDate.toEpochMillis()
|
null -> productionDate.toEpochMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
val datePickerState = rememberDatePickerState(initialSelectedDateMillis = initialMillis)
|
val productionMillis = productionDate.toEpochMillis()
|
||||||
|
// Row 日期选择器禁选早于生产日期(到期日不应在生产之前);
|
||||||
|
// Production 选择器本身不受限制。当前 BOM 无 SelectableDates.AllDates,
|
||||||
|
// 用空实现 object 等价于默认全允许(default 方法均返回 true)。
|
||||||
|
val datePickerState = rememberDatePickerState(
|
||||||
|
initialSelectedDateMillis = initialMillis,
|
||||||
|
selectableDates = when (datePickerTarget) {
|
||||||
|
is DatePickerTarget.Row -> object : SelectableDates {
|
||||||
|
override fun isSelectableDate(utcTimeMillis: Long): Boolean =
|
||||||
|
utcTimeMillis >= productionMillis
|
||||||
|
|
||||||
|
override fun isSelectableYear(year: Int): Boolean =
|
||||||
|
year >= productionDate.year
|
||||||
|
}
|
||||||
|
else -> object : SelectableDates {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
DatePickerDialog(
|
DatePickerDialog(
|
||||||
onDismissRequest = { showDatePicker = false },
|
onDismissRequest = { showDatePicker = false },
|
||||||
@ -463,7 +482,8 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
|||||||
when (val target = datePickerTarget) {
|
when (val target = datePickerTarget) {
|
||||||
is DatePickerTarget.Production -> productionDate = selected
|
is DatePickerTarget.Production -> productionDate = selected
|
||||||
is DatePickerTarget.Row -> {
|
is DatePickerTarget.Row -> {
|
||||||
val newDays = productionDate.daysUntil(selected)
|
val rawDays = productionDate.daysUntil(selected)
|
||||||
|
val newDays = clampExpiryDays(rawDays)
|
||||||
rows = rows.map {
|
rows = rows.map {
|
||||||
if (it.id == target.rowId) it.copy(days = newDays) else it
|
if (it.id == target.rowId) it.copy(days = newDays) else it
|
||||||
}
|
}
|
||||||
@ -745,6 +765,17 @@ private fun ArrowRightIcon(color: Color, modifier: Modifier = Modifier) {
|
|||||||
|
|
||||||
// region Helpers
|
// region Helpers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将保质期天数钳制到合法范围 [0, +∞)。
|
||||||
|
*
|
||||||
|
* 天数语义上不能为负(到期日不应早于生产日期)。
|
||||||
|
* 无论来自天数输入框还是日期选择器,写入 [ExpiryRow.days] 前都应经过此函数。
|
||||||
|
*
|
||||||
|
* @param days 原始天数
|
||||||
|
* @return 钳制后的天数,最小为 0
|
||||||
|
*/
|
||||||
|
fun clampExpiryDays(days: Int): Int = days.coerceAtLeast(0)
|
||||||
|
|
||||||
private fun LocalDate.toEpochMillis(): Long =
|
private fun LocalDate.toEpochMillis(): Long =
|
||||||
this.atStartOfDayIn(TimeZone.UTC).toEpochMilliseconds()
|
this.atStartOfDayIn(TimeZone.UTC).toEpochMilliseconds()
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
package plus.rua.project.ui
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class DateCheckerScreenLogicTest {
|
||||||
|
|
||||||
|
// ---- clampExpiryDays ----
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clampExpiryDays_positiveValue_unchanged() {
|
||||||
|
assertEquals(30, clampExpiryDays(30))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clampExpiryDays_zero_unchanged() {
|
||||||
|
assertEquals(0, clampExpiryDays(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clampExpiryDays_negativeValue_clampedToZero() {
|
||||||
|
assertEquals(0, clampExpiryDays(-1))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clampExpiryDays_largeNegative_clampedToZero() {
|
||||||
|
assertEquals(0, clampExpiryDays(-365))
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user