feat(shift): inject ShiftPatternStorage into CalendarViewModel

This commit is contained in:
xfy 2026-07-06 18:34:12 +08:00
parent 1f1db17384
commit ef55ffcf64
2 changed files with 120 additions and 8 deletions

View File

@ -59,7 +59,8 @@ data class CalendarUiState(
* @param clock 时钟源默认系统时钟测试时可注入固定时钟
*/
class CalendarViewModel(
private val clock: Clock = Clock.System
private val clock: Clock = Clock.System,
private val shiftStorage: ShiftPatternStorage? = null
) : ViewModel() {
private val today: LocalDate = clock.todayIn(TimeZone.currentSystemDefault())
@ -112,17 +113,30 @@ class CalendarViewModel(
/**
* 个人轮班与法定节假日完全独立,不受调休影响
* MVP 默认:2026-05-15 ,2 2 休循环后续接入设置页与持久化
* [shiftStorage] 加载;storage 为空或未存时回退到 [DEFAULT_PATTERN]
*/
private val _shiftPattern = MutableStateFlow<ShiftPattern?>(
ShiftPattern(
private val _shiftPattern = MutableStateFlow(loadShiftPattern())
val shiftPattern: StateFlow<ShiftPattern?> = _shiftPattern.asStateFlow()
private fun loadShiftPattern(): ShiftPattern =
shiftStorage?.load() ?: DEFAULT_PATTERN
/**
* 设置页返回后调用, storage 重新加载,立即生效
*/
fun refreshShiftPattern() {
_shiftPattern.value = loadShiftPattern()
}
fun shiftKindAt(date: LocalDate): ShiftKind? = shiftPattern.value?.kindAt(date)
companion object {
/** 默认轮班:2026-05-15 起,2 班 2 休循环。 */
val DEFAULT_PATTERN = ShiftPattern(
anchorDate = LocalDate(2026, 5, 15),
cycle = listOf(ShiftKind.WORK, ShiftKind.WORK, ShiftKind.OFF, ShiftKind.OFF)
)
)
val shiftPattern: StateFlow<ShiftPattern?> = _shiftPattern.asStateFlow()
fun shiftKindAt(date: LocalDate): ShiftKind? = shiftPattern.value?.kindAt(date)
}
/**
* 是否在右上角显示法定调休角标默认禁用,此时右上角让位给个人排班

View File

@ -1,9 +1,11 @@
package plus.rua.project
import android.content.SharedPreferences
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.number
import plus.rua.project.ShiftKind
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
@ -127,4 +129,100 @@ class CalendarViewModelTest {
val selectedCell = days.first { it.isSelected }
assertEquals(15, selectedCell.date.day)
}
// ---- shiftPattern: 默认值与 refresh ----
@Test
fun shiftPattern_noStorage_returnsDefault() {
val vm = createViewModel() // 不传 storage
assertEquals(ShiftKind.WORK, vm.shiftKindAt(LocalDate(2026, 5, 15)))
assertEquals(ShiftKind.OFF, vm.shiftKindAt(LocalDate(2026, 5, 17)))
}
@Test
fun refreshShiftPattern_reloadsFromStorage() {
val prefs = CalendarVmTestPrefs()
val storage = ShiftPatternStorage(prefs)
// storage 里存一个 1班1休,锚点 2026-01-01
storage.save(
ShiftPattern(
anchorDate = LocalDate(2026, 1, 1),
cycle = listOf(ShiftKind.WORK, ShiftKind.OFF)
)
)
val vm = CalendarViewModel(clock = testClock, shiftStorage = storage)
// 初始即从 storage 读
assertEquals(ShiftKind.WORK, vm.shiftKindAt(LocalDate(2026, 1, 1)))
assertEquals(ShiftKind.OFF, vm.shiftKindAt(LocalDate(2026, 1, 2)))
}
}
private class CalendarVmTestPrefs : SharedPreferences {
private val data = mutableMapOf<String, Any?>()
override fun getAll(): Map<String, *> = data.toMap()
override fun getString(key: String, defValue: String?): String? =
data[key] as? String ?: defValue
override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? =
data[key] as? Set<String> ?: defValues
override fun getInt(key: String, defValue: Int): Int =
data[key] as? Int ?: defValue
override fun getLong(key: String, defValue: Long): Long =
data[key] as? Long ?: defValue
override fun getFloat(key: String, defValue: Float): Float =
data[key] as? Float ?: defValue
override fun getBoolean(key: String, defValue: Boolean): Boolean =
data[key] as? Boolean ?: defValue
override fun contains(key: String): Boolean = data.containsKey(key)
override fun edit(): SharedPreferences.Editor = CalendarVmTestPrefsEditor(data)
override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) {}
override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) {}
}
private class CalendarVmTestPrefsEditor(private val data: MutableMap<String, Any?>) : SharedPreferences.Editor {
private val pending = mutableMapOf<String, Any?>()
private var clearPending = false
override fun putString(key: String, value: String?): SharedPreferences.Editor = apply {
pending[key] = value
}
override fun putStringSet(key: String, values: Set<String>?): SharedPreferences.Editor = apply {
pending[key] = values
}
override fun putInt(key: String, value: Int): SharedPreferences.Editor = apply { pending[key] = value }
override fun putLong(key: String, value: Long): SharedPreferences.Editor = apply { pending[key] = value }
override fun putFloat(key: String, value: Float): SharedPreferences.Editor = apply { pending[key] = value }
override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor = apply { pending[key] = value }
override fun remove(key: String): SharedPreferences.Editor = apply { pending[key] = null }
override fun clear(): SharedPreferences.Editor = apply { clearPending = true }
override fun commit(): Boolean {
apply()
return true
}
override fun apply() {
if (clearPending) {
data.clear()
clearPending = false
}
data.putAll(pending)
pending.clear()
}
}