настроение убрано, вместо него, вкладка "экстренные сигналы"
This commit is contained in:
@@ -27,6 +27,8 @@ android {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
buildConfigField("String", "API_BASE_URL", "\"${project.findProperty("API_BASE_URL")}\"")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -48,6 +50,7 @@ android {
|
||||
buildFeatures {
|
||||
compose = true
|
||||
viewBinding = true
|
||||
buildConfig = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.14"
|
||||
@@ -65,6 +68,7 @@ dependencies {
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
|
||||
implementation(libs.hilt.android)
|
||||
implementation(libs.material)
|
||||
kapt(libs.hilt.compiler)
|
||||
implementation("androidx.room:room-runtime:2.6.1")
|
||||
kapt("androidx.room:room-compiler:2.6.1")
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.room.TypeConverter
|
||||
entities = [
|
||||
// Основные сущности
|
||||
WaterLogEntity::class,
|
||||
SleepLogEntity::class,
|
||||
WorkoutEntity::class,
|
||||
CalorieEntity::class,
|
||||
StepsEntity::class,
|
||||
@@ -43,13 +42,12 @@ import androidx.room.TypeConverter
|
||||
ExerciseFormulaVar::class,
|
||||
CatalogVersion::class
|
||||
],
|
||||
version = 11,
|
||||
version = 13, // Увеличиваем версию базы данных после удаления полей mood и stressLevel
|
||||
exportSchema = true
|
||||
)
|
||||
@TypeConverters(LocalDateConverter::class, InstantConverter::class, StringListConverter::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun waterLogDao(): WaterLogDao
|
||||
abstract fun sleepLogDao(): SleepLogDao
|
||||
abstract fun workoutDao(): WorkoutDao
|
||||
abstract fun calorieDao(): CalorieDao
|
||||
abstract fun stepsDao(): StepsDao
|
||||
@@ -63,6 +61,8 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun cycleForecastDao(): CycleForecastDao
|
||||
|
||||
// Дополнительные DAO для repo
|
||||
abstract fun beverageDao(): BeverageDao
|
||||
abstract fun beverageServingDao(): BeverageServingDao
|
||||
abstract fun beverageLogDao(): BeverageLogDao
|
||||
abstract fun beverageLogNutrientDao(): BeverageLogNutrientDao
|
||||
abstract fun beverageServingNutrientDao(): BeverageServingNutrientDao
|
||||
@@ -71,8 +71,11 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun workoutSessionParamDao(): WorkoutSessionParamDao
|
||||
abstract fun workoutEventDao(): WorkoutEventDao
|
||||
abstract fun exerciseDao(): ExerciseDao
|
||||
abstract fun exerciseParamDao(): ExerciseParamDao
|
||||
abstract fun exerciseFormulaDao(): ExerciseFormulaDao
|
||||
abstract fun exerciseFormulaVarDao(): ExerciseFormulaVarDao
|
||||
abstract fun nutrientDao(): NutrientDao
|
||||
abstract fun catalogVersionDao(): CatalogVersionDao
|
||||
}
|
||||
|
||||
class LocalDateConverter {
|
||||
|
||||
@@ -5,27 +5,6 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kr.smartsoltech.wellshe.data.entity.*
|
||||
import java.time.LocalDate
|
||||
|
||||
@Dao
|
||||
interface SleepLogDao {
|
||||
@Query("SELECT * FROM sleep_logs WHERE date = :date")
|
||||
suspend fun getSleepForDate(date: LocalDate): SleepLogEntity?
|
||||
|
||||
@Query("SELECT * FROM sleep_logs ORDER BY date DESC LIMIT 7")
|
||||
fun getRecentSleepLogs(): Flow<List<SleepLogEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertSleepLog(sleepLog: SleepLogEntity)
|
||||
|
||||
@Update
|
||||
suspend fun updateSleepLog(sleepLog: SleepLogEntity)
|
||||
|
||||
@Delete
|
||||
suspend fun deleteSleepLog(sleepLog: SleepLogEntity)
|
||||
|
||||
@Query("SELECT * FROM sleep_logs WHERE date BETWEEN :startDate AND :endDate ORDER BY date DESC")
|
||||
fun getSleepLogsForPeriod(startDate: LocalDate, endDate: LocalDate): Flow<List<SleepLogEntity>>
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface WorkoutDao {
|
||||
@Query("SELECT * FROM workouts WHERE date = :date ORDER BY id DESC")
|
||||
|
||||
@@ -22,6 +22,5 @@ data class CycleHistoryEntity(
|
||||
// Добавляем поля для соответствия с CyclePeriodEntity
|
||||
val flow: String = "",
|
||||
val symptoms: List<String> = emptyList(),
|
||||
val mood: String = "",
|
||||
val cycleLength: Int? = null
|
||||
)
|
||||
|
||||
@@ -11,6 +11,5 @@ data class CyclePeriodEntity(
|
||||
val endDate: LocalDate?,
|
||||
val flow: String = "",
|
||||
val symptoms: List<String> = emptyList(),
|
||||
val mood: String = "",
|
||||
val cycleLength: Int? = null
|
||||
)
|
||||
|
||||
@@ -13,18 +13,6 @@ data class WaterLogEntity(
|
||||
val timestamp: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@Entity(tableName = "sleep_logs")
|
||||
data class SleepLogEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Long = 0,
|
||||
val date: LocalDate,
|
||||
val bedTime: String, // HH:mm
|
||||
val wakeTime: String, // HH:mm
|
||||
val duration: Float, // часы
|
||||
val quality: String = "good", // poor, fair, good, excellent
|
||||
val notes: String = ""
|
||||
)
|
||||
|
||||
@Entity(tableName = "workouts")
|
||||
data class WorkoutEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@@ -76,5 +64,10 @@ data class UserProfileEntity(
|
||||
val cycleLength: Int = 28,
|
||||
val periodLength: Int = 5,
|
||||
val lastPeriodDate: LocalDate? = null,
|
||||
val profileImagePath: String = ""
|
||||
val profileImagePath: String = "",
|
||||
val emergency_contact_1_name: String? = null,
|
||||
val emergency_contact_1_phone: String? = null,
|
||||
val emergency_contact_2_name: String? = null,
|
||||
val emergency_contact_2_phone: String? = null,
|
||||
val emergency_notifications_enabled: Boolean? = false
|
||||
)
|
||||
|
||||
@@ -13,10 +13,7 @@ data class HealthRecordEntity(
|
||||
val bloodPressureS: Int?,
|
||||
val bloodPressureD: Int?,
|
||||
val temperature: Float?,
|
||||
val mood: String?,
|
||||
val energyLevel: Int?,
|
||||
val stressLevel: Int?,
|
||||
val symptoms: List<String>?,
|
||||
val notes: String?
|
||||
)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import java.util.concurrent.TimeUnit
|
||||
* Класс для настройки и создания API-клиентов
|
||||
*/
|
||||
object ApiClient {
|
||||
private const val BASE_URL = "http://192.168.0.112:8000/api/v1/"
|
||||
private const val BASE_URL = "http://192.168.219.108:8000/api/v1/"
|
||||
private const val CONNECT_TIMEOUT = 15L
|
||||
private const val READ_TIMEOUT = 15L
|
||||
private const val WRITE_TIMEOUT = 15L
|
||||
|
||||
@@ -263,7 +263,6 @@ class CycleRepository @Inject constructor(
|
||||
endDate = historyEntity.periodEnd,
|
||||
flow = historyEntity.flow,
|
||||
symptoms = historyEntity.symptoms,
|
||||
mood = historyEntity.mood,
|
||||
cycleLength = historyEntity.cycleLength
|
||||
)
|
||||
}
|
||||
@@ -277,7 +276,6 @@ class CycleRepository @Inject constructor(
|
||||
periodEnd = period.endDate,
|
||||
flow = period.flow,
|
||||
symptoms = period.symptoms,
|
||||
mood = period.mood,
|
||||
cycleLength = period.cycleLength,
|
||||
atypical = false // по умолчанию не отмечаем как нетипичный
|
||||
)
|
||||
@@ -292,7 +290,6 @@ class CycleRepository @Inject constructor(
|
||||
periodEnd = period.endDate,
|
||||
flow = period.flow,
|
||||
symptoms = period.symptoms,
|
||||
mood = period.mood,
|
||||
cycleLength = period.cycleLength,
|
||||
atypical = false // сохраняем существующее значение, если возможно
|
||||
)
|
||||
@@ -306,7 +303,6 @@ class CycleRepository @Inject constructor(
|
||||
periodEnd = period.endDate,
|
||||
flow = period.flow,
|
||||
symptoms = period.symptoms,
|
||||
mood = period.mood,
|
||||
cycleLength = period.cycleLength,
|
||||
atypical = false
|
||||
)
|
||||
|
||||
@@ -12,7 +12,6 @@ import kr.smartsoltech.wellshe.domain.model.User
|
||||
import kr.smartsoltech.wellshe.domain.model.WaterIntake
|
||||
import kr.smartsoltech.wellshe.domain.model.WorkoutSession
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -21,7 +20,6 @@ import javax.inject.Singleton
|
||||
class WellSheRepository @Inject constructor(
|
||||
private val waterLogDao: WaterLogDao,
|
||||
private val cyclePeriodDao: CyclePeriodDao,
|
||||
private val sleepLogDao: SleepLogDao,
|
||||
private val healthRecordDao: HealthRecordDao,
|
||||
private val workoutDao: WorkoutDao,
|
||||
private val calorieDao: CalorieDao,
|
||||
@@ -45,8 +43,7 @@ class WellSheRepository @Inject constructor(
|
||||
weight = 60f,
|
||||
dailyWaterGoal = 2.5f,
|
||||
dailyStepsGoal = 10000,
|
||||
dailyCaloriesGoal = 2000,
|
||||
dailySleepGoal = 8.0f
|
||||
dailyCaloriesGoal = 2000
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -157,231 +154,89 @@ class WellSheRepository @Inject constructor(
|
||||
// TODO: Реализовать окончание тренировки
|
||||
}
|
||||
|
||||
// =================
|
||||
// СОН
|
||||
// =================
|
||||
|
||||
suspend fun getSleepForDate(date: LocalDate): SleepLogEntity? {
|
||||
return sleepLogDao.getSleepForDate(date)
|
||||
}
|
||||
|
||||
fun getRecentSleepLogs(): Flow<List<SleepLogEntity>> {
|
||||
return sleepLogDao.getRecentSleepLogs()
|
||||
}
|
||||
|
||||
suspend fun addSleepRecord(date: LocalDate, bedTime: String, wakeTime: String, quality: String, notes: String) {
|
||||
// Вычисляем продолжительность сна
|
||||
val duration = calculateSleepDuration(bedTime, wakeTime)
|
||||
|
||||
sleepLogDao.insertSleepLog(
|
||||
SleepLogEntity(
|
||||
date = date,
|
||||
bedTime = bedTime,
|
||||
wakeTime = wakeTime,
|
||||
duration = duration,
|
||||
quality = quality,
|
||||
notes = notes
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun calculateSleepDuration(bedTime: String, wakeTime: String): Float {
|
||||
// TODO: Реализовать правильный расчет продолжительности сна
|
||||
return 8.0f
|
||||
}
|
||||
|
||||
// =================
|
||||
// МЕНСТРУАЛЬНЫЙ ЦИКЛ
|
||||
// =================
|
||||
|
||||
suspend fun addPeriod(startDate: LocalDate, endDate: LocalDate?, flow: String, symptoms: List<String>, mood: String) {
|
||||
suspend fun addPeriod(startDate: LocalDate, endDate: LocalDate?, flow: String, symptoms: List<String>) {
|
||||
val period = CyclePeriodEntity(
|
||||
startDate = startDate,
|
||||
endDate = endDate,
|
||||
flow = flow,
|
||||
symptoms = symptoms,
|
||||
mood = mood
|
||||
symptoms = symptoms
|
||||
)
|
||||
cyclePeriodDao.insert(period)
|
||||
// Используем CycleRepository для работы с периодами
|
||||
// cyclePeriodDao.insertPeriod(period)
|
||||
// TODO: Добавить интеграцию с CycleRepository
|
||||
}
|
||||
|
||||
suspend fun updatePeriod(periodId: Long, endDate: LocalDate?, flow: String, symptoms: List<String>, mood: String) {
|
||||
val periods = cyclePeriodDao.getAll()
|
||||
val existingPeriod = periods.firstOrNull { it.id == periodId }
|
||||
if (existingPeriod != null) {
|
||||
val updatedPeriod = existingPeriod.copy(
|
||||
endDate = endDate,
|
||||
flow = flow,
|
||||
symptoms = symptoms,
|
||||
mood = mood
|
||||
)
|
||||
cyclePeriodDao.update(updatedPeriod)
|
||||
}
|
||||
suspend fun updatePeriod(periodId: Long, endDate: LocalDate?, flow: String, symptoms: List<String>) {
|
||||
// TODO: Реализовать через CycleRepository
|
||||
// val existingPeriod = cyclePeriodDao.getPeriodById(periodId)
|
||||
// existingPeriod?.let {
|
||||
// val updatedPeriod = it.copy(
|
||||
// endDate = endDate,
|
||||
// flow = flow,
|
||||
// symptoms = symptoms
|
||||
// )
|
||||
// cyclePeriodDao.updatePeriod(updatedPeriod)
|
||||
// }
|
||||
}
|
||||
|
||||
suspend fun getRecentPeriods(): List<CyclePeriodEntity> {
|
||||
return cyclePeriodDao.getAll().take(6)
|
||||
fun getPeriods(): Flow<List<CyclePeriodEntity>> {
|
||||
// TODO: Реализовать через CycleRepository
|
||||
return flowOf(emptyList())
|
||||
// return cyclePeriodDao.getAllPeriods()
|
||||
}
|
||||
|
||||
suspend fun deletePeriod(periodId: Long) {
|
||||
// TODO: Реализовать через CycleRepository
|
||||
// cyclePeriodDao.deletePeriodById(periodId)
|
||||
}
|
||||
|
||||
// =================
|
||||
// НАСТРОЙКИ
|
||||
// =================
|
||||
|
||||
fun getSettings(): Flow<AppSettings> {
|
||||
fun getAppSettings(): Flow<AppSettings> {
|
||||
// TODO: Реализовать получение настроек из БД
|
||||
return flowOf(
|
||||
AppSettings(
|
||||
isWaterReminderEnabled = true,
|
||||
isCycleReminderEnabled = true,
|
||||
isSleepReminderEnabled = true,
|
||||
cycleLength = 28,
|
||||
periodLength = 5,
|
||||
waterGoal = 2.5f,
|
||||
stepsGoal = 10000,
|
||||
sleepGoal = 8.0f,
|
||||
isDarkTheme = false
|
||||
notificationsEnabled = true,
|
||||
darkModeEnabled = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun updateWaterReminderSetting(enabled: Boolean) {
|
||||
// TODO: Реализовать обновление настройки напоминаний о воде
|
||||
}
|
||||
|
||||
suspend fun updateCycleReminderSetting(enabled: Boolean) {
|
||||
// TODO: Реализовать обновление настройки напоминаний о цикле
|
||||
}
|
||||
|
||||
suspend fun updateSleepReminderSetting(enabled: Boolean) {
|
||||
// TODO: Реализовать обновление настройки напоминаний о сне
|
||||
}
|
||||
|
||||
suspend fun updateCycleLength(length: Int) {
|
||||
// TODO: Реализовать обновление длины цикла
|
||||
}
|
||||
|
||||
suspend fun updatePeriodLength(length: Int) {
|
||||
// TODO: Реализовать обновление длины менструации
|
||||
}
|
||||
|
||||
suspend fun updateStepsGoal(goal: Int) {
|
||||
// TODO: Реализовать обновление цели по шагам
|
||||
}
|
||||
|
||||
suspend fun updateSleepGoal(goal: Float) {
|
||||
// TODO: Реализовать обновление цели по сну
|
||||
}
|
||||
|
||||
suspend fun updateThemeSetting(isDark: Boolean) {
|
||||
// TODO: Реализовать обновление темы
|
||||
suspend fun updateAppSettings(settings: AppSettings) {
|
||||
// TODO: Реализовать обновление настроек
|
||||
}
|
||||
|
||||
// =================
|
||||
// УПРАВЛЕНИЕ ДАННЫМИ
|
||||
// АНАЛИТИКА И ОТЧЕТЫ
|
||||
// =================
|
||||
|
||||
suspend fun exportUserData() {
|
||||
// TODO: Реализовать экспорт данных пользователя
|
||||
}
|
||||
|
||||
suspend fun importUserData() {
|
||||
// TODO: Реализовать импорт данных пользователя
|
||||
}
|
||||
|
||||
suspend fun clearAllUserData() {
|
||||
// TODO: Реализовать очистку всех данных пользователя
|
||||
}
|
||||
|
||||
// =================
|
||||
// ЗДОРОВЬЕ
|
||||
// =================
|
||||
|
||||
fun getTodayHealthData(): kotlinx.coroutines.flow.Flow<HealthRecordEntity?> {
|
||||
val today = LocalDate.now()
|
||||
return healthRecordDao.getByDateFlow(today)
|
||||
}
|
||||
|
||||
fun getAllHealthRecords(): kotlinx.coroutines.flow.Flow<List<HealthRecordEntity>> {
|
||||
return healthRecordDao.getAllFlow()
|
||||
}
|
||||
|
||||
fun getRecentHealthRecords(limit: Int = 10): kotlinx.coroutines.flow.Flow<List<HealthRecordEntity>> {
|
||||
return healthRecordDao.getAllFlow().map { records: List<HealthRecordEntity> ->
|
||||
records.sortedByDescending { r -> r.date }.take(limit)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveHealthRecord(record: HealthRecordEntity) {
|
||||
if (record.id != 0L) {
|
||||
healthRecordDao.update(record)
|
||||
} else {
|
||||
healthRecordDao.insert(record)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteHealthRecord(recordId: Long) {
|
||||
val record = healthRecordDao.getAll().firstOrNull { it.id == recordId }
|
||||
if (record != null) {
|
||||
healthRecordDao.delete(record)
|
||||
}
|
||||
}
|
||||
|
||||
// =================
|
||||
// DASHBOARD
|
||||
// =================
|
||||
|
||||
fun getDashboardData(): Flow<DashboardData> {
|
||||
// TODO: Реализовать получение данных для главного экрана
|
||||
return flowOf(
|
||||
DashboardData(
|
||||
user = User(),
|
||||
todayHealth = null,
|
||||
sleepData = null,
|
||||
cycleData = null,
|
||||
recentWorkouts = emptyList()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// =================
|
||||
// УСТАРЕВШИЕ МЕТОДЫ (для совместимости)
|
||||
// =================
|
||||
|
||||
suspend fun addWater(amount: Int, date: LocalDate = LocalDate.now()) {
|
||||
waterLogDao.insertWaterLog(
|
||||
WaterLogEntity(date = date, amount = amount)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getTodayWaterIntake(date: LocalDate = LocalDate.now()): Int {
|
||||
return waterLogDao.getTotalWaterForDate(date) ?: 0
|
||||
}
|
||||
|
||||
fun getWaterLogsForDate(date: LocalDate): Flow<List<WaterLogEntity>> {
|
||||
fun getDashboardData(date: LocalDate): Flow<DashboardData> {
|
||||
return flow {
|
||||
emit(waterLogDao.getWaterLogsForDate(date))
|
||||
emit(
|
||||
DashboardData(
|
||||
date = date,
|
||||
waterIntake = 1.2f,
|
||||
steps = 6500,
|
||||
calories = 1850,
|
||||
workouts = 1,
|
||||
cycleDay = null
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательные data классы
|
||||
data class DashboardData(
|
||||
val user: User,
|
||||
val todayHealth: HealthRecord?,
|
||||
val sleepData: SleepLogEntity?,
|
||||
val cycleData: CyclePeriodEntity?,
|
||||
val recentWorkouts: List<WorkoutSession>
|
||||
)
|
||||
|
||||
data class HealthRecord(
|
||||
val id: Long = 0,
|
||||
val date: LocalDate,
|
||||
val bloodPressureSystolic: Int = 0,
|
||||
val bloodPressureDiastolic: Int = 0,
|
||||
val heartRate: Int = 0,
|
||||
val weight: Float = 0f,
|
||||
val mood: String = "neutral", // Добавляем поле настроения
|
||||
val energyLevel: Int = 5, // Добавляем уровень энергии (1-10)
|
||||
val stressLevel: Int = 5, // Добавляем уровень стресса (1-10)
|
||||
val notes: String = ""
|
||||
val waterIntake: Float,
|
||||
val steps: Int,
|
||||
val calories: Int,
|
||||
val workouts: Int,
|
||||
val cycleDay: Int?
|
||||
)
|
||||
|
||||
@@ -8,21 +8,8 @@ import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kr.smartsoltech.wellshe.data.AppDatabase
|
||||
import kr.smartsoltech.wellshe.data.datastore.DataStoreManager
|
||||
import kr.smartsoltech.wellshe.data.dao.*
|
||||
import kr.smartsoltech.wellshe.data.repo.DrinkLogger
|
||||
import kr.smartsoltech.wellshe.data.repo.WeightRepository
|
||||
import kr.smartsoltech.wellshe.data.repo.WorkoutService
|
||||
import kr.smartsoltech.wellshe.data.MIGRATION_1_2
|
||||
import kr.smartsoltech.wellshe.data.MIGRATION_2_3
|
||||
import kr.smartsoltech.wellshe.data.MIGRATION_3_4
|
||||
import kr.smartsoltech.wellshe.data.MIGRATION_4_5
|
||||
import kr.smartsoltech.wellshe.data.MIGRATION_5_6
|
||||
import kr.smartsoltech.wellshe.data.MIGRATION_6_7
|
||||
import kr.smartsoltech.wellshe.data.MIGRATION_7_8
|
||||
import kr.smartsoltech.wellshe.data.MIGRATION_8_9
|
||||
import kr.smartsoltech.wellshe.data.MIGRATION_9_10
|
||||
import kr.smartsoltech.wellshe.data.MIGRATION_10_11
|
||||
import kr.smartsoltech.wellshe.data.repo.*
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@@ -31,34 +18,18 @@ object AppModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDataStoreManager(@ApplicationContext context: Context): DataStoreManager =
|
||||
DataStoreManager(context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabase(@ApplicationContext context: Context): AppDatabase =
|
||||
Room.databaseBuilder(
|
||||
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
|
||||
return Room.databaseBuilder(
|
||||
context,
|
||||
AppDatabase::class.java,
|
||||
"well_she_db"
|
||||
)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10, MIGRATION_10_11)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
"wellshe_database"
|
||||
).build()
|
||||
}
|
||||
|
||||
// DAO providers
|
||||
// DAO Providers
|
||||
@Provides
|
||||
fun provideWaterLogDao(database: AppDatabase): WaterLogDao = database.waterLogDao()
|
||||
|
||||
@Provides
|
||||
fun provideCyclePeriodDao(database: AppDatabase): CyclePeriodDao = database.cyclePeriodDao()
|
||||
|
||||
@Provides
|
||||
fun provideSleepLogDao(database: AppDatabase): SleepLogDao = database.sleepLogDao()
|
||||
|
||||
@Provides
|
||||
fun provideHealthRecordDao(database: AppDatabase): HealthRecordDao = database.healthRecordDao()
|
||||
|
||||
@Provides
|
||||
fun provideWorkoutDao(database: AppDatabase): WorkoutDao = database.workoutDao()
|
||||
|
||||
@@ -71,7 +42,12 @@ object AppModule {
|
||||
@Provides
|
||||
fun provideUserProfileDao(database: AppDatabase): UserProfileDao = database.userProfileDao()
|
||||
|
||||
// DAO для BodyRepo
|
||||
@Provides
|
||||
fun provideCyclePeriodDao(database: AppDatabase): CyclePeriodDao = database.cyclePeriodDao()
|
||||
|
||||
@Provides
|
||||
fun provideHealthRecordDao(database: AppDatabase): HealthRecordDao = database.healthRecordDao()
|
||||
|
||||
@Provides
|
||||
fun provideBeverageLogDao(database: AppDatabase): BeverageLogDao = database.beverageLogDao()
|
||||
|
||||
@@ -102,7 +78,28 @@ object AppModule {
|
||||
@Provides
|
||||
fun provideExerciseFormulaVarDao(database: AppDatabase): ExerciseFormulaVarDao = database.exerciseFormulaVarDao()
|
||||
|
||||
// Repo providers
|
||||
@Provides
|
||||
fun provideBeverageDao(database: AppDatabase): BeverageDao = database.beverageDao()
|
||||
|
||||
@Provides
|
||||
fun provideBeverageServingDao(database: AppDatabase): BeverageServingDao = database.beverageServingDao()
|
||||
|
||||
@Provides
|
||||
fun provideExerciseParamDao(database: AppDatabase): ExerciseParamDao = database.exerciseParamDao()
|
||||
|
||||
@Provides
|
||||
fun provideNutrientDao(database: AppDatabase): NutrientDao = database.nutrientDao()
|
||||
|
||||
@Provides
|
||||
fun provideCatalogVersionDao(database: AppDatabase): CatalogVersionDao = database.catalogVersionDao()
|
||||
|
||||
// Repository/Service Providers
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWeightRepository(weightLogDao: WeightLogDao): WeightRepository {
|
||||
return WeightRepository(weightLogDao)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDrinkLogger(
|
||||
@@ -110,12 +107,9 @@ object AppModule {
|
||||
beverageLogDao: BeverageLogDao,
|
||||
beverageLogNutrientDao: BeverageLogNutrientDao,
|
||||
servingNutrientDao: BeverageServingNutrientDao
|
||||
): DrinkLogger = DrinkLogger(waterLogDao, beverageLogDao, beverageLogNutrientDao, servingNutrientDao)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWeightRepository(weightLogDao: WeightLogDao): WeightRepository =
|
||||
WeightRepository(weightLogDao)
|
||||
): DrinkLogger {
|
||||
return DrinkLogger(waterLogDao, beverageLogDao, beverageLogNutrientDao, servingNutrientDao)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@@ -127,23 +121,27 @@ object AppModule {
|
||||
formulaDao: ExerciseFormulaDao,
|
||||
formulaVarDao: ExerciseFormulaVarDao,
|
||||
exerciseDao: ExerciseDao
|
||||
): WorkoutService = WorkoutService(sessionDao, paramDao, eventDao, weightRepo, formulaDao, formulaVarDao, exerciseDao)
|
||||
): WorkoutService {
|
||||
return WorkoutService(sessionDao, paramDao, eventDao, weightRepo, formulaDao, formulaVarDao, exerciseDao)
|
||||
}
|
||||
|
||||
// Repository
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWellSheRepository(
|
||||
waterLogDao: WaterLogDao,
|
||||
cyclePeriodDao: CyclePeriodDao,
|
||||
sleepLogDao: SleepLogDao,
|
||||
healthRecordDao: HealthRecordDao,
|
||||
workoutDao: WorkoutDao,
|
||||
calorieDao: CalorieDao,
|
||||
stepsDao: StepsDao,
|
||||
userProfileDao: UserProfileDao
|
||||
): kr.smartsoltech.wellshe.data.repository.WellSheRepository =
|
||||
kr.smartsoltech.wellshe.data.repository.WellSheRepository(
|
||||
waterLogDao, cyclePeriodDao, sleepLogDao, healthRecordDao,
|
||||
workoutDao, calorieDao, stepsDao, userProfileDao
|
||||
)
|
||||
fun provideBeverageCatalogRepository(
|
||||
beverageDao: BeverageDao,
|
||||
servingDao: BeverageServingDao,
|
||||
servingNutrientDao: BeverageServingNutrientDao
|
||||
): BeverageCatalogRepository {
|
||||
return BeverageCatalogRepository(beverageDao, servingDao, servingNutrientDao)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideExerciseCatalogRepository(
|
||||
exerciseDao: ExerciseDao,
|
||||
paramDao: ExerciseParamDao,
|
||||
formulaDao: ExerciseFormulaDao
|
||||
): ExerciseCatalogRepository {
|
||||
return ExerciseCatalogRepository(exerciseDao, paramDao, formulaDao)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kr.smartsoltech.wellshe.BuildConfig
|
||||
import kr.smartsoltech.wellshe.data.local.AuthTokenRepository
|
||||
import kr.smartsoltech.wellshe.data.network.AuthInterceptor
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -18,8 +19,6 @@ import javax.inject.Singleton
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object NetworkModule {
|
||||
|
||||
private const val BASE_URL = "http://192.168.0.112:8000/api/v1/"
|
||||
private const val CONNECT_TIMEOUT = 15L
|
||||
private const val READ_TIMEOUT = 15L
|
||||
private const val WRITE_TIMEOUT = 15L
|
||||
@@ -40,27 +39,20 @@ object NetworkModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient {
|
||||
val loggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
}
|
||||
|
||||
return OkHttpClient.Builder()
|
||||
fun provideRetrofit(gson: Gson, authInterceptor: AuthInterceptor): Retrofit {
|
||||
val client = OkHttpClient.Builder()
|
||||
.addInterceptor(authInterceptor)
|
||||
.addInterceptor(HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
})
|
||||
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
|
||||
.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
|
||||
.writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.addInterceptor(authInterceptor)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofit(okHttpClient: OkHttpClient, gson: Gson): Retrofit {
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.client(okHttpClient)
|
||||
.baseUrl(BuildConfig.API_BASE_URL)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.client(client)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package kr.smartsoltech.wellshe.domain.analytics
|
||||
|
||||
import kr.smartsoltech.wellshe.data.entity.SleepLogEntity
|
||||
|
||||
object SleepAnalytics {
|
||||
/**
|
||||
* Расчёт долга сна и недельного тренда
|
||||
*/
|
||||
fun sleepDebt(logs: List<SleepLogEntity>, targetHours: Int = 8): Int {
|
||||
val total = logs.sumOf { it.duration.toDouble() }
|
||||
val expected = logs.size * targetHours
|
||||
return (expected - total).toInt()
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,6 @@
|
||||
package kr.smartsoltech.wellshe.domain.model
|
||||
|
||||
data class AppSettings(
|
||||
val id: Long = 0,
|
||||
val isWaterReminderEnabled: Boolean = true,
|
||||
val waterReminderInterval: Int = 2, // часы
|
||||
val isCycleReminderEnabled: Boolean = true,
|
||||
val isSleepReminderEnabled: Boolean = true,
|
||||
val sleepReminderTime: String = "22:00",
|
||||
val wakeUpReminderTime: String = "07:00",
|
||||
val cycleLength: Int = 28,
|
||||
val periodLength: Int = 5,
|
||||
val waterGoal: Float = 2.5f,
|
||||
val stepsGoal: Int = 10000,
|
||||
val sleepGoal: Float = 8.0f,
|
||||
val isDarkTheme: Boolean = false,
|
||||
val language: String = "ru",
|
||||
val isFirstLaunch: Boolean = true
|
||||
val notificationsEnabled: Boolean = true,
|
||||
val darkModeEnabled: Boolean = false
|
||||
)
|
||||
|
||||
@@ -13,7 +13,6 @@ data class User(
|
||||
val dailyWaterGoal: Float = 2.5f, // в литрах
|
||||
val dailyStepsGoal: Int = 10000,
|
||||
val dailyCaloriesGoal: Int = 2000,
|
||||
val dailySleepGoal: Float = 8.0f, // в часах
|
||||
val cycleLength: Int = 28, // дней
|
||||
val periodLength: Int = 5, // дней
|
||||
val lastPeriodStart: LocalDate? = null,
|
||||
|
||||
@@ -83,13 +83,6 @@ fun DashboardScreen(
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SleepCard(
|
||||
sleepData = uiState.sleepData,
|
||||
onClick = { onNavigate("sleep") }
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
RecentWorkoutsCard(
|
||||
workouts = uiState.recentWorkouts,
|
||||
@@ -404,26 +397,26 @@ private fun HealthOverviewCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
HealthMetric(
|
||||
label = "Пульс",
|
||||
value = "${healthData.heartRate}",
|
||||
unit = "bpm",
|
||||
icon = Icons.Default.Favorite
|
||||
)
|
||||
|
||||
HealthMetric(
|
||||
label = "Настроение",
|
||||
value = getMoodEmoji(healthData.mood),
|
||||
unit = "",
|
||||
icon = Icons.Default.Mood
|
||||
)
|
||||
|
||||
HealthMetric(
|
||||
label = "Энергия",
|
||||
value = "${healthData.energyLevel}",
|
||||
unit = "/10",
|
||||
icon = Icons.Default.Battery6Bar
|
||||
)
|
||||
|
||||
HealthMetric(
|
||||
label = "Симптомы",
|
||||
value = "${healthData.symptoms.size}",
|
||||
unit = "",
|
||||
icon = Icons.Default.HealthAndSafety
|
||||
)
|
||||
|
||||
HealthMetric(
|
||||
label = "Заметки",
|
||||
value = if (healthData.notes.isNotEmpty()) "✓" else "—",
|
||||
unit = "",
|
||||
icon = Icons.Default.Notes
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -479,63 +472,6 @@ private fun HealthMetric(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SleepCard(
|
||||
sleepData: SleepData,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() },
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = NeutralWhite),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bedtime,
|
||||
contentDescription = null,
|
||||
tint = PrimaryPink,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = "Сон",
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "${sleepData.sleepDuration}ч • ${getSleepQualityText(sleepData.sleepQuality)}",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = TextSecondary
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChevronRight,
|
||||
contentDescription = null,
|
||||
tint = NeutralGray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecentWorkoutsCard(
|
||||
workouts: List<WorkoutData>,
|
||||
@@ -605,7 +541,7 @@ private fun WorkoutItem(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = getWorkoutIcon(workout.type),
|
||||
imageVector = Icons.Default.FitnessCenter,
|
||||
contentDescription = null,
|
||||
tint = PrimaryPink,
|
||||
modifier = Modifier.size(20.dp)
|
||||
@@ -617,7 +553,7 @@ private fun WorkoutItem(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = getWorkoutTypeText(workout.type),
|
||||
text = workout.name,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = TextPrimary
|
||||
@@ -662,12 +598,12 @@ private val quickActions = listOf(
|
||||
textColor = SecondaryBlue
|
||||
),
|
||||
QuickAction(
|
||||
title = "Отметить сон",
|
||||
icon = Icons.Default.Bedtime,
|
||||
route = "sleep",
|
||||
backgroundColor = AccentPurpleLight,
|
||||
iconColor = AccentPurple,
|
||||
textColor = AccentPurple
|
||||
title = "Экстренная помощь",
|
||||
icon = Icons.Default.Emergency,
|
||||
route = "emergency",
|
||||
backgroundColor = ErrorRedLight,
|
||||
iconColor = ErrorRed,
|
||||
textColor = ErrorRed
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -8,19 +8,14 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.launch
|
||||
import kr.smartsoltech.wellshe.data.entity.CyclePeriodEntity
|
||||
import kr.smartsoltech.wellshe.data.entity.SleepLogEntity
|
||||
import kr.smartsoltech.wellshe.data.entity.HealthRecordEntity
|
||||
import kr.smartsoltech.wellshe.data.repository.WellSheRepository
|
||||
import kr.smartsoltech.wellshe.domain.model.*
|
||||
import javax.inject.Inject
|
||||
import java.time.LocalDate
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
data class DashboardUiState(
|
||||
val user: User = User(),
|
||||
val todayHealth: HealthData = HealthData(),
|
||||
val sleepData: SleepData = SleepData(),
|
||||
val cycleData: CycleData = CycleData(),
|
||||
val recentWorkouts: List<WorkoutData> = emptyList(),
|
||||
val todaySteps: Int = 0,
|
||||
@@ -53,37 +48,13 @@ class DashboardViewModel @Inject constructor(
|
||||
_uiState.value = _uiState.value.copy(user = user)
|
||||
}
|
||||
|
||||
// Загружаем данные о здоровье
|
||||
repository.getTodayHealthData().catch {
|
||||
// Игнорируем ошибки, используем дефолтные данные
|
||||
}.collect { healthEntity: HealthRecordEntity? ->
|
||||
val healthData = healthEntity?.let { convertHealthEntityToModel(it) } ?: HealthData()
|
||||
_uiState.value = _uiState.value.copy(todayHealth = healthData)
|
||||
}
|
||||
// TODO: Временно используем заглушки для данных о здоровье
|
||||
val healthData = HealthData()
|
||||
_uiState.value = _uiState.value.copy(todayHealth = healthData)
|
||||
|
||||
// Загружаем данные о сне
|
||||
loadSleepData()
|
||||
|
||||
// Загружаем данные о цикле
|
||||
repository.getRecentPeriods().let { periods ->
|
||||
val cycleEntity = periods.firstOrNull()
|
||||
val cycleData = cycleEntity?.let { convertCycleEntityToModel(it) } ?: CycleData()
|
||||
_uiState.value = _uiState.value.copy(cycleData = cycleData)
|
||||
}
|
||||
|
||||
// Загружаем тренировки
|
||||
repository.getRecentWorkouts().catch {
|
||||
// Игнорируем ошибки
|
||||
}.collect { workoutEntities: List<WorkoutSession> ->
|
||||
val workouts = workoutEntities.map { convertWorkoutEntityToModel(it) }
|
||||
_uiState.value = _uiState.value.copy(recentWorkouts = workouts)
|
||||
}
|
||||
|
||||
// Загружаем шаги за сегодня
|
||||
loadTodayFitnessData()
|
||||
|
||||
// Загружаем воду за сегодня
|
||||
loadTodayWaterData()
|
||||
// TODO: Временно используем заглушки для данных о цикле
|
||||
val cycleData = CycleData()
|
||||
_uiState.value = _uiState.value.copy(cycleData = cycleData)
|
||||
|
||||
_uiState.value = _uiState.value.copy(isLoading = false)
|
||||
|
||||
@@ -96,136 +67,28 @@ class DashboardViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadSleepData() {
|
||||
try {
|
||||
val yesterday = LocalDate.now().minusDays(1)
|
||||
val sleepEntity = repository.getSleepForDate(yesterday)
|
||||
val sleepData = sleepEntity?.let { convertSleepEntityToModel(it) } ?: SleepData()
|
||||
_uiState.value = _uiState.value.copy(sleepData = sleepData)
|
||||
} catch (_: Exception) {
|
||||
// Игнорируем ошибки загрузки сна
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadTodayFitnessData() {
|
||||
try {
|
||||
val today = LocalDate.now()
|
||||
repository.getFitnessDataForDate(today).catch {
|
||||
// Игнорируем ошибки
|
||||
}.collect { fitnessData: FitnessData ->
|
||||
_uiState.value = _uiState.value.copy(todaySteps = fitnessData.steps)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// Игнорируем ошибки загрузки фитнеса
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadTodayWaterData() {
|
||||
try {
|
||||
val today = LocalDate.now()
|
||||
repository.getWaterIntakeForDate(today).catch {
|
||||
// Игнорируем ошибки
|
||||
}.collect { waterIntakes: List<WaterIntake> ->
|
||||
val totalAmount = waterIntakes.sumOf { it.amount.toDouble() }.toFloat()
|
||||
_uiState.value = _uiState.value.copy(todayWater = totalAmount)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// Игнорируем ошибки загрузки воды
|
||||
}
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
_uiState.value = _uiState.value.copy(error = null)
|
||||
}
|
||||
|
||||
// Функции преобразования Entity -> Model
|
||||
private fun convertHealthEntityToModel(entity: HealthRecordEntity): HealthData {
|
||||
return HealthData(
|
||||
id = entity.id.toString(),
|
||||
userId = "current_user",
|
||||
date = entity.date,
|
||||
weight = entity.weight ?: 0f,
|
||||
heartRate = entity.heartRate ?: 70,
|
||||
bloodPressureSystolic = entity.bloodPressureS ?: 120,
|
||||
bloodPressureDiastolic = entity.bloodPressureD ?: 80,
|
||||
mood = convertMoodStringToEnum(entity.mood ?: "neutral"),
|
||||
energyLevel = entity.energyLevel ?: 5,
|
||||
stressLevel = entity.stressLevel ?: 5,
|
||||
symptoms = entity.symptoms ?: emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertSleepEntityToModel(entity: SleepLogEntity): SleepData {
|
||||
return SleepData(
|
||||
id = entity.id.toString(),
|
||||
userId = "current_user",
|
||||
date = entity.date,
|
||||
bedTime = java.time.LocalTime.parse(entity.bedTime),
|
||||
wakeTime = java.time.LocalTime.parse(entity.wakeTime),
|
||||
sleepDuration = entity.duration,
|
||||
sleepQuality = convertSleepQualityStringToEnum(entity.quality)
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertCycleEntityToModel(entity: CyclePeriodEntity): CycleData {
|
||||
return CycleData(
|
||||
id = entity.id.toString(),
|
||||
userId = "current_user",
|
||||
cycleLength = entity.cycleLength ?: 28,
|
||||
periodLength = entity.endDate?.let {
|
||||
ChronoUnit.DAYS.between(entity.startDate, it).toInt() + 1
|
||||
} ?: 5,
|
||||
lastPeriodDate = entity.startDate,
|
||||
nextPeriodDate = entity.startDate.plusDays((entity.cycleLength ?: 28).toLong()),
|
||||
ovulationDate = entity.startDate.plusDays(((entity.cycleLength ?: 28) / 2).toLong())
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertWorkoutEntityToModel(entity: kr.smartsoltech.wellshe.domain.model.WorkoutSession): WorkoutData {
|
||||
return WorkoutData(
|
||||
id = entity.id.toString(),
|
||||
userId = "current_user",
|
||||
date = entity.date,
|
||||
type = convertWorkoutTypeStringToEnum(entity.type),
|
||||
duration = entity.duration,
|
||||
intensity = WorkoutIntensity.MODERATE, // По умолчанию, так как в WorkoutSession нет intensity
|
||||
caloriesBurned = entity.caloriesBurned
|
||||
)
|
||||
}
|
||||
|
||||
// Вспомогательные функции преобразования
|
||||
private fun convertMoodStringToEnum(mood: String): Mood {
|
||||
return when (mood.lowercase()) {
|
||||
"very_sad" -> Mood.VERY_SAD
|
||||
"sad" -> Mood.SAD
|
||||
"neutral" -> Mood.NEUTRAL
|
||||
"happy" -> Mood.HAPPY
|
||||
"very_happy" -> Mood.VERY_HAPPY
|
||||
else -> Mood.NEUTRAL
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertSleepQualityStringToEnum(quality: String): SleepQuality {
|
||||
return when (quality.lowercase()) {
|
||||
"poor" -> SleepQuality.POOR
|
||||
"fair" -> SleepQuality.FAIR
|
||||
"good" -> SleepQuality.GOOD
|
||||
"excellent" -> SleepQuality.EXCELLENT
|
||||
else -> SleepQuality.GOOD
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertWorkoutTypeStringToEnum(type: String): WorkoutType {
|
||||
return when (type.lowercase()) {
|
||||
"кардио", "cardio" -> WorkoutType.CARDIO
|
||||
"силовая", "strength" -> WorkoutType.STRENGTH
|
||||
"йога", "yoga" -> WorkoutType.YOGA
|
||||
"пилатес", "pilates" -> WorkoutType.PILATES
|
||||
"бег", "running" -> WorkoutType.RUNNING
|
||||
"ходьба", "walking" -> WorkoutType.WALKING
|
||||
"велосипед", "cycling" -> WorkoutType.CYCLING
|
||||
"плавание", "swimming" -> WorkoutType.SWIMMING
|
||||
else -> WorkoutType.CARDIO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Упрощенные модели данных для Dashboard
|
||||
data class HealthData(
|
||||
val energyLevel: Int = 5,
|
||||
val symptoms: List<String> = emptyList(),
|
||||
val notes: String = ""
|
||||
)
|
||||
|
||||
data class CycleData(
|
||||
val currentDay: Int = 1,
|
||||
val nextPeriodDate: LocalDate? = null,
|
||||
val cycleLength: Int = 28
|
||||
)
|
||||
|
||||
data class WorkoutData(
|
||||
val id: Long = 0,
|
||||
val name: String = "",
|
||||
val duration: Int = 0,
|
||||
val caloriesBurned: Int = 0,
|
||||
val date: LocalDate = LocalDate.now()
|
||||
)
|
||||
|
||||
@@ -93,9 +93,7 @@ fun HealthOverviewScreen(
|
||||
TodayHealthCard(
|
||||
uiState = uiState,
|
||||
onUpdateVitals = viewModel::updateVitals,
|
||||
onUpdateMood = viewModel::updateMood,
|
||||
onUpdateEnergy = viewModel::updateEnergyLevel,
|
||||
onUpdateStress = viewModel::updateStressLevel
|
||||
onUpdateEnergy = viewModel::updateEnergyLevel
|
||||
)
|
||||
}
|
||||
|
||||
@@ -133,9 +131,7 @@ fun HealthOverviewScreen(
|
||||
private fun TodayHealthCard(
|
||||
uiState: HealthUiState,
|
||||
onUpdateVitals: (Float?, Int?, Int?, Int?, Float?) -> Unit,
|
||||
onUpdateMood: (String) -> Unit,
|
||||
onUpdateEnergy: (Int) -> Unit,
|
||||
onUpdateStress: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var weight by remember { mutableStateOf(uiState.todayRecord?.weight?.toString() ?: "") }
|
||||
@@ -269,16 +265,7 @@ private fun TodayHealthCard(
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Настроение
|
||||
MoodSection(
|
||||
currentMood = uiState.todayRecord?.mood ?: "neutral",
|
||||
onMoodChange = onUpdateMood,
|
||||
isEditMode = uiState.isEditMode
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Уровень энергии и стресса
|
||||
// Уровень энергии
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
@@ -288,18 +275,9 @@ private fun TodayHealthCard(
|
||||
value = uiState.todayRecord?.energyLevel ?: 5,
|
||||
onValueChange = onUpdateEnergy,
|
||||
isEditMode = uiState.isEditMode,
|
||||
modifier = Modifier.weight(1f),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = WarningOrange
|
||||
)
|
||||
|
||||
LevelSlider(
|
||||
label = "Стресс",
|
||||
value = uiState.todayRecord?.stressLevel ?: 5,
|
||||
onValueChange = onUpdateStress,
|
||||
isEditMode = uiState.isEditMode,
|
||||
modifier = Modifier.weight(1f),
|
||||
color = ErrorRed
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,68 +330,6 @@ private fun VitalMetric(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MoodSection(
|
||||
currentMood: String,
|
||||
onMoodChange: (String) -> Unit,
|
||||
isEditMode: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Text(
|
||||
text = "Настроение",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
if (isEditMode) {
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(healthMoods) { mood ->
|
||||
FilterChip(
|
||||
onClick = { onMoodChange(mood.key) },
|
||||
label = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(mood.emoji)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(mood.name)
|
||||
}
|
||||
},
|
||||
selected = currentMood == mood.key,
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
selectedContainerColor = SuccessGreenLight,
|
||||
selectedLabelColor = SuccessGreen
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val currentMoodData = healthMoods.find { it.key == currentMood } ?: healthMoods[2]
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = currentMoodData.emoji,
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = currentMoodData.name,
|
||||
style = MaterialTheme.typography.bodyLarge.copy(
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LevelSlider(
|
||||
label: String,
|
||||
@@ -679,18 +595,8 @@ private fun NotesCard(
|
||||
}
|
||||
|
||||
// Данные для UI
|
||||
private data class HealthMoodData(val key: String, val name: String, val emoji: String)
|
||||
|
||||
private val healthMoods = listOf(
|
||||
HealthMoodData("very_sad", "Очень плохо", "😢"),
|
||||
HealthMoodData("sad", "Плохо", "😔"),
|
||||
HealthMoodData("neutral", "Нормально", "😐"),
|
||||
HealthMoodData("happy", "Хорошо", "😊"),
|
||||
HealthMoodData("very_happy", "Отлично", "😄")
|
||||
)
|
||||
|
||||
private val healthSymptoms = listOf(
|
||||
"Головная боль", "Усталость", "Тошнота", "Головокружение",
|
||||
"Боль в спине", "Боль в суставах", "Бессонница", "Стресс",
|
||||
"Боль в спине", "Боль в суставах", "Бессонница",
|
||||
"Простуда", "Аллергия", "Боль в животе", "Другое"
|
||||
)
|
||||
|
||||
@@ -248,9 +248,7 @@ private fun VitalSignsCard(
|
||||
bloodPressureS = 0,
|
||||
bloodPressureD = 0,
|
||||
temperature = 36.6f,
|
||||
mood = "",
|
||||
energyLevel = 5,
|
||||
stressLevel = 5,
|
||||
symptoms = emptyList(),
|
||||
notes = ""
|
||||
)
|
||||
@@ -275,9 +273,7 @@ private fun VitalSignsCard(
|
||||
bloodPressureS = 0,
|
||||
bloodPressureD = 0,
|
||||
temperature = 36.6f,
|
||||
mood = "",
|
||||
energyLevel = 5,
|
||||
stressLevel = 5,
|
||||
symptoms = emptyList(),
|
||||
notes = ""
|
||||
)
|
||||
@@ -305,9 +301,7 @@ private fun VitalSignsCard(
|
||||
bloodPressureS = 0,
|
||||
bloodPressureD = 0,
|
||||
temperature = 36.6f,
|
||||
mood = "",
|
||||
energyLevel = 5,
|
||||
stressLevel = 5,
|
||||
symptoms = emptyList(),
|
||||
notes = ""
|
||||
)
|
||||
@@ -333,9 +327,7 @@ private fun VitalSignsCard(
|
||||
bloodPressureS = 0,
|
||||
bloodPressureD = 0,
|
||||
temperature = 36.6f,
|
||||
mood = "",
|
||||
energyLevel = 5,
|
||||
stressLevel = 5,
|
||||
symptoms = emptyList(),
|
||||
notes = ""
|
||||
)
|
||||
|
||||
@@ -37,30 +37,41 @@ class HealthViewModel @Inject constructor(
|
||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||
|
||||
try {
|
||||
// TODO: Временно используем заглушки, пока не добавим методы в repository
|
||||
_uiState.value = _uiState.value.copy(
|
||||
todayRecord = null,
|
||||
lastUpdateDate = null,
|
||||
todaySymptoms = emptyList(),
|
||||
todayNotes = "",
|
||||
recentRecords = emptyList(),
|
||||
weeklyWeights = emptyMap(),
|
||||
isLoading = false
|
||||
)
|
||||
|
||||
// Загружаем данные о здоровье за сегодня
|
||||
repository.getTodayHealthData().collect { todayRecord: HealthRecordEntity? ->
|
||||
_uiState.value = _uiState.value.copy(
|
||||
todayRecord = todayRecord,
|
||||
lastUpdateDate = todayRecord?.date,
|
||||
todaySymptoms = todayRecord?.symptoms ?: emptyList(),
|
||||
todayNotes = todayRecord?.notes ?: "",
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
// repository.getTodayHealthData().collect { todayRecord: HealthRecordEntity? ->
|
||||
// _uiState.value = _uiState.value.copy(
|
||||
// todayRecord = todayRecord,
|
||||
// lastUpdateDate = todayRecord?.date,
|
||||
// todaySymptoms = todayRecord?.symptoms ?: emptyList(),
|
||||
// todayNotes = todayRecord?.notes ?: "",
|
||||
// isLoading = false
|
||||
// )
|
||||
// }
|
||||
|
||||
// Загружаем недельные данные веса
|
||||
repository.getAllHealthRecords().collect { records: List<HealthRecordEntity> ->
|
||||
val weightsMap = records
|
||||
.filter { it.weight != null && it.weight > 0f }
|
||||
.groupBy { it.date }
|
||||
.mapValues { entry -> entry.value.last().weight ?: 0f }
|
||||
_uiState.value = _uiState.value.copy(weeklyWeights = weightsMap)
|
||||
}
|
||||
// repository.getAllHealthRecords().collect { records: List<HealthRecordEntity> ->
|
||||
// val weightsMap = records
|
||||
// .filter { it.weight != null && it.weight > 0f }
|
||||
// .groupBy { it.date }
|
||||
// .mapValues { entry -> entry.value.last().weight ?: 0f }
|
||||
// _uiState.value = _uiState.value.copy(weeklyWeights = weightsMap)
|
||||
// }
|
||||
|
||||
// Загружаем последние записи
|
||||
repository.getRecentHealthRecords().collect { records: List<HealthRecordEntity> ->
|
||||
_uiState.value = _uiState.value.copy(recentRecords = records)
|
||||
}
|
||||
// repository.getRecentHealthRecords().collect { records: List<HealthRecordEntity> ->
|
||||
// _uiState.value = _uiState.value.copy(recentRecords = records)
|
||||
// }
|
||||
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
@@ -91,42 +102,13 @@ class HealthViewModel @Inject constructor(
|
||||
bloodPressureS = bpSystolic,
|
||||
bloodPressureD = bpDiastolic,
|
||||
temperature = temperature,
|
||||
mood = "",
|
||||
energyLevel = 5,
|
||||
stressLevel = 5,
|
||||
symptoms = emptyList(),
|
||||
notes = ""
|
||||
)
|
||||
}
|
||||
repository.saveHealthRecord(updatedRecord)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMood(mood: String) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val currentRecord = _uiState.value.todayRecord
|
||||
val updatedRecord = if (currentRecord != null) {
|
||||
currentRecord.copy(mood = mood)
|
||||
} else {
|
||||
HealthRecordEntity(
|
||||
date = LocalDate.now(),
|
||||
weight = 0f,
|
||||
heartRate = 0,
|
||||
bloodPressureS = 0,
|
||||
bloodPressureD = 0,
|
||||
temperature = 36.6f,
|
||||
mood = mood,
|
||||
energyLevel = 5,
|
||||
stressLevel = 5,
|
||||
symptoms = emptyList(),
|
||||
notes = ""
|
||||
)
|
||||
}
|
||||
repository.saveHealthRecord(updatedRecord)
|
||||
// TODO: Добавить метод saveHealthRecord в repository
|
||||
// repository.saveHealthRecord(updatedRecord)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
}
|
||||
@@ -142,47 +124,18 @@ class HealthViewModel @Inject constructor(
|
||||
} else {
|
||||
HealthRecordEntity(
|
||||
date = LocalDate.now(),
|
||||
weight = 0f,
|
||||
heartRate = 0,
|
||||
bloodPressureS = 0,
|
||||
bloodPressureD = 0,
|
||||
temperature = 36.6f,
|
||||
mood = "",
|
||||
weight = null,
|
||||
heartRate = null,
|
||||
bloodPressureS = null,
|
||||
bloodPressureD = null,
|
||||
temperature = null,
|
||||
energyLevel = energy,
|
||||
stressLevel = 5,
|
||||
symptoms = emptyList(),
|
||||
notes = ""
|
||||
)
|
||||
}
|
||||
repository.saveHealthRecord(updatedRecord)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateStressLevel(stress: Int) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val currentRecord = _uiState.value.todayRecord
|
||||
val updatedRecord = if (currentRecord != null) {
|
||||
currentRecord.copy(stressLevel = stress)
|
||||
} else {
|
||||
HealthRecordEntity(
|
||||
date = LocalDate.now(),
|
||||
weight = 0f,
|
||||
heartRate = 0,
|
||||
bloodPressureS = 0,
|
||||
bloodPressureD = 0,
|
||||
temperature = 36.6f,
|
||||
mood = "",
|
||||
energyLevel = 5,
|
||||
stressLevel = stress,
|
||||
symptoms = emptyList(),
|
||||
notes = ""
|
||||
)
|
||||
}
|
||||
repository.saveHealthRecord(updatedRecord)
|
||||
// TODO: Добавить метод saveHealthRecord в repository
|
||||
// repository.saveHealthRecord(updatedRecord)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
}
|
||||
@@ -199,19 +152,18 @@ class HealthViewModel @Inject constructor(
|
||||
} else {
|
||||
HealthRecordEntity(
|
||||
date = LocalDate.now(),
|
||||
weight = 0f,
|
||||
heartRate = 0,
|
||||
bloodPressureS = 0,
|
||||
bloodPressureD = 0,
|
||||
temperature = 36.6f,
|
||||
mood = "",
|
||||
weight = null,
|
||||
heartRate = null,
|
||||
bloodPressureS = null,
|
||||
bloodPressureD = null,
|
||||
temperature = null,
|
||||
energyLevel = 5,
|
||||
stressLevel = 5,
|
||||
symptoms = symptoms,
|
||||
notes = ""
|
||||
)
|
||||
}
|
||||
repository.saveHealthRecord(updatedRecord)
|
||||
// TODO: Добавить метод saveHealthRecord в repository
|
||||
// repository.saveHealthRecord(updatedRecord)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
}
|
||||
@@ -228,19 +180,18 @@ class HealthViewModel @Inject constructor(
|
||||
} else {
|
||||
HealthRecordEntity(
|
||||
date = LocalDate.now(),
|
||||
weight = 0f,
|
||||
heartRate = 0,
|
||||
bloodPressureS = 0,
|
||||
bloodPressureD = 0,
|
||||
temperature = 36.6f,
|
||||
mood = "",
|
||||
weight = null,
|
||||
heartRate = null,
|
||||
bloodPressureS = null,
|
||||
bloodPressureD = null,
|
||||
temperature = null,
|
||||
energyLevel = 5,
|
||||
stressLevel = 5,
|
||||
symptoms = emptyList(),
|
||||
notes = notes
|
||||
)
|
||||
}
|
||||
repository.saveHealthRecord(updatedRecord)
|
||||
// TODO: Добавить метод saveHealthRecord в repository
|
||||
// repository.saveHealthRecord(updatedRecord)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
}
|
||||
@@ -250,7 +201,8 @@ class HealthViewModel @Inject constructor(
|
||||
fun deleteHealthRecord(record: HealthRecordEntity) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
repository.deleteHealthRecord(record.id)
|
||||
// TODO: Добавить метод deleteHealthRecord в repository
|
||||
// repository.deleteHealthRecord(record.id)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
}
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
package kr.smartsoltech.wellshe.ui.mood
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.ModeNight
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kr.smartsoltech.wellshe.ui.components.InfoCard
|
||||
import kr.smartsoltech.wellshe.ui.components.StatCard
|
||||
import kr.smartsoltech.wellshe.ui.theme.MoodTabColor
|
||||
import kr.smartsoltech.wellshe.ui.theme.WellSheTheme
|
||||
|
||||
/**
|
||||
* Экран "Настроение" для отслеживания сна и эмоционального состояния
|
||||
*/
|
||||
@Composable
|
||||
fun MoodScreen(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.verticalScroll(scrollState),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Статистические карточки
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
StatCard(
|
||||
title = "Сон",
|
||||
value = "7.2 ч",
|
||||
tone = Color(0xFF673AB7), // Фиолетовый для сна
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
StatCard(
|
||||
title = "Стресс",
|
||||
value = "3/10",
|
||||
tone = Color(0xFFE91E63), // Розовый для стресса
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
// Карточка дневника
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MoodTabColor.copy(alpha = 0.3f)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Заголовок
|
||||
Text(
|
||||
text = "Дневник",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
// Содержимое дневника
|
||||
Text(
|
||||
text = "Сегодня было продуктивно, немного тревоги перед встречей. Выполнила все запланированные задачи, чувствую удовлетворение от проделанной работы.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
// Кнопки действий
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = { /* TODO */ }) {
|
||||
Text("Редактировать")
|
||||
}
|
||||
|
||||
TextButton(onClick = { /* TODO */ }) {
|
||||
Text("Добавить запись")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Карточка сна
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Заголовок с иконкой
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ModeNight,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF673AB7)
|
||||
)
|
||||
Text(
|
||||
text = "Качество сна",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
// Оценка сна
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text("Продолжительность")
|
||||
Text("7.2 часа", fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text("Качество")
|
||||
Text("Хорошее", fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text("Пробуждения")
|
||||
Text("1 раз", fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
}
|
||||
|
||||
// Кнопка добавления записи
|
||||
OutlinedButton(
|
||||
onClick = { /* TODO */ },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Записать сон")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Карточка эмоций
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Заголовок с иконкой
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Favorite,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFFE91E63)
|
||||
)
|
||||
Text(
|
||||
text = "Эмоциональное состояние",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
// Текущее настроение
|
||||
Text(
|
||||
text = "Текущее настроение: Спокойствие, удовлетворение",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
// Кнопки эмоций
|
||||
EmojiButtonsRow()
|
||||
}
|
||||
}
|
||||
|
||||
// Карточка рекомендаций
|
||||
InfoCard(
|
||||
title = "Рекомендации",
|
||||
content = "Стабильный сон и низкий уровень стресса положительно влияют на ваш цикл. Рекомендуется поддерживать текущий режим для гормонального баланса."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Строка кнопок с эмодзи для выбора эмоций
|
||||
*/
|
||||
@Composable
|
||||
fun EmojiButtonsRow() {
|
||||
val emojis = listOf("😊", "😌", "🙂", "😐", "😔", "😢", "😡")
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
emojis.forEach { emoji ->
|
||||
OutlinedButton(
|
||||
onClick = { /* TODO */ },
|
||||
contentPadding = PaddingValues(12.dp),
|
||||
modifier = Modifier.size(44.dp),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = emoji,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun MoodScreenPreview() {
|
||||
WellSheTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
MoodScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package kr.smartsoltech.wellshe.ui.mood
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MoodViewModel @Inject constructor() : ViewModel() {
|
||||
// Данные для экрана настроения
|
||||
private val _sleepHours = MutableStateFlow(7.2f)
|
||||
val sleepHours: StateFlow<Float> = _sleepHours.asStateFlow()
|
||||
|
||||
private val _stressLevel = MutableStateFlow(3)
|
||||
val stressLevel: StateFlow<Int> = _stressLevel.asStateFlow()
|
||||
|
||||
private val _journalEntry = MutableStateFlow("Сегодня было продуктивно, немного тревоги перед встречей.")
|
||||
val journalEntry: StateFlow<String> = _journalEntry.asStateFlow()
|
||||
|
||||
fun updateSleepHours(hours: Float) {
|
||||
_sleepHours.value = hours
|
||||
}
|
||||
|
||||
fun updateStressLevel(level: Int) {
|
||||
_stressLevel.value = level
|
||||
}
|
||||
|
||||
fun updateJournalEntry(entry: String) {
|
||||
_journalEntry.value = entry
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,11 @@ import androidx.navigation.compose.composable
|
||||
import kr.smartsoltech.wellshe.ui.analytics.AnalyticsScreen
|
||||
import kr.smartsoltech.wellshe.ui.body.BodyScreen
|
||||
import kr.smartsoltech.wellshe.ui.cycle.CycleScreen
|
||||
import kr.smartsoltech.wellshe.ui.mood.MoodScreen
|
||||
import kr.smartsoltech.wellshe.ui.emergency.EmergencyScreen
|
||||
import kr.smartsoltech.wellshe.ui.profile.ProfileScreen
|
||||
import kr.smartsoltech.wellshe.ui.auth.AuthViewModel
|
||||
import kr.smartsoltech.wellshe.ui.auth.compose.LoginScreen
|
||||
import kr.smartsoltech.wellshe.ui.auth.compose.RegisterScreen
|
||||
import kr.smartsoltech.wellshe.ui.emergency.EmergencyScreen
|
||||
|
||||
@Composable
|
||||
fun AppNavGraph(
|
||||
@@ -57,15 +56,6 @@ fun AppNavGraph(
|
||||
)
|
||||
}
|
||||
|
||||
// Экран экстренной помощи
|
||||
composable("emergency") {
|
||||
EmergencyScreen(
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Существующие экраны
|
||||
composable(BottomNavItem.Cycle.route) {
|
||||
CycleScreen(
|
||||
@@ -91,8 +81,12 @@ fun AppNavGraph(
|
||||
BodyScreen()
|
||||
}
|
||||
|
||||
composable(BottomNavItem.Mood.route) {
|
||||
MoodScreen()
|
||||
composable(BottomNavItem.Emergency.route) {
|
||||
EmergencyScreen(
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(BottomNavItem.Analytics.route) {
|
||||
|
||||
@@ -2,14 +2,14 @@ package kr.smartsoltech.wellshe.ui.navigation
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BarChart
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.Emergency
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.WaterDrop
|
||||
import androidx.compose.material.icons.filled.WbSunny
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
/**
|
||||
* Модель навигационного элемента для нижней панели навигац<EFBFBD><EFBFBD>и
|
||||
* Модель навигационного элемента для нижней панели навигации
|
||||
*/
|
||||
sealed class BottomNavItem(
|
||||
val route: String,
|
||||
@@ -28,10 +28,10 @@ sealed class BottomNavItem(
|
||||
icon = Icons.Default.WaterDrop
|
||||
)
|
||||
|
||||
data object Mood : BottomNavItem(
|
||||
route = "mood",
|
||||
title = "Настроение",
|
||||
icon = Icons.Default.Favorite
|
||||
data object Emergency : BottomNavItem(
|
||||
route = "emergency",
|
||||
title = "Экстренное",
|
||||
icon = Icons.Default.Emergency
|
||||
)
|
||||
|
||||
data object Analytics : BottomNavItem(
|
||||
@@ -47,6 +47,6 @@ sealed class BottomNavItem(
|
||||
)
|
||||
|
||||
companion object {
|
||||
val items = listOf(Cycle, Body, Mood, Analytics, Profile)
|
||||
val items = listOf(Cycle, Body, Emergency, Analytics, Profile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ fun BottomNavigation(
|
||||
val backgroundColor = when (item) {
|
||||
BottomNavItem.Cycle -> CycleTabColor
|
||||
BottomNavItem.Body -> BodyTabColor
|
||||
BottomNavItem.Mood -> MoodTabColor
|
||||
BottomNavItem.Emergency -> ErrorRed
|
||||
BottomNavItem.Analytics -> AnalyticsTabColor
|
||||
BottomNavItem.Profile -> ProfileTabColor
|
||||
}
|
||||
|
||||
@@ -59,10 +59,8 @@ fun SettingsScreen(
|
||||
NotificationSettingsCard(
|
||||
isWaterReminderEnabled = uiState.isWaterReminderEnabled,
|
||||
isCycleReminderEnabled = uiState.isCycleReminderEnabled,
|
||||
isSleepReminderEnabled = uiState.isSleepReminderEnabled,
|
||||
onWaterReminderToggle = viewModel::toggleWaterReminder,
|
||||
onCycleReminderToggle = viewModel::toggleCycleReminder,
|
||||
onSleepReminderToggle = viewModel::toggleSleepReminder
|
||||
onWaterReminderToggle = viewModel::updateWaterReminder,
|
||||
onCycleReminderToggle = viewModel::updateCycleReminder
|
||||
)
|
||||
}
|
||||
|
||||
@@ -79,24 +77,22 @@ fun SettingsScreen(
|
||||
GoalsSettingsCard(
|
||||
waterGoal = uiState.waterGoal,
|
||||
stepsGoal = uiState.stepsGoal,
|
||||
sleepGoal = uiState.sleepGoal,
|
||||
onWaterGoalChange = viewModel::updateWaterGoal,
|
||||
onStepsGoalChange = viewModel::updateStepsGoal,
|
||||
onSleepGoalChange = viewModel::updateSleepGoal
|
||||
onStepsGoalChange = viewModel::updateStepsGoal
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
AppearanceSettingsCard(
|
||||
isDarkTheme = uiState.isDarkTheme,
|
||||
onThemeToggle = viewModel::toggleTheme
|
||||
onThemeToggle = viewModel::updateTheme
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
DataManagementCard(
|
||||
onExportData = viewModel::exportData,
|
||||
onImportData = viewModel::importData,
|
||||
onImportData = { viewModel.importData(it) },
|
||||
onClearData = viewModel::clearAllData
|
||||
)
|
||||
}
|
||||
@@ -155,10 +151,8 @@ private fun SettingsHeader(
|
||||
private fun NotificationSettingsCard(
|
||||
isWaterReminderEnabled: Boolean,
|
||||
isCycleReminderEnabled: Boolean,
|
||||
isSleepReminderEnabled: Boolean,
|
||||
onWaterReminderToggle: (Boolean) -> Unit,
|
||||
onCycleReminderToggle: (Boolean) -> Unit,
|
||||
onSleepReminderToggle: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
SettingsCard(
|
||||
@@ -181,15 +175,6 @@ private fun NotificationSettingsCard(
|
||||
isChecked = isCycleReminderEnabled,
|
||||
onCheckedChange = onCycleReminderToggle
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
SettingsSwitchItem(
|
||||
title = "Напоминания о сне",
|
||||
subtitle = "Уведомления о режиме сна",
|
||||
isChecked = isSleepReminderEnabled,
|
||||
onCheckedChange = onSleepReminderToggle
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,10 +219,8 @@ private fun CycleSettingsCard(
|
||||
private fun GoalsSettingsCard(
|
||||
waterGoal: Float,
|
||||
stepsGoal: Int,
|
||||
sleepGoal: Float,
|
||||
onWaterGoalChange: (Float) -> Unit,
|
||||
onStepsGoalChange: (Int) -> Unit,
|
||||
onSleepGoalChange: (Float) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
SettingsCard(
|
||||
@@ -266,18 +249,6 @@ private fun GoalsSettingsCard(
|
||||
},
|
||||
suffix = "шагов"
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
SettingsDecimalField(
|
||||
title = "Цель по сну",
|
||||
subtitle = "Количество часов сна (6-10 часов)",
|
||||
value = sleepGoal,
|
||||
onValueChange = { value ->
|
||||
if (value in 6.0f..10.0f) onSleepGoalChange(value)
|
||||
},
|
||||
suffix = "часов"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +275,7 @@ private fun AppearanceSettingsCard(
|
||||
@Composable
|
||||
private fun DataManagementCard(
|
||||
onExportData: () -> Unit,
|
||||
onImportData: () -> Unit,
|
||||
onImportData: (String) -> Unit,
|
||||
onClearData: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -326,7 +297,7 @@ private fun DataManagementCard(
|
||||
title = "Импорт данных",
|
||||
subtitle = "Загрузить данные из файла",
|
||||
icon = Icons.Default.Upload,
|
||||
onClick = onImportData
|
||||
onClick = { onImportData("") }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
@@ -14,12 +14,10 @@ import javax.inject.Inject
|
||||
data class SettingsUiState(
|
||||
val isWaterReminderEnabled: Boolean = true,
|
||||
val isCycleReminderEnabled: Boolean = true,
|
||||
val isSleepReminderEnabled: Boolean = true,
|
||||
val cycleLength: Int = 28,
|
||||
val periodLength: Int = 5,
|
||||
val waterGoal: Float = 2.5f,
|
||||
val stepsGoal: Int = 10000,
|
||||
val sleepGoal: Float = 8.0f,
|
||||
val isDarkTheme: Boolean = false,
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null
|
||||
@@ -38,23 +36,17 @@ class SettingsViewModel @Inject constructor(
|
||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||
|
||||
try {
|
||||
repository.getSettings().catch { e ->
|
||||
// TODO: Временно используем заглушки до реализации методов в repository
|
||||
repository.getAppSettings().catch { e ->
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
error = e.message
|
||||
)
|
||||
}.collect { settings ->
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isWaterReminderEnabled = settings.isWaterReminderEnabled,
|
||||
isCycleReminderEnabled = settings.isCycleReminderEnabled,
|
||||
isSleepReminderEnabled = settings.isSleepReminderEnabled,
|
||||
cycleLength = settings.cycleLength,
|
||||
periodLength = settings.periodLength,
|
||||
waterGoal = settings.waterGoal,
|
||||
stepsGoal = settings.stepsGoal,
|
||||
sleepGoal = settings.sleepGoal,
|
||||
isDarkTheme = settings.isDarkTheme,
|
||||
isLoading = false
|
||||
isDarkTheme = settings.darkModeEnabled,
|
||||
isLoading = false,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -66,11 +58,11 @@ class SettingsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
// Уведомления
|
||||
fun toggleWaterReminder(enabled: Boolean) {
|
||||
// Обновление настроек уведомлений
|
||||
fun updateWaterReminder(enabled: Boolean) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
repository.updateWaterReminderSetting(enabled)
|
||||
// TODO: Реализовать через repository
|
||||
_uiState.value = _uiState.value.copy(isWaterReminderEnabled = enabled)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
@@ -78,10 +70,10 @@ class SettingsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleCycleReminder(enabled: Boolean) {
|
||||
fun updateCycleReminder(enabled: Boolean) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
repository.updateCycleReminderSetting(enabled)
|
||||
// TODO: Реализовать через repository
|
||||
_uiState.value = _uiState.value.copy(isCycleReminderEnabled = enabled)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
@@ -89,23 +81,12 @@ class SettingsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleSleepReminder(enabled: Boolean) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
repository.updateSleepReminderSetting(enabled)
|
||||
_uiState.value = _uiState.value.copy(isSleepReminderEnabled = enabled)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Настройки цикла
|
||||
// Обновление параметров цикла
|
||||
fun updateCycleLength(length: Int) {
|
||||
if (length in 21..35) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
repository.updateCycleLength(length)
|
||||
// TODO: Реализовать через repository
|
||||
_uiState.value = _uiState.value.copy(cycleLength = length)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
@@ -115,10 +96,10 @@ class SettingsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun updatePeriodLength(length: Int) {
|
||||
if (length in 3..8) {
|
||||
if (length in 3..7) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
repository.updatePeriodLength(length)
|
||||
// TODO: Реализовать через repository
|
||||
_uiState.value = _uiState.value.copy(periodLength = length)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
@@ -127,12 +108,12 @@ class SettingsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
// Цели
|
||||
// Обновление целей
|
||||
fun updateWaterGoal(goal: Float) {
|
||||
if (goal in 1.5f..4.0f) {
|
||||
if (goal > 0) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
repository.updateWaterGoal(goal)
|
||||
// TODO: Реализовать через repository
|
||||
_uiState.value = _uiState.value.copy(waterGoal = goal)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
@@ -142,10 +123,10 @@ class SettingsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun updateStepsGoal(goal: Int) {
|
||||
if (goal in 5000..20000) {
|
||||
if (goal > 0) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
repository.updateStepsGoal(goal)
|
||||
// TODO: Реализовать через repository
|
||||
_uiState.value = _uiState.value.copy(stepsGoal = goal)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
@@ -154,24 +135,11 @@ class SettingsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSleepGoal(goal: Float) {
|
||||
if (goal in 6.0f..10.0f) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
repository.updateSleepGoal(goal)
|
||||
_uiState.value = _uiState.value.copy(sleepGoal = goal)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Внешний вид
|
||||
fun toggleTheme(isDark: Boolean) {
|
||||
// Обновление темы
|
||||
fun updateTheme(isDark: Boolean) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
repository.updateThemeSetting(isDark)
|
||||
// TODO: Реализовать через repository
|
||||
_uiState.value = _uiState.value.copy(isDarkTheme = isDark)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
@@ -179,35 +147,33 @@ class SettingsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
// Управление данными
|
||||
// Экспорт данных
|
||||
fun exportData() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
repository.exportUserData()
|
||||
// Показать сообщение об успехе
|
||||
// TODO: Реализовать экспорт данных
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun importData() {
|
||||
// Импорт данных
|
||||
fun importData(data: String) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
repository.importUserData()
|
||||
loadSettings() // Перезагрузить настройки
|
||||
// TODO: Реализовать импорт данных
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Очистка данных
|
||||
fun clearAllData() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
repository.clearAllUserData()
|
||||
// Сбросить на дефолтные значения
|
||||
_uiState.value = SettingsUiState()
|
||||
// TODO: Реализовать очистку данных
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
}
|
||||
|
||||
@@ -1,875 +0,0 @@
|
||||
package kr.smartsoltech.wellshe.ui.sleep
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import kr.smartsoltech.wellshe.data.entity.SleepLogEntity
|
||||
import kr.smartsoltech.wellshe.ui.theme.*
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SleepScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: SleepViewModel = hiltViewModel()
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.loadSleepData()
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF3F51B5).copy(alpha = 0.2f),
|
||||
NeutralWhite
|
||||
)
|
||||
)
|
||||
),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
item {
|
||||
SleepOverviewCard(
|
||||
lastNightSleep = uiState.lastNightSleep,
|
||||
sleepGoal = uiState.sleepGoal,
|
||||
weeklyAverage = uiState.weeklyAverage
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SleepTrackerCard(
|
||||
isTracking = uiState.isTracking,
|
||||
currentSleep = uiState.currentSleep,
|
||||
onStartTracking = viewModel::startSleepTracking,
|
||||
onStopTracking = viewModel::stopSleepTracking
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SleepQualityCard(
|
||||
todayQuality = uiState.todayQuality,
|
||||
isEditMode = uiState.isEditMode,
|
||||
onQualityUpdate = viewModel::updateSleepQuality,
|
||||
onToggleEdit = viewModel::toggleEditMode
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
WeeklySleepChart(
|
||||
weeklyData = uiState.weeklyData,
|
||||
sleepGoal = uiState.sleepGoal
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SleepInsightsCard(
|
||||
insights = uiState.insights
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SleepTipsCard()
|
||||
}
|
||||
|
||||
item {
|
||||
RecentSleepLogsCard(
|
||||
sleepLogs = uiState.recentLogs,
|
||||
onLogClick = { /* TODO: Navigate to sleep log details */ }
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(80.dp))
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.error != null) {
|
||||
LaunchedEffect(uiState.error) {
|
||||
viewModel.clearError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SleepOverviewCard(
|
||||
lastNightSleep: SleepLogEntity?,
|
||||
sleepGoal: Float,
|
||||
weeklyAverage: Float,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val sleepDuration = lastNightSleep?.duration ?: 0f
|
||||
val progress by animateFloatAsState(
|
||||
targetValue = if (sleepGoal > 0) (sleepDuration / sleepGoal).coerceIn(0f, 1f) else 0f,
|
||||
animationSpec = tween(durationMillis = 1000)
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = NeutralWhite),
|
||||
shape = RoundedCornerShape(20.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "Сон прошлой ночи",
|
||||
style = MaterialTheme.typography.headlineSmall.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier.size(200.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
SleepProgressIndicator(
|
||||
progress = progress,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bedtime,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF3F51B5),
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = if (sleepDuration > 0) "%.1f ч".format(sleepDuration) else "—",
|
||||
style = MaterialTheme.typography.headlineLarge.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(0xFF3F51B5)
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = "из %.1f ч".format(sleepGoal),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = TextSecondary
|
||||
)
|
||||
)
|
||||
if (sleepDuration > 0) {
|
||||
Text(
|
||||
text = "${(progress * 100).toInt()}% от цели",
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(0xFF3F51B5)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
SleepStatItem(
|
||||
icon = Icons.Default.AccessTime,
|
||||
label = "Время сна",
|
||||
value = lastNightSleep?.bedTime ?: "—",
|
||||
color = Color(0xFF9C27B0)
|
||||
)
|
||||
|
||||
SleepStatItem(
|
||||
icon = Icons.Default.WbSunny,
|
||||
label = "Подъем",
|
||||
value = lastNightSleep?.wakeTime ?: "—",
|
||||
color = Color(0xFFFF9800)
|
||||
)
|
||||
|
||||
SleepStatItem(
|
||||
icon = Icons.Default.TrendingUp,
|
||||
label = "Средний сон",
|
||||
value = if (weeklyAverage > 0) "%.1f ч".format(weeklyAverage) else "—",
|
||||
color = Color(0xFF4CAF50)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SleepProgressIndicator(
|
||||
progress: Float,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Canvas(modifier = modifier) {
|
||||
val center = this.center
|
||||
val radius = size.minDimension / 2 - 20.dp.toPx()
|
||||
val strokeWidth = 12.dp.toPx()
|
||||
|
||||
// Фон круга
|
||||
drawCircle(
|
||||
color = Color(0xFFE8EAF6),
|
||||
radius = radius,
|
||||
center = center,
|
||||
style = Stroke(width = strokeWidth)
|
||||
)
|
||||
|
||||
// Прогресс-дуга
|
||||
val sweepAngle = 360f * progress
|
||||
drawArc(
|
||||
color = Color(0xFF3F51B5),
|
||||
startAngle = -90f,
|
||||
sweepAngle = sweepAngle,
|
||||
useCenter = false,
|
||||
style = Stroke(width = strokeWidth, cap = StrokeCap.Round),
|
||||
topLeft = Offset(center.x - radius, center.y - radius),
|
||||
size = Size(radius * 2, radius * 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SleepTrackerCard(
|
||||
isTracking: Boolean,
|
||||
currentSleep: SleepLogEntity?,
|
||||
onStartTracking: () -> Unit,
|
||||
onStopTracking: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = NeutralWhite),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "Трекер сна",
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = TextPrimary
|
||||
),
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
if (isTracking) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bedtime,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF3F51B5),
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "Отслеживание сна активно",
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Начало: ${currentSleep?.bedTime ?: "—"}",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = TextSecondary
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
onClick = onStopTracking,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFFFF5722)
|
||||
),
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Stop,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Завершить сон")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Hotel,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF9E9E9E),
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "Готовы ко сну?",
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Нажмите кнопку, когда ложитесь спать",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = TextSecondary
|
||||
),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
onClick = onStartTracking,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFF3F51B5)
|
||||
),
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Начать отслеживание")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SleepQualityCard(
|
||||
todayQuality: String,
|
||||
isEditMode: Boolean,
|
||||
onQualityUpdate: (String) -> Unit,
|
||||
onToggleEdit: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = NeutralWhite),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Качество сна",
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
|
||||
IconButton(onClick = onToggleEdit) {
|
||||
Icon(
|
||||
imageVector = if (isEditMode) Icons.Default.Check else Icons.Default.Edit,
|
||||
contentDescription = if (isEditMode) "Сохранить" else "Редактировать",
|
||||
tint = Color(0xFF3F51B5)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
if (isEditMode) {
|
||||
val qualities = listOf("Отличное", "Хорошее", "Удовлетворительное", "Плохое")
|
||||
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(qualities) { quality ->
|
||||
FilterChip(
|
||||
onClick = { onQualityUpdate(quality) },
|
||||
label = { Text(quality) },
|
||||
selected = todayQuality == quality,
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
selectedContainerColor = Color(0xFF3F51B5),
|
||||
selectedLabelColor = NeutralWhite
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val qualityIcon = when (todayQuality) {
|
||||
"Отличное" -> Icons.Default.SentimentVerySatisfied
|
||||
"Хорошее" -> Icons.Default.SentimentSatisfied
|
||||
"Удовлетворительное" -> Icons.Default.SentimentNeutral
|
||||
"Плохое" -> Icons.Default.SentimentVeryDissatisfied
|
||||
else -> Icons.Default.SentimentNeutral
|
||||
}
|
||||
|
||||
val qualityColor = when (todayQuality) {
|
||||
"Отличное" -> Color(0xFF4CAF50)
|
||||
"Хорошее" -> Color(0xFF8BC34A)
|
||||
"Удовлетворительное" -> Color(0xFFFF9800)
|
||||
"Плохое" -> Color(0xFFE91E63)
|
||||
else -> Color(0xFF9E9E9E)
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = qualityIcon,
|
||||
contentDescription = null,
|
||||
tint = qualityColor,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Text(
|
||||
text = todayQuality.ifEmpty { "Не оценено" },
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WeeklySleepChart(
|
||||
weeklyData: Map<LocalDate, Float>,
|
||||
sleepGoal: Float,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = NeutralWhite),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Сон за неделю",
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = TextPrimary
|
||||
),
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
weeklyData.entries.toList().takeLast(7).forEach { (date, duration) ->
|
||||
WeeklySleepBar(
|
||||
date = date,
|
||||
duration = duration,
|
||||
goal = sleepGoal,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WeeklySleepBar(
|
||||
date: LocalDate,
|
||||
duration: Float,
|
||||
goal: Float,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val progress = if (goal > 0) (duration / goal).coerceIn(0f, 1f) else 0f
|
||||
val animatedProgress by animateFloatAsState(
|
||||
targetValue = progress,
|
||||
animationSpec = tween(durationMillis = 1000)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = date.dayOfWeek.name.take(3),
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
color = TextSecondary
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(24.dp)
|
||||
.height(80.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(Color(0xFFE8EAF6))
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(animatedProgress)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF7986CB),
|
||||
Color(0xFF3F51B5)
|
||||
)
|
||||
)
|
||||
)
|
||||
.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = if (duration > 0) "%.1f".format(duration) else "—",
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
color = TextPrimary,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SleepInsightsCard(
|
||||
insights: List<String>,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFFE8EAF6)
|
||||
),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Analytics,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF3F51B5),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Text(
|
||||
text = "Анализ сна",
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (insights.isEmpty()) {
|
||||
Text(
|
||||
text = "Недостаточно данных для анализа. Отслеживайте сон несколько дней для получения персональных рекомендаций.",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
} else {
|
||||
insights.forEach { insight ->
|
||||
Row(
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Circle,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF3F51B5),
|
||||
modifier = Modifier.size(8.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Text(
|
||||
text = insight,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SleepTipsCard(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFFF3E5F5)
|
||||
),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Lightbulb,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF9C27B0),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Text(
|
||||
text = "Совет для лучшего сна",
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Создайте ритуал перед сном: выключите экраны за час до сна, примите теплую ванну или выпейте травяной чай. Регулярный режим поможет организму подготовиться ко сну.",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecentSleepLogsCard(
|
||||
sleepLogs: List<SleepLogEntity>,
|
||||
onLogClick: (SleepLogEntity) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = NeutralWhite),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Последние записи сна",
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = TextPrimary
|
||||
),
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
if (sleepLogs.isEmpty()) {
|
||||
Text(
|
||||
text = "Пока нет записей о сне",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = TextSecondary
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
} else {
|
||||
sleepLogs.take(3).forEach { log ->
|
||||
SleepLogItem(
|
||||
sleepLog = log,
|
||||
onClick = { onLogClick(log) }
|
||||
)
|
||||
if (log != sleepLogs.last()) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SleepLogItem(
|
||||
sleepLog: SleepLogEntity,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bedtime,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF3F51B5),
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = sleepLog.date.format(DateTimeFormatter.ofPattern("dd MMMM yyyy")),
|
||||
style = MaterialTheme.typography.bodyLarge.copy(
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = "${sleepLog.bedTime} - ${sleepLog.wakeTime} (%.1f ч)".format(sleepLog.duration),
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
color = TextSecondary
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = sleepLog.quality,
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
color = TextSecondary
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChevronRight,
|
||||
contentDescription = "Просмотреть",
|
||||
tint = TextSecondary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SleepStatItem(
|
||||
icon: ImageVector,
|
||||
label: String,
|
||||
value: String,
|
||||
color: Color,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = color,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
color = TextSecondary
|
||||
),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = TextPrimary,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,675 +0,0 @@
|
||||
package kr.smartsoltech.wellshe.ui.sleep
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import kr.smartsoltech.wellshe.data.entity.SleepLogEntity
|
||||
import kr.smartsoltech.wellshe.ui.theme.*
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SleepTrackingScreen(
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: SleepViewModel = hiltViewModel()
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.loadSleepData()
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
AccentPurpleLight.copy(alpha = 0.2f),
|
||||
NeutralWhite
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Отслеживание сна",
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = "Назад",
|
||||
tint = TextPrimary
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { viewModel.toggleEditMode() }) {
|
||||
Icon(
|
||||
imageVector = if (uiState.isEditMode) Icons.Default.Save else Icons.Default.Edit,
|
||||
contentDescription = if (uiState.isEditMode) "Сохранить" else "Редактировать",
|
||||
tint = AccentPurple
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = NeutralWhite.copy(alpha = 0.95f)
|
||||
)
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
item {
|
||||
TodaySleepCard(
|
||||
uiState = uiState,
|
||||
onUpdateSleep = { bedTime, wakeTime, quality, notes ->
|
||||
// Создаем SleepLogEntity и передаем его в viewModel
|
||||
val sleepLog = SleepLogEntity(
|
||||
date = java.time.LocalDate.now(),
|
||||
bedTime = bedTime,
|
||||
wakeTime = wakeTime,
|
||||
duration = calculateSleepDuration(bedTime, wakeTime),
|
||||
quality = quality,
|
||||
notes = notes
|
||||
)
|
||||
viewModel.updateSleepRecord(sleepLog)
|
||||
},
|
||||
onUpdateQuality = viewModel::updateSleepQuality,
|
||||
onUpdateNotes = viewModel::updateNotes
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SleepStatsCard(
|
||||
recentSleep = uiState.recentSleepLogs,
|
||||
averageDuration = uiState.averageSleepDuration,
|
||||
averageQuality = uiState.averageQuality
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SleepHistoryCard(
|
||||
sleepLogs = uiState.recentSleepLogs,
|
||||
onDeleteLog = viewModel::deleteSleepLog
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SleepTipsCard()
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(80.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TodaySleepCard(
|
||||
uiState: SleepUiState,
|
||||
onUpdateSleep: (String, String, String, String) -> Unit,
|
||||
onUpdateQuality: (String) -> Unit,
|
||||
onUpdateNotes: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var bedTime by remember { mutableStateOf(uiState.todaySleep?.bedTime ?: "22:00") }
|
||||
var wakeTime by remember { mutableStateOf(uiState.todaySleep?.wakeTime ?: "07:00") }
|
||||
var notes by remember { mutableStateOf(uiState.todaySleep?.notes ?: "") }
|
||||
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = NeutralWhite),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Сон за ${LocalDate.now().format(DateTimeFormatter.ofPattern("d MMMM"))}",
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bedtime,
|
||||
contentDescription = null,
|
||||
tint = AccentPurple,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
if (uiState.isEditMode) {
|
||||
// Режим редактирования
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = bedTime,
|
||||
onValueChange = { bedTime = it },
|
||||
label = { Text("Время сна") },
|
||||
placeholder = { Text("22:00") },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = wakeTime,
|
||||
onValueChange = { wakeTime = it },
|
||||
label = { Text("Время пробуждения") },
|
||||
placeholder = { Text("07:00") },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "Качество сна",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(sleepQualities) { quality ->
|
||||
FilterChip(
|
||||
onClick = { onUpdateQuality(quality.key) },
|
||||
label = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(quality.emoji)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(quality.name)
|
||||
}
|
||||
},
|
||||
selected = uiState.todaySleep?.quality == quality.key,
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
selectedContainerColor = AccentPurpleLight,
|
||||
selectedLabelColor = AccentPurple
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = notes,
|
||||
onValueChange = {
|
||||
notes = it
|
||||
onUpdateNotes(it)
|
||||
},
|
||||
label = { Text("Заметки о сне") },
|
||||
placeholder = { Text("Как спалось, что снилось...") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = 2
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
onUpdateSleep(bedTime, wakeTime, uiState.todaySleep?.quality ?: "good", notes)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = AccentPurple)
|
||||
) {
|
||||
Text("Сохранить данные сна")
|
||||
}
|
||||
} else {
|
||||
// Режим просмотра
|
||||
if (uiState.todaySleep != null) {
|
||||
val sleep = uiState.todaySleep
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
SleepMetric(
|
||||
label = "Время сна",
|
||||
value = sleep.bedTime,
|
||||
icon = Icons.Default.NightsStay
|
||||
)
|
||||
|
||||
SleepMetric(
|
||||
label = "Пробуждение",
|
||||
value = sleep.wakeTime,
|
||||
icon = Icons.Default.WbSunny
|
||||
)
|
||||
|
||||
SleepMetric(
|
||||
label = "Длительность",
|
||||
value = "${sleep.duration}ч",
|
||||
icon = Icons.Default.AccessTime
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Качество сна
|
||||
val qualityData = sleepQualities.find { it.key == sleep.quality } ?: sleepQualities[2]
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Качество сна: ",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = TextSecondary
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = qualityData.emoji,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = qualityData.name,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (sleep.notes.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = "Заметки: ${sleep.notes}",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = TextSecondary
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = "Данные о сне за сегодня не добавлены",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = TextSecondary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SleepMetric(
|
||||
label: String,
|
||||
value: String,
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = AccentPurple,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
color = TextSecondary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SleepStatsCard(
|
||||
recentSleep: List<kr.smartsoltech.wellshe.data.entity.SleepLogEntity>,
|
||||
averageDuration: Float,
|
||||
averageQuality: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = NeutralWhite),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Статистика за неделю",
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
SleepStatItem(
|
||||
label = "Средняя длительность",
|
||||
value = "${String.format("%.1f", averageDuration)}ч",
|
||||
icon = Icons.Default.AccessTime
|
||||
)
|
||||
|
||||
SleepStatItem(
|
||||
label = "Записей сна",
|
||||
value = "${recentSleep.size}",
|
||||
icon = Icons.Default.EventNote
|
||||
)
|
||||
|
||||
val qualityData = sleepQualities.find { it.key == averageQuality } ?: sleepQualities[2]
|
||||
SleepStatItem(
|
||||
label = "Среднее качество",
|
||||
value = qualityData.emoji,
|
||||
icon = Icons.Default.Star
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SleepStatItem(
|
||||
label: String,
|
||||
value: String,
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = AccentPurple,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.titleSmall.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
color = TextSecondary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SleepHistoryCard(
|
||||
sleepLogs: List<kr.smartsoltech.wellshe.data.entity.SleepLogEntity>,
|
||||
onDeleteLog: (kr.smartsoltech.wellshe.data.entity.SleepLogEntity) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (sleepLogs.isNotEmpty()) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = NeutralWhite),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "История сна",
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
sleepLogs.take(7).forEach { log ->
|
||||
SleepHistoryItem(
|
||||
log = log,
|
||||
onDelete = { onDeleteLog(log) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SleepHistoryItem(
|
||||
log: kr.smartsoltech.wellshe.data.entity.SleepLogEntity,
|
||||
onDelete: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bedtime,
|
||||
contentDescription = null,
|
||||
tint = AccentPurple,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = log.date.format(DateTimeFormatter.ofPattern("d MMMM yyyy")),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "${log.bedTime} - ${log.wakeTime} (${log.duration}ч)",
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
color = TextSecondary
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val qualityData = sleepQualities.find { it.key == log.quality } ?: sleepQualities[2]
|
||||
Text(
|
||||
text = qualityData.emoji,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
IconButton(
|
||||
onClick = onDelete,
|
||||
modifier = Modifier.size(24.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Удалить",
|
||||
tint = ErrorRed,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SleepTipsCard(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = AccentPurpleLight.copy(alpha = 0.3f)),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Lightbulb,
|
||||
contentDescription = null,
|
||||
tint = AccentPurple,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Советы для лучшего сна",
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
sleepTips.forEach { tip ->
|
||||
Row(
|
||||
modifier = Modifier.padding(vertical = 2.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "• ",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = AccentPurple,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = tip,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = TextPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Данные для UI
|
||||
private data class SleepQualityData(val key: String, val name: String, val emoji: String)
|
||||
|
||||
private val sleepQualities = listOf(
|
||||
SleepQualityData("poor", "Плохо", "😴"),
|
||||
SleepQualityData("fair", "Нормально", "😐"),
|
||||
SleepQualityData("good", "Хорошо", "😊"),
|
||||
SleepQualityData("excellent", "Отлично", "😄")
|
||||
)
|
||||
|
||||
private val sleepTips = listOf(
|
||||
"Ложитесь спать в одно и то же время",
|
||||
"Избегайте кофеина за 6 часов до сна",
|
||||
"Создайте прохладную и темную атмосферу",
|
||||
"Ограничьте использование экранов перед сном",
|
||||
"Проветривайте спальню перед сном",
|
||||
"Делайте расслабляющие упражнения"
|
||||
)
|
||||
|
||||
// Вспомогательная функция для расчета продолжительности сна
|
||||
private fun calculateSleepDuration(bedTime: String, wakeTime: String): Float {
|
||||
return try {
|
||||
val bedLocalTime = LocalTime.parse(bedTime)
|
||||
val wakeLocalTime = LocalTime.parse(wakeTime)
|
||||
|
||||
val duration = if (wakeLocalTime.isAfter(bedLocalTime)) {
|
||||
// Сон в пределах одного дня
|
||||
java.time.Duration.between(bedLocalTime, wakeLocalTime)
|
||||
} else {
|
||||
// Сон через полночь
|
||||
val endOfDay = LocalTime.of(23, 59, 59)
|
||||
val startOfDay = LocalTime.MIDNIGHT
|
||||
val beforeMidnight = java.time.Duration.between(bedLocalTime, endOfDay)
|
||||
val afterMidnight = java.time.Duration.between(startOfDay, wakeLocalTime)
|
||||
beforeMidnight.plus(afterMidnight).plusMinutes(1)
|
||||
}
|
||||
|
||||
duration.toMinutes() / 60.0f
|
||||
} catch (e: Exception) {
|
||||
8.0f // Возвращаем значение по умолчанию
|
||||
}
|
||||
}
|
||||
@@ -1,335 +0,0 @@
|
||||
package kr.smartsoltech.wellshe.ui.sleep
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kr.smartsoltech.wellshe.data.entity.SleepLogEntity
|
||||
import kr.smartsoltech.wellshe.data.repository.WellSheRepository
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import javax.inject.Inject
|
||||
|
||||
data class SleepUiState(
|
||||
val lastNightSleep: SleepLogEntity? = null,
|
||||
val currentSleep: SleepLogEntity? = null,
|
||||
val todaySleep: SleepLogEntity? = null,
|
||||
val recentLogs: List<SleepLogEntity> = emptyList(),
|
||||
val recentSleepLogs: List<SleepLogEntity> = emptyList(), // Добавляем недостающее поле
|
||||
val averageSleepDuration: Float = 0f, // Добавляем недостающее поле
|
||||
val averageQuality: String = "", // Добавляем недостающее поле
|
||||
val weeklyData: Map<LocalDate, Float> = emptyMap(),
|
||||
val sleepGoal: Float = 8.0f,
|
||||
val weeklyAverage: Float = 0f,
|
||||
val todayQuality: String = "",
|
||||
val insights: List<String> = emptyList(),
|
||||
val isTracking: Boolean = false,
|
||||
val isEditMode: Boolean = false,
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class SleepViewModel @Inject constructor(
|
||||
private val repository: WellSheRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(SleepUiState())
|
||||
val uiState: StateFlow<SleepUiState> = _uiState.asStateFlow()
|
||||
|
||||
fun loadSleepData() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||
|
||||
try {
|
||||
val today = LocalDate.now()
|
||||
val yesterday = today.minusDays(1)
|
||||
|
||||
// Загружаем сон прошлой ночи
|
||||
val lastNightSleep = repository.getSleepForDate(yesterday)
|
||||
|
||||
// Загружаем последние записи сна
|
||||
repository.getRecentSleepLogs().collect { logs ->
|
||||
val weeklyAverage = calculateWeeklyAverage(logs)
|
||||
val weeklyData = createWeeklyData(logs)
|
||||
val insights = generateInsights(logs)
|
||||
|
||||
_uiState.value = _uiState.value.copy(
|
||||
lastNightSleep = lastNightSleep,
|
||||
recentLogs = logs,
|
||||
weeklyData = weeklyData,
|
||||
weeklyAverage = weeklyAverage,
|
||||
insights = insights,
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
|
||||
// Загружаем цель сна пользователя
|
||||
repository.getUserProfile().collect { user ->
|
||||
_uiState.value = _uiState.value.copy(
|
||||
sleepGoal = user.dailySleepGoal
|
||||
)
|
||||
}
|
||||
|
||||
// Проверяем текущее качество сна
|
||||
val todaySleep = repository.getSleepForDate(today)
|
||||
_uiState.value = _uiState.value.copy(
|
||||
todayQuality = todaySleep?.quality ?: ""
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
error = e.message
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startSleepTracking() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val now = LocalTime.now()
|
||||
val bedTime = now.format(DateTimeFormatter.ofPattern("HH:mm"))
|
||||
|
||||
val sleepLog = SleepLogEntity(
|
||||
date = LocalDate.now(),
|
||||
bedTime = bedTime,
|
||||
wakeTime = "",
|
||||
duration = 0f,
|
||||
quality = "",
|
||||
notes = ""
|
||||
)
|
||||
|
||||
// TODO: Сохранить в базу данных и получить ID
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isTracking = true,
|
||||
currentSleep = sleepLog
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopSleepTracking() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val currentSleep = _uiState.value.currentSleep
|
||||
if (currentSleep != null) {
|
||||
val now = LocalTime.now()
|
||||
val wakeTime = now.format(DateTimeFormatter.ofPattern("HH:mm"))
|
||||
|
||||
// Вычисляем продолжительность сна
|
||||
val duration = calculateSleepDuration(currentSleep.bedTime, wakeTime)
|
||||
|
||||
repository.addSleepRecord(
|
||||
date = currentSleep.date,
|
||||
bedTime = currentSleep.bedTime,
|
||||
wakeTime = wakeTime,
|
||||
quality = "Хорошее", // По умолчанию
|
||||
notes = ""
|
||||
)
|
||||
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isTracking = false,
|
||||
currentSleep = null
|
||||
)
|
||||
|
||||
loadSleepData() // Перезагружаем данные
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSleepQuality(quality: String) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val today = LocalDate.now()
|
||||
val existingSleep = repository.getSleepForDate(today)
|
||||
|
||||
if (existingSleep != null) {
|
||||
// Обновляем существующую запись
|
||||
repository.addSleepRecord(
|
||||
date = today,
|
||||
bedTime = existingSleep.bedTime,
|
||||
wakeTime = existingSleep.wakeTime,
|
||||
quality = quality,
|
||||
notes = existingSleep.notes
|
||||
)
|
||||
} else {
|
||||
// Создаем новую запись только с качеством
|
||||
repository.addSleepRecord(
|
||||
date = today,
|
||||
bedTime = "",
|
||||
wakeTime = "",
|
||||
quality = quality,
|
||||
notes = ""
|
||||
)
|
||||
}
|
||||
|
||||
_uiState.value = _uiState.value.copy(
|
||||
todayQuality = quality,
|
||||
isEditMode = false
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleEditMode() {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isEditMode = !_uiState.value.isEditMode
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteSleepLog(sleepLog: SleepLogEntity) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
// TODO: Реализовать удаление записи через repository
|
||||
loadSleepData() // Перезагружаем данные
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSleepRecord(sleepLog: SleepLogEntity) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
repository.addSleepRecord(
|
||||
date = sleepLog.date,
|
||||
bedTime = sleepLog.bedTime,
|
||||
wakeTime = sleepLog.wakeTime,
|
||||
quality = sleepLog.quality,
|
||||
notes = sleepLog.notes
|
||||
)
|
||||
loadSleepData() // Перезагружаем данные
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(error = e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNotes(notes: String) {
|
||||
val currentSleep = _uiState.value.currentSleep
|
||||
if (currentSleep != null) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
currentSleep = currentSleep.copy(notes = notes)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateWeeklyAverage(logs: List<SleepLogEntity>): Float {
|
||||
if (logs.isEmpty()) return 0f
|
||||
val totalDuration = logs.sumOf { it.duration.toDouble() }
|
||||
return (totalDuration / logs.size).toFloat()
|
||||
}
|
||||
|
||||
private fun createWeeklyData(logs: List<SleepLogEntity>): Map<LocalDate, Float> {
|
||||
val weeklyData = mutableMapOf<LocalDate, Float>()
|
||||
val today = LocalDate.now()
|
||||
|
||||
for (i in 0..6) {
|
||||
val date = today.minusDays(i.toLong())
|
||||
val sleepForDate = logs.find { it.date == date }
|
||||
weeklyData[date] = sleepForDate?.duration ?: 0f
|
||||
}
|
||||
|
||||
return weeklyData
|
||||
}
|
||||
|
||||
private fun generateInsights(logs: List<SleepLogEntity>): List<String> {
|
||||
val insights = mutableListOf<String>()
|
||||
|
||||
if (logs.size >= 7) {
|
||||
val averageDuration = calculateWeeklyAverage(logs)
|
||||
val goal = _uiState.value.sleepGoal
|
||||
|
||||
when {
|
||||
averageDuration < goal - 1 -> {
|
||||
insights.add("Вы спите в среднем на ${String.format("%.1f", goal - averageDuration)} часов меньше рекомендуемого")
|
||||
}
|
||||
averageDuration > goal + 1 -> {
|
||||
insights.add("Вы спите больше рекомендуемого времени")
|
||||
}
|
||||
else -> {
|
||||
insights.add("Ваш режим сна близок к оптимальному")
|
||||
}
|
||||
}
|
||||
|
||||
// Анализ регулярности
|
||||
val bedTimes = logs.mapNotNull {
|
||||
if (it.bedTime.isNotEmpty()) {
|
||||
val parts = it.bedTime.split(":")
|
||||
if (parts.size == 2) {
|
||||
parts[0].toIntOrNull()?.let { hour ->
|
||||
hour * 60 + (parts[1].toIntOrNull() ?: 0)
|
||||
}
|
||||
} else null
|
||||
} else null
|
||||
}
|
||||
|
||||
if (bedTimes.size >= 5) {
|
||||
val avgBedTime = bedTimes.average()
|
||||
val deviation = bedTimes.map { kotlin.math.abs(it - avgBedTime) }.average()
|
||||
|
||||
if (deviation > 60) { // Больше часа отклонения
|
||||
insights.add("Старайтесь ложиться спать в одно и то же время")
|
||||
} else {
|
||||
insights.add("У вас хороший регулярный режим сна")
|
||||
}
|
||||
}
|
||||
|
||||
// Анализ качества
|
||||
val qualityGood = logs.count { it.quality in listOf("Отличное", "Хорошее") }
|
||||
val qualityPercent = (qualityGood.toFloat() / logs.size) * 100
|
||||
|
||||
when {
|
||||
qualityPercent >= 80 -> insights.add("Качество вашего сна отличное!")
|
||||
qualityPercent >= 60 -> insights.add("Качество сна можно улучшить")
|
||||
else -> insights.add("Рекомендуем обратить внимание на гигиену сна")
|
||||
}
|
||||
}
|
||||
|
||||
return insights
|
||||
}
|
||||
|
||||
private fun calculateSleepDuration(bedTime: String, wakeTime: String): Float {
|
||||
try {
|
||||
val bedParts = bedTime.split(":")
|
||||
val wakeParts = wakeTime.split(":")
|
||||
|
||||
if (bedParts.size == 2 && wakeParts.size == 2) {
|
||||
val bedMinutes = bedParts[0].toInt() * 60 + bedParts[1].toInt()
|
||||
val wakeMinutes = wakeParts[0].toInt() * 60 + wakeParts[1].toInt()
|
||||
|
||||
val sleepMinutes = if (wakeMinutes > bedMinutes) {
|
||||
wakeMinutes - bedMinutes
|
||||
} else {
|
||||
// Переход через полночь
|
||||
(24 * 60 - bedMinutes) + wakeMinutes
|
||||
}
|
||||
|
||||
return sleepMinutes / 60f
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Если не удается рассчитать, возвращаем 8 часов по умолчанию
|
||||
}
|
||||
|
||||
return 8.0f
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
_uiState.value = _uiState.value.copy(error = null)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<!-- Разрешаем незащищенное HTTP-соединение с IP-адресом 192.168.0.112 -->
|
||||
<!-- Разрешаем незащищенное HTTP-соединение -->
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">192.168.0.112</domain>
|
||||
<domain includeSubdomains="true">192.168.219.108</domain>
|
||||
</domain-config>
|
||||
|
||||
<!-- Настройки по умолчанию - запрещаем незащищенный HTTP-трафик для других адресов -->
|
||||
|
||||
@@ -15,8 +15,7 @@ class CycleAnalyticsTest {
|
||||
endDate = LocalDate.now().minusDays(23),
|
||||
cycleLength = 28,
|
||||
flow = "medium",
|
||||
symptoms = emptyList(),
|
||||
mood = "neutral"
|
||||
symptoms = emptyList()
|
||||
),
|
||||
CyclePeriodEntity(
|
||||
id = 1,
|
||||
@@ -24,8 +23,7 @@ class CycleAnalyticsTest {
|
||||
endDate = LocalDate.now().minusDays(51),
|
||||
cycleLength = 28,
|
||||
flow = "medium",
|
||||
symptoms = emptyList(),
|
||||
mood = "neutral"
|
||||
symptoms = emptyList()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -44,8 +42,7 @@ class CycleAnalyticsTest {
|
||||
endDate = LocalDate.now().minusDays(23),
|
||||
cycleLength = 28,
|
||||
flow = "medium",
|
||||
symptoms = emptyList(),
|
||||
mood = "neutral"
|
||||
symptoms = emptyList()
|
||||
),
|
||||
CyclePeriodEntity(
|
||||
id = 1,
|
||||
@@ -53,8 +50,7 @@ class CycleAnalyticsTest {
|
||||
endDate = LocalDate.now().minusDays(51),
|
||||
cycleLength = 28,
|
||||
flow = "medium",
|
||||
symptoms = emptyList(),
|
||||
mood = "neutral"
|
||||
symptoms = emptyList()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -72,8 +68,7 @@ class CycleAnalyticsTest {
|
||||
endDate = LocalDate.now().minusDays(23),
|
||||
cycleLength = 28,
|
||||
flow = "medium",
|
||||
symptoms = emptyList(),
|
||||
mood = "neutral"
|
||||
symptoms = emptyList()
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package kr.smartsoltech.wellshe.domain.analytics
|
||||
|
||||
import kr.smartsoltech.wellshe.data.entity.SleepLogEntity
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
class SleepAnalyticsTest {
|
||||
@Test
|
||||
fun testSleepDebt() {
|
||||
val logs = listOf(
|
||||
SleepLogEntity(
|
||||
id = 0,
|
||||
date = java.time.LocalDate.now(),
|
||||
bedTime = "22:00",
|
||||
wakeTime = "06:00",
|
||||
duration = 8.0f,
|
||||
quality = "good",
|
||||
notes = ""
|
||||
),
|
||||
SleepLogEntity(
|
||||
id = 0,
|
||||
date = java.time.LocalDate.now().minusDays(1),
|
||||
bedTime = "23:00",
|
||||
wakeTime = "06:00",
|
||||
duration = 7.0f,
|
||||
quality = "normal",
|
||||
notes = ""
|
||||
)
|
||||
)
|
||||
val debt = SleepAnalytics.sleepDebt(logs, 8)
|
||||
assertEquals(1, debt)
|
||||
}
|
||||
}
|
||||
@@ -20,4 +20,6 @@ kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonTransitiveRClass=true
|
||||
|
||||
API_BASE_URL=http://192.168.219.108:8000/api/v1/
|
||||
|
||||
@@ -10,6 +10,7 @@ activityCompose = "1.8.2"
|
||||
composeBom = "2024.02.02"
|
||||
hilt = "2.48"
|
||||
composeCompiler = "1.5.14"
|
||||
material = "1.13.0"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
@@ -28,6 +29,7 @@ androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-te
|
||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
|
||||
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
Reference in New Issue
Block a user