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 时钟源默认系统时钟测试时可注入固定时钟 * @param clock 时钟源默认系统时钟测试时可注入固定时钟
*/ */
class CalendarViewModel( class CalendarViewModel(
private val clock: Clock = Clock.System private val clock: Clock = Clock.System,
private val shiftStorage: ShiftPatternStorage? = null
) : ViewModel() { ) : ViewModel() {
private val today: LocalDate = clock.todayIn(TimeZone.currentSystemDefault()) 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?>( private val _shiftPattern = MutableStateFlow(loadShiftPattern())
ShiftPattern( 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), anchorDate = LocalDate(2026, 5, 15),
cycle = listOf(ShiftKind.WORK, ShiftKind.WORK, ShiftKind.OFF, ShiftKind.OFF) 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 package plus.rua.project
import android.content.SharedPreferences
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import kotlinx.datetime.number import kotlinx.datetime.number
import plus.rua.project.ShiftKind
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
@ -127,4 +129,100 @@ class CalendarViewModelTest {
val selectedCell = days.first { it.isSelected } val selectedCell = days.first { it.isSelected }
assertEquals(15, selectedCell.date.day) 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()
}
} }