настроение убрано, вместо него, вкладка "экстренные сигналы"

This commit is contained in:
2025-10-16 19:46:29 +09:00
parent f429d54e1b
commit 18753b214d
35 changed files with 305 additions and 3147 deletions

View File

@@ -27,6 +27,8 @@ android {
) )
} }
} }
buildConfigField("String", "API_BASE_URL", "\"${project.findProperty("API_BASE_URL")}\"")
} }
buildTypes { buildTypes {
@@ -48,6 +50,7 @@ android {
buildFeatures { buildFeatures {
compose = true compose = true
viewBinding = true viewBinding = true
buildConfig = true
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion = "1.5.14" kotlinCompilerExtensionVersion = "1.5.14"
@@ -65,6 +68,7 @@ dependencies {
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
implementation("androidx.hilt:hilt-navigation-compose:1.1.0") implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
implementation(libs.hilt.android) implementation(libs.hilt.android)
implementation(libs.material)
kapt(libs.hilt.compiler) kapt(libs.hilt.compiler)
implementation("androidx.room:room-runtime:2.6.1") implementation("androidx.room:room-runtime:2.6.1")
kapt("androidx.room:room-compiler:2.6.1") kapt("androidx.room:room-compiler:2.6.1")

View File

@@ -12,7 +12,6 @@ import androidx.room.TypeConverter
entities = [ entities = [
// Основные сущности // Основные сущности
WaterLogEntity::class, WaterLogEntity::class,
SleepLogEntity::class,
WorkoutEntity::class, WorkoutEntity::class,
CalorieEntity::class, CalorieEntity::class,
StepsEntity::class, StepsEntity::class,
@@ -43,13 +42,12 @@ import androidx.room.TypeConverter
ExerciseFormulaVar::class, ExerciseFormulaVar::class,
CatalogVersion::class CatalogVersion::class
], ],
version = 11, version = 13, // Увеличиваем версию базы данных после удаления полей mood и stressLevel
exportSchema = true exportSchema = true
) )
@TypeConverters(LocalDateConverter::class, InstantConverter::class, StringListConverter::class) @TypeConverters(LocalDateConverter::class, InstantConverter::class, StringListConverter::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun waterLogDao(): WaterLogDao abstract fun waterLogDao(): WaterLogDao
abstract fun sleepLogDao(): SleepLogDao
abstract fun workoutDao(): WorkoutDao abstract fun workoutDao(): WorkoutDao
abstract fun calorieDao(): CalorieDao abstract fun calorieDao(): CalorieDao
abstract fun stepsDao(): StepsDao abstract fun stepsDao(): StepsDao
@@ -63,6 +61,8 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun cycleForecastDao(): CycleForecastDao abstract fun cycleForecastDao(): CycleForecastDao
// Дополнительные DAO для repo // Дополнительные DAO для repo
abstract fun beverageDao(): BeverageDao
abstract fun beverageServingDao(): BeverageServingDao
abstract fun beverageLogDao(): BeverageLogDao abstract fun beverageLogDao(): BeverageLogDao
abstract fun beverageLogNutrientDao(): BeverageLogNutrientDao abstract fun beverageLogNutrientDao(): BeverageLogNutrientDao
abstract fun beverageServingNutrientDao(): BeverageServingNutrientDao abstract fun beverageServingNutrientDao(): BeverageServingNutrientDao
@@ -71,8 +71,11 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun workoutSessionParamDao(): WorkoutSessionParamDao abstract fun workoutSessionParamDao(): WorkoutSessionParamDao
abstract fun workoutEventDao(): WorkoutEventDao abstract fun workoutEventDao(): WorkoutEventDao
abstract fun exerciseDao(): ExerciseDao abstract fun exerciseDao(): ExerciseDao
abstract fun exerciseParamDao(): ExerciseParamDao
abstract fun exerciseFormulaDao(): ExerciseFormulaDao abstract fun exerciseFormulaDao(): ExerciseFormulaDao
abstract fun exerciseFormulaVarDao(): ExerciseFormulaVarDao abstract fun exerciseFormulaVarDao(): ExerciseFormulaVarDao
abstract fun nutrientDao(): NutrientDao
abstract fun catalogVersionDao(): CatalogVersionDao
} }
class LocalDateConverter { class LocalDateConverter {

View File

@@ -5,27 +5,6 @@ import kotlinx.coroutines.flow.Flow
import kr.smartsoltech.wellshe.data.entity.* import kr.smartsoltech.wellshe.data.entity.*
import java.time.LocalDate 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 @Dao
interface WorkoutDao { interface WorkoutDao {
@Query("SELECT * FROM workouts WHERE date = :date ORDER BY id DESC") @Query("SELECT * FROM workouts WHERE date = :date ORDER BY id DESC")

View File

@@ -22,6 +22,5 @@ data class CycleHistoryEntity(
// Добавляем поля для соответствия с CyclePeriodEntity // Добавляем поля для соответствия с CyclePeriodEntity
val flow: String = "", val flow: String = "",
val symptoms: List<String> = emptyList(), val symptoms: List<String> = emptyList(),
val mood: String = "",
val cycleLength: Int? = null val cycleLength: Int? = null
) )

View File

@@ -11,6 +11,5 @@ data class CyclePeriodEntity(
val endDate: LocalDate?, val endDate: LocalDate?,
val flow: String = "", val flow: String = "",
val symptoms: List<String> = emptyList(), val symptoms: List<String> = emptyList(),
val mood: String = "",
val cycleLength: Int? = null val cycleLength: Int? = null
) )

View File

@@ -13,18 +13,6 @@ data class WaterLogEntity(
val timestamp: Long = System.currentTimeMillis() 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") @Entity(tableName = "workouts")
data class WorkoutEntity( data class WorkoutEntity(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@@ -76,5 +64,10 @@ data class UserProfileEntity(
val cycleLength: Int = 28, val cycleLength: Int = 28,
val periodLength: Int = 5, val periodLength: Int = 5,
val lastPeriodDate: LocalDate? = null, 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
) )

View File

@@ -13,10 +13,7 @@ data class HealthRecordEntity(
val bloodPressureS: Int?, val bloodPressureS: Int?,
val bloodPressureD: Int?, val bloodPressureD: Int?,
val temperature: Float?, val temperature: Float?,
val mood: String?,
val energyLevel: Int?, val energyLevel: Int?,
val stressLevel: Int?,
val symptoms: List<String>?, val symptoms: List<String>?,
val notes: String? val notes: String?
) )

View File

@@ -12,7 +12,7 @@ import java.util.concurrent.TimeUnit
* Класс для настройки и создания API-клиентов * Класс для настройки и создания API-клиентов
*/ */
object ApiClient { 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 CONNECT_TIMEOUT = 15L
private const val READ_TIMEOUT = 15L private const val READ_TIMEOUT = 15L
private const val WRITE_TIMEOUT = 15L private const val WRITE_TIMEOUT = 15L

View File

@@ -263,7 +263,6 @@ class CycleRepository @Inject constructor(
endDate = historyEntity.periodEnd, endDate = historyEntity.periodEnd,
flow = historyEntity.flow, flow = historyEntity.flow,
symptoms = historyEntity.symptoms, symptoms = historyEntity.symptoms,
mood = historyEntity.mood,
cycleLength = historyEntity.cycleLength cycleLength = historyEntity.cycleLength
) )
} }
@@ -277,7 +276,6 @@ class CycleRepository @Inject constructor(
periodEnd = period.endDate, periodEnd = period.endDate,
flow = period.flow, flow = period.flow,
symptoms = period.symptoms, symptoms = period.symptoms,
mood = period.mood,
cycleLength = period.cycleLength, cycleLength = period.cycleLength,
atypical = false // по умолчанию не отмечаем как нетипичный atypical = false // по умолчанию не отмечаем как нетипичный
) )
@@ -292,7 +290,6 @@ class CycleRepository @Inject constructor(
periodEnd = period.endDate, periodEnd = period.endDate,
flow = period.flow, flow = period.flow,
symptoms = period.symptoms, symptoms = period.symptoms,
mood = period.mood,
cycleLength = period.cycleLength, cycleLength = period.cycleLength,
atypical = false // сохраняем существующее значение, если возможно atypical = false // сохраняем существующее значение, если возможно
) )
@@ -306,7 +303,6 @@ class CycleRepository @Inject constructor(
periodEnd = period.endDate, periodEnd = period.endDate,
flow = period.flow, flow = period.flow,
symptoms = period.symptoms, symptoms = period.symptoms,
mood = period.mood,
cycleLength = period.cycleLength, cycleLength = period.cycleLength,
atypical = false atypical = false
) )

View File

@@ -12,7 +12,6 @@ import kr.smartsoltech.wellshe.domain.model.User
import kr.smartsoltech.wellshe.domain.model.WaterIntake import kr.smartsoltech.wellshe.domain.model.WaterIntake
import kr.smartsoltech.wellshe.domain.model.WorkoutSession import kr.smartsoltech.wellshe.domain.model.WorkoutSession
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -21,7 +20,6 @@ import javax.inject.Singleton
class WellSheRepository @Inject constructor( class WellSheRepository @Inject constructor(
private val waterLogDao: WaterLogDao, private val waterLogDao: WaterLogDao,
private val cyclePeriodDao: CyclePeriodDao, private val cyclePeriodDao: CyclePeriodDao,
private val sleepLogDao: SleepLogDao,
private val healthRecordDao: HealthRecordDao, private val healthRecordDao: HealthRecordDao,
private val workoutDao: WorkoutDao, private val workoutDao: WorkoutDao,
private val calorieDao: CalorieDao, private val calorieDao: CalorieDao,
@@ -45,8 +43,7 @@ class WellSheRepository @Inject constructor(
weight = 60f, weight = 60f,
dailyWaterGoal = 2.5f, dailyWaterGoal = 2.5f,
dailyStepsGoal = 10000, dailyStepsGoal = 10000,
dailyCaloriesGoal = 2000, dailyCaloriesGoal = 2000
dailySleepGoal = 8.0f
) )
) )
} }
@@ -157,231 +154,89 @@ class WellSheRepository @Inject constructor(
// TODO: Реализовать окончание тренировки // 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( val period = CyclePeriodEntity(
startDate = startDate, startDate = startDate,
endDate = endDate, endDate = endDate,
flow = flow, flow = flow,
symptoms = symptoms, symptoms = symptoms
mood = mood
) )
cyclePeriodDao.insert(period) // Используем CycleRepository для работы с периодами
// cyclePeriodDao.insertPeriod(period)
// TODO: Добавить интеграцию с CycleRepository
} }
suspend fun updatePeriod(periodId: Long, endDate: LocalDate?, flow: String, symptoms: List<String>, mood: String) { suspend fun updatePeriod(periodId: Long, endDate: LocalDate?, flow: String, symptoms: List<String>) {
val periods = cyclePeriodDao.getAll() // TODO: Реализовать через CycleRepository
val existingPeriod = periods.firstOrNull { it.id == periodId } // val existingPeriod = cyclePeriodDao.getPeriodById(periodId)
if (existingPeriod != null) { // existingPeriod?.let {
val updatedPeriod = existingPeriod.copy( // val updatedPeriod = it.copy(
endDate = endDate, // endDate = endDate,
flow = flow, // flow = flow,
symptoms = symptoms, // symptoms = symptoms
mood = mood // )
) // cyclePeriodDao.updatePeriod(updatedPeriod)
cyclePeriodDao.update(updatedPeriod) // }
}
} }
suspend fun getRecentPeriods(): List<CyclePeriodEntity> { fun getPeriods(): Flow<List<CyclePeriodEntity>> {
return cyclePeriodDao.getAll().take(6) // 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: Реализовать получение настроек из БД // TODO: Реализовать получение настроек из БД
return flowOf( return flowOf(
AppSettings( AppSettings(
isWaterReminderEnabled = true, notificationsEnabled = true,
isCycleReminderEnabled = true, darkModeEnabled = false
isSleepReminderEnabled = true,
cycleLength = 28,
periodLength = 5,
waterGoal = 2.5f,
stepsGoal = 10000,
sleepGoal = 8.0f,
isDarkTheme = false
) )
) )
} }
suspend fun updateWaterReminderSetting(enabled: Boolean) { suspend fun updateAppSettings(settings: AppSettings) {
// TODO: Реализовать обновление настройки напоминаний о воде // 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 exportUserData() { fun getDashboardData(date: LocalDate): Flow<DashboardData> {
// 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>> {
return flow { 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( 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 date: LocalDate,
val bloodPressureSystolic: Int = 0, val waterIntake: Float,
val bloodPressureDiastolic: Int = 0, val steps: Int,
val heartRate: Int = 0, val calories: Int,
val weight: Float = 0f, val workouts: Int,
val mood: String = "neutral", // Добавляем поле настроения val cycleDay: Int?
val energyLevel: Int = 5, // Добавляем уровень энергии (1-10)
val stressLevel: Int = 5, // Добавляем уровень стресса (1-10)
val notes: String = ""
) )

View File

@@ -8,21 +8,8 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kr.smartsoltech.wellshe.data.AppDatabase import kr.smartsoltech.wellshe.data.AppDatabase
import kr.smartsoltech.wellshe.data.datastore.DataStoreManager
import kr.smartsoltech.wellshe.data.dao.* import kr.smartsoltech.wellshe.data.dao.*
import kr.smartsoltech.wellshe.data.repo.DrinkLogger import kr.smartsoltech.wellshe.data.repo.*
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 javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@@ -31,34 +18,18 @@ object AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideDataStoreManager(@ApplicationContext context: Context): DataStoreManager = fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
DataStoreManager(context) return Room.databaseBuilder(
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase =
Room.databaseBuilder(
context, context,
AppDatabase::class.java, AppDatabase::class.java,
"well_she_db" "wellshe_database"
) ).build()
.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()
// DAO providers // DAO Providers
@Provides @Provides
fun provideWaterLogDao(database: AppDatabase): WaterLogDao = database.waterLogDao() 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 @Provides
fun provideWorkoutDao(database: AppDatabase): WorkoutDao = database.workoutDao() fun provideWorkoutDao(database: AppDatabase): WorkoutDao = database.workoutDao()
@@ -71,7 +42,12 @@ object AppModule {
@Provides @Provides
fun provideUserProfileDao(database: AppDatabase): UserProfileDao = database.userProfileDao() 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 @Provides
fun provideBeverageLogDao(database: AppDatabase): BeverageLogDao = database.beverageLogDao() fun provideBeverageLogDao(database: AppDatabase): BeverageLogDao = database.beverageLogDao()
@@ -102,7 +78,28 @@ object AppModule {
@Provides @Provides
fun provideExerciseFormulaVarDao(database: AppDatabase): ExerciseFormulaVarDao = database.exerciseFormulaVarDao() 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 @Provides
@Singleton @Singleton
fun provideDrinkLogger( fun provideDrinkLogger(
@@ -110,12 +107,9 @@ object AppModule {
beverageLogDao: BeverageLogDao, beverageLogDao: BeverageLogDao,
beverageLogNutrientDao: BeverageLogNutrientDao, beverageLogNutrientDao: BeverageLogNutrientDao,
servingNutrientDao: BeverageServingNutrientDao servingNutrientDao: BeverageServingNutrientDao
): DrinkLogger = DrinkLogger(waterLogDao, beverageLogDao, beverageLogNutrientDao, servingNutrientDao) ): DrinkLogger {
return DrinkLogger(waterLogDao, beverageLogDao, beverageLogNutrientDao, servingNutrientDao)
@Provides }
@Singleton
fun provideWeightRepository(weightLogDao: WeightLogDao): WeightRepository =
WeightRepository(weightLogDao)
@Provides @Provides
@Singleton @Singleton
@@ -127,23 +121,27 @@ object AppModule {
formulaDao: ExerciseFormulaDao, formulaDao: ExerciseFormulaDao,
formulaVarDao: ExerciseFormulaVarDao, formulaVarDao: ExerciseFormulaVarDao,
exerciseDao: ExerciseDao exerciseDao: ExerciseDao
): WorkoutService = WorkoutService(sessionDao, paramDao, eventDao, weightRepo, formulaDao, formulaVarDao, exerciseDao) ): WorkoutService {
return WorkoutService(sessionDao, paramDao, eventDao, weightRepo, formulaDao, formulaVarDao, exerciseDao)
}
// Repository
@Provides @Provides
@Singleton @Singleton
fun provideWellSheRepository( fun provideBeverageCatalogRepository(
waterLogDao: WaterLogDao, beverageDao: BeverageDao,
cyclePeriodDao: CyclePeriodDao, servingDao: BeverageServingDao,
sleepLogDao: SleepLogDao, servingNutrientDao: BeverageServingNutrientDao
healthRecordDao: HealthRecordDao, ): BeverageCatalogRepository {
workoutDao: WorkoutDao, return BeverageCatalogRepository(beverageDao, servingDao, servingNutrientDao)
calorieDao: CalorieDao, }
stepsDao: StepsDao,
userProfileDao: UserProfileDao @Provides
): kr.smartsoltech.wellshe.data.repository.WellSheRepository = @Singleton
kr.smartsoltech.wellshe.data.repository.WellSheRepository( fun provideExerciseCatalogRepository(
waterLogDao, cyclePeriodDao, sleepLogDao, healthRecordDao, exerciseDao: ExerciseDao,
workoutDao, calorieDao, stepsDao, userProfileDao paramDao: ExerciseParamDao,
) formulaDao: ExerciseFormulaDao
): ExerciseCatalogRepository {
return ExerciseCatalogRepository(exerciseDao, paramDao, formulaDao)
}
} }

View File

@@ -6,6 +6,7 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kr.smartsoltech.wellshe.BuildConfig
import kr.smartsoltech.wellshe.data.local.AuthTokenRepository import kr.smartsoltech.wellshe.data.local.AuthTokenRepository
import kr.smartsoltech.wellshe.data.network.AuthInterceptor import kr.smartsoltech.wellshe.data.network.AuthInterceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@@ -18,8 +19,6 @@ import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object NetworkModule { object NetworkModule {
private const val BASE_URL = "http://192.168.0.112:8000/api/v1/"
private const val CONNECT_TIMEOUT = 15L private const val CONNECT_TIMEOUT = 15L
private const val READ_TIMEOUT = 15L private const val READ_TIMEOUT = 15L
private const val WRITE_TIMEOUT = 15L private const val WRITE_TIMEOUT = 15L
@@ -40,27 +39,20 @@ object NetworkModule {
@Provides @Provides
@Singleton @Singleton
fun provideOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient { fun provideRetrofit(gson: Gson, authInterceptor: AuthInterceptor): Retrofit {
val loggingInterceptor = HttpLoggingInterceptor().apply { val client = OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY level = HttpLoggingInterceptor.Level.BODY
} })
return OkHttpClient.Builder()
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS) .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS) .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS) .writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
.addInterceptor(loggingInterceptor)
.addInterceptor(authInterceptor)
.build() .build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient, gson: Gson): Retrofit {
return Retrofit.Builder() return Retrofit.Builder()
.baseUrl(BASE_URL) .baseUrl(BuildConfig.API_BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson)) .addConverterFactory(GsonConverterFactory.create(gson))
.client(client)
.build() .build()
} }
} }

View File

@@ -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()
}
}

View File

@@ -1,19 +1,6 @@
package kr.smartsoltech.wellshe.domain.model package kr.smartsoltech.wellshe.domain.model
data class AppSettings( data class AppSettings(
val id: Long = 0, val notificationsEnabled: Boolean = true,
val isWaterReminderEnabled: Boolean = true, val darkModeEnabled: Boolean = false
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
) )

View File

@@ -13,7 +13,6 @@ data class User(
val dailyWaterGoal: Float = 2.5f, // в литрах val dailyWaterGoal: Float = 2.5f, // в литрах
val dailyStepsGoal: Int = 10000, val dailyStepsGoal: Int = 10000,
val dailyCaloriesGoal: Int = 2000, val dailyCaloriesGoal: Int = 2000,
val dailySleepGoal: Float = 8.0f, // в часах
val cycleLength: Int = 28, // дней val cycleLength: Int = 28, // дней
val periodLength: Int = 5, // дней val periodLength: Int = 5, // дней
val lastPeriodStart: LocalDate? = null, val lastPeriodStart: LocalDate? = null,

View File

@@ -83,13 +83,6 @@ fun DashboardScreen(
) )
} }
item {
SleepCard(
sleepData = uiState.sleepData,
onClick = { onNavigate("sleep") }
)
}
item { item {
RecentWorkoutsCard( RecentWorkoutsCard(
workouts = uiState.recentWorkouts, workouts = uiState.recentWorkouts,
@@ -404,26 +397,26 @@ private fun HealthOverviewCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly 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( HealthMetric(
label = "Энергия", label = "Энергия",
value = "${healthData.energyLevel}", value = "${healthData.energyLevel}",
unit = "/10", unit = "/10",
icon = Icons.Default.Battery6Bar 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 @Composable
private fun RecentWorkoutsCard( private fun RecentWorkoutsCard(
workouts: List<WorkoutData>, workouts: List<WorkoutData>,
@@ -605,7 +541,7 @@ private fun WorkoutItem(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
imageVector = getWorkoutIcon(workout.type), imageVector = Icons.Default.FitnessCenter,
contentDescription = null, contentDescription = null,
tint = PrimaryPink, tint = PrimaryPink,
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
@@ -617,7 +553,7 @@ private fun WorkoutItem(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
Text( Text(
text = getWorkoutTypeText(workout.type), text = workout.name,
style = MaterialTheme.typography.bodyMedium.copy( style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = TextPrimary color = TextPrimary
@@ -662,12 +598,12 @@ private val quickActions = listOf(
textColor = SecondaryBlue textColor = SecondaryBlue
), ),
QuickAction( QuickAction(
title = "Отметить сон", title = "Экстренная помощь",
icon = Icons.Default.Bedtime, icon = Icons.Default.Emergency,
route = "sleep", route = "emergency",
backgroundColor = AccentPurpleLight, backgroundColor = ErrorRedLight,
iconColor = AccentPurple, iconColor = ErrorRed,
textColor = AccentPurple textColor = ErrorRed
) )
) )

View File

@@ -8,19 +8,14 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.launch 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.data.repository.WellSheRepository
import kr.smartsoltech.wellshe.domain.model.* import kr.smartsoltech.wellshe.domain.model.*
import javax.inject.Inject import javax.inject.Inject
import java.time.LocalDate import java.time.LocalDate
import java.time.temporal.ChronoUnit
data class DashboardUiState( data class DashboardUiState(
val user: User = User(), val user: User = User(),
val todayHealth: HealthData = HealthData(), val todayHealth: HealthData = HealthData(),
val sleepData: SleepData = SleepData(),
val cycleData: CycleData = CycleData(), val cycleData: CycleData = CycleData(),
val recentWorkouts: List<WorkoutData> = emptyList(), val recentWorkouts: List<WorkoutData> = emptyList(),
val todaySteps: Int = 0, val todaySteps: Int = 0,
@@ -53,37 +48,13 @@ class DashboardViewModel @Inject constructor(
_uiState.value = _uiState.value.copy(user = user) _uiState.value = _uiState.value.copy(user = user)
} }
// Загружаем данные о здоровье // TODO: Временно используем заглушки для данных о здоровье
repository.getTodayHealthData().catch { val healthData = HealthData()
// Игнорируем ошибки, используем дефолтные данные
}.collect { healthEntity: HealthRecordEntity? ->
val healthData = healthEntity?.let { convertHealthEntityToModel(it) } ?: HealthData()
_uiState.value = _uiState.value.copy(todayHealth = healthData) _uiState.value = _uiState.value.copy(todayHealth = healthData)
}
// Загружаем данные о сне // TODO: Временно используем заглушки для данных о цикле
loadSleepData() val cycleData = CycleData()
// Загружаем данные о цикле
repository.getRecentPeriods().let { periods ->
val cycleEntity = periods.firstOrNull()
val cycleData = cycleEntity?.let { convertCycleEntityToModel(it) } ?: CycleData()
_uiState.value = _uiState.value.copy(cycleData = 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()
_uiState.value = _uiState.value.copy(isLoading = false) _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() { fun clearError() {
_uiState.value = _uiState.value.copy(error = null) _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()
)

View File

@@ -93,9 +93,7 @@ fun HealthOverviewScreen(
TodayHealthCard( TodayHealthCard(
uiState = uiState, uiState = uiState,
onUpdateVitals = viewModel::updateVitals, onUpdateVitals = viewModel::updateVitals,
onUpdateMood = viewModel::updateMood, onUpdateEnergy = viewModel::updateEnergyLevel
onUpdateEnergy = viewModel::updateEnergyLevel,
onUpdateStress = viewModel::updateStressLevel
) )
} }
@@ -133,9 +131,7 @@ fun HealthOverviewScreen(
private fun TodayHealthCard( private fun TodayHealthCard(
uiState: HealthUiState, uiState: HealthUiState,
onUpdateVitals: (Float?, Int?, Int?, Int?, Float?) -> Unit, onUpdateVitals: (Float?, Int?, Int?, Int?, Float?) -> Unit,
onUpdateMood: (String) -> Unit,
onUpdateEnergy: (Int) -> Unit, onUpdateEnergy: (Int) -> Unit,
onUpdateStress: (Int) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
var weight by remember { mutableStateOf(uiState.todayRecord?.weight?.toString() ?: "") } var weight by remember { mutableStateOf(uiState.todayRecord?.weight?.toString() ?: "") }
@@ -269,16 +265,7 @@ private fun TodayHealthCard(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// Настроение // Уровень энергии
MoodSection(
currentMood = uiState.todayRecord?.mood ?: "neutral",
onMoodChange = onUpdateMood,
isEditMode = uiState.isEditMode
)
Spacer(modifier = Modifier.height(16.dp))
// Уровень энергии и стресса
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
@@ -288,18 +275,9 @@ private fun TodayHealthCard(
value = uiState.todayRecord?.energyLevel ?: 5, value = uiState.todayRecord?.energyLevel ?: 5,
onValueChange = onUpdateEnergy, onValueChange = onUpdateEnergy,
isEditMode = uiState.isEditMode, isEditMode = uiState.isEditMode,
modifier = Modifier.weight(1f), modifier = Modifier.fillMaxWidth(),
color = WarningOrange 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 @Composable
private fun LevelSlider( private fun LevelSlider(
label: String, label: String,
@@ -679,18 +595,8 @@ private fun NotesCard(
} }
// Данные для UI // Данные для 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( private val healthSymptoms = listOf(
"Головная боль", "Усталость", "Тошнота", "Головокружение", "Головная боль", "Усталость", "Тошнота", "Головокружение",
"Боль в спине", "Боль в суставах", "Бессонница", "Стресс", "Боль в спине", "Боль в суставах", "Бессонница",
"Простуда", "Аллергия", "Боль в животе", "Другое" "Простуда", "Аллергия", "Боль в животе", "Другое"
) )

View File

@@ -248,9 +248,7 @@ private fun VitalSignsCard(
bloodPressureS = 0, bloodPressureS = 0,
bloodPressureD = 0, bloodPressureD = 0,
temperature = 36.6f, temperature = 36.6f,
mood = "",
energyLevel = 5, energyLevel = 5,
stressLevel = 5,
symptoms = emptyList(), symptoms = emptyList(),
notes = "" notes = ""
) )
@@ -275,9 +273,7 @@ private fun VitalSignsCard(
bloodPressureS = 0, bloodPressureS = 0,
bloodPressureD = 0, bloodPressureD = 0,
temperature = 36.6f, temperature = 36.6f,
mood = "",
energyLevel = 5, energyLevel = 5,
stressLevel = 5,
symptoms = emptyList(), symptoms = emptyList(),
notes = "" notes = ""
) )
@@ -305,9 +301,7 @@ private fun VitalSignsCard(
bloodPressureS = 0, bloodPressureS = 0,
bloodPressureD = 0, bloodPressureD = 0,
temperature = 36.6f, temperature = 36.6f,
mood = "",
energyLevel = 5, energyLevel = 5,
stressLevel = 5,
symptoms = emptyList(), symptoms = emptyList(),
notes = "" notes = ""
) )
@@ -333,9 +327,7 @@ private fun VitalSignsCard(
bloodPressureS = 0, bloodPressureS = 0,
bloodPressureD = 0, bloodPressureD = 0,
temperature = 36.6f, temperature = 36.6f,
mood = "",
energyLevel = 5, energyLevel = 5,
stressLevel = 5,
symptoms = emptyList(), symptoms = emptyList(),
notes = "" notes = ""
) )

View File

@@ -37,30 +37,41 @@ class HealthViewModel @Inject constructor(
_uiState.value = _uiState.value.copy(isLoading = true) _uiState.value = _uiState.value.copy(isLoading = true)
try { try {
// Загружаем данные о здоровье за сегодня // TODO: Временно используем заглушки, пока не добавим методы в repository
repository.getTodayHealthData().collect { todayRecord: HealthRecordEntity? ->
_uiState.value = _uiState.value.copy( _uiState.value = _uiState.value.copy(
todayRecord = todayRecord, todayRecord = null,
lastUpdateDate = todayRecord?.date, lastUpdateDate = null,
todaySymptoms = todayRecord?.symptoms ?: emptyList(), todaySymptoms = emptyList(),
todayNotes = todayRecord?.notes ?: "", todayNotes = "",
recentRecords = emptyList(),
weeklyWeights = emptyMap(),
isLoading = false 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> -> // repository.getAllHealthRecords().collect { records: List<HealthRecordEntity> ->
val weightsMap = records // val weightsMap = records
.filter { it.weight != null && it.weight > 0f } // .filter { it.weight != null && it.weight > 0f }
.groupBy { it.date } // .groupBy { it.date }
.mapValues { entry -> entry.value.last().weight ?: 0f } // .mapValues { entry -> entry.value.last().weight ?: 0f }
_uiState.value = _uiState.value.copy(weeklyWeights = weightsMap) // _uiState.value = _uiState.value.copy(weeklyWeights = weightsMap)
} // }
// Загружаем последние записи // Загружаем последние записи
repository.getRecentHealthRecords().collect { records: List<HealthRecordEntity> -> // repository.getRecentHealthRecords().collect { records: List<HealthRecordEntity> ->
_uiState.value = _uiState.value.copy(recentRecords = records) // _uiState.value = _uiState.value.copy(recentRecords = records)
} // }
} catch (e: Exception) { } catch (e: Exception) {
_uiState.value = _uiState.value.copy( _uiState.value = _uiState.value.copy(
@@ -91,42 +102,13 @@ class HealthViewModel @Inject constructor(
bloodPressureS = bpSystolic, bloodPressureS = bpSystolic,
bloodPressureD = bpDiastolic, bloodPressureD = bpDiastolic,
temperature = temperature, temperature = temperature,
mood = "",
energyLevel = 5, energyLevel = 5,
stressLevel = 5,
symptoms = emptyList(), symptoms = emptyList(),
notes = "" notes = ""
) )
} }
repository.saveHealthRecord(updatedRecord) // TODO: Добавить метод saveHealthRecord в repository
} catch (e: Exception) { // repository.saveHealthRecord(updatedRecord)
_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)
} catch (e: Exception) { } catch (e: Exception) {
_uiState.value = _uiState.value.copy(error = e.message) _uiState.value = _uiState.value.copy(error = e.message)
} }
@@ -142,47 +124,18 @@ class HealthViewModel @Inject constructor(
} else { } else {
HealthRecordEntity( HealthRecordEntity(
date = LocalDate.now(), date = LocalDate.now(),
weight = 0f, weight = null,
heartRate = 0, heartRate = null,
bloodPressureS = 0, bloodPressureS = null,
bloodPressureD = 0, bloodPressureD = null,
temperature = 36.6f, temperature = null,
mood = "",
energyLevel = energy, energyLevel = energy,
stressLevel = 5,
symptoms = emptyList(), symptoms = emptyList(),
notes = "" notes = ""
) )
} }
repository.saveHealthRecord(updatedRecord) // TODO: Добавить метод saveHealthRecord в repository
} catch (e: Exception) { // repository.saveHealthRecord(updatedRecord)
_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)
} catch (e: Exception) { } catch (e: Exception) {
_uiState.value = _uiState.value.copy(error = e.message) _uiState.value = _uiState.value.copy(error = e.message)
} }
@@ -199,19 +152,18 @@ class HealthViewModel @Inject constructor(
} else { } else {
HealthRecordEntity( HealthRecordEntity(
date = LocalDate.now(), date = LocalDate.now(),
weight = 0f, weight = null,
heartRate = 0, heartRate = null,
bloodPressureS = 0, bloodPressureS = null,
bloodPressureD = 0, bloodPressureD = null,
temperature = 36.6f, temperature = null,
mood = "",
energyLevel = 5, energyLevel = 5,
stressLevel = 5,
symptoms = symptoms, symptoms = symptoms,
notes = "" notes = ""
) )
} }
repository.saveHealthRecord(updatedRecord) // TODO: Добавить метод saveHealthRecord в repository
// repository.saveHealthRecord(updatedRecord)
} catch (e: Exception) { } catch (e: Exception) {
_uiState.value = _uiState.value.copy(error = e.message) _uiState.value = _uiState.value.copy(error = e.message)
} }
@@ -228,19 +180,18 @@ class HealthViewModel @Inject constructor(
} else { } else {
HealthRecordEntity( HealthRecordEntity(
date = LocalDate.now(), date = LocalDate.now(),
weight = 0f, weight = null,
heartRate = 0, heartRate = null,
bloodPressureS = 0, bloodPressureS = null,
bloodPressureD = 0, bloodPressureD = null,
temperature = 36.6f, temperature = null,
mood = "",
energyLevel = 5, energyLevel = 5,
stressLevel = 5,
symptoms = emptyList(), symptoms = emptyList(),
notes = notes notes = notes
) )
} }
repository.saveHealthRecord(updatedRecord) // TODO: Добавить метод saveHealthRecord в repository
// repository.saveHealthRecord(updatedRecord)
} catch (e: Exception) { } catch (e: Exception) {
_uiState.value = _uiState.value.copy(error = e.message) _uiState.value = _uiState.value.copy(error = e.message)
} }
@@ -250,7 +201,8 @@ class HealthViewModel @Inject constructor(
fun deleteHealthRecord(record: HealthRecordEntity) { fun deleteHealthRecord(record: HealthRecordEntity) {
viewModelScope.launch { viewModelScope.launch {
try { try {
repository.deleteHealthRecord(record.id) // TODO: Добавить метод deleteHealthRecord в repository
// repository.deleteHealthRecord(record.id)
} catch (e: Exception) { } catch (e: Exception) {
_uiState.value = _uiState.value.copy(error = e.message) _uiState.value = _uiState.value.copy(error = e.message)
} }

View File

@@ -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()
}
}
}

View File

@@ -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
}
}

View File

@@ -8,12 +8,11 @@ import androidx.navigation.compose.composable
import kr.smartsoltech.wellshe.ui.analytics.AnalyticsScreen import kr.smartsoltech.wellshe.ui.analytics.AnalyticsScreen
import kr.smartsoltech.wellshe.ui.body.BodyScreen import kr.smartsoltech.wellshe.ui.body.BodyScreen
import kr.smartsoltech.wellshe.ui.cycle.CycleScreen 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.profile.ProfileScreen
import kr.smartsoltech.wellshe.ui.auth.AuthViewModel import kr.smartsoltech.wellshe.ui.auth.AuthViewModel
import kr.smartsoltech.wellshe.ui.auth.compose.LoginScreen import kr.smartsoltech.wellshe.ui.auth.compose.LoginScreen
import kr.smartsoltech.wellshe.ui.auth.compose.RegisterScreen import kr.smartsoltech.wellshe.ui.auth.compose.RegisterScreen
import kr.smartsoltech.wellshe.ui.emergency.EmergencyScreen
@Composable @Composable
fun AppNavGraph( fun AppNavGraph(
@@ -57,15 +56,6 @@ fun AppNavGraph(
) )
} }
// Экран экстренной помощи
composable("emergency") {
EmergencyScreen(
onNavigateBack = {
navController.popBackStack()
}
)
}
// Существующие экраны // Существующие экраны
composable(BottomNavItem.Cycle.route) { composable(BottomNavItem.Cycle.route) {
CycleScreen( CycleScreen(
@@ -91,8 +81,12 @@ fun AppNavGraph(
BodyScreen() BodyScreen()
} }
composable(BottomNavItem.Mood.route) { composable(BottomNavItem.Emergency.route) {
MoodScreen() EmergencyScreen(
onNavigateBack = {
navController.popBackStack()
}
)
} }
composable(BottomNavItem.Analytics.route) { composable(BottomNavItem.Analytics.route) {

View File

@@ -2,14 +2,14 @@ package kr.smartsoltech.wellshe.ui.navigation
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BarChart 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.Person
import androidx.compose.material.icons.filled.WaterDrop import androidx.compose.material.icons.filled.WaterDrop
import androidx.compose.material.icons.filled.WbSunny import androidx.compose.material.icons.filled.WbSunny
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
/** /**
* Модель навигационного элемента для нижней панели навигац<EFBFBD><EFBFBD>и * Модель навигационного элемента для нижней панели навигации
*/ */
sealed class BottomNavItem( sealed class BottomNavItem(
val route: String, val route: String,
@@ -28,10 +28,10 @@ sealed class BottomNavItem(
icon = Icons.Default.WaterDrop icon = Icons.Default.WaterDrop
) )
data object Mood : BottomNavItem( data object Emergency : BottomNavItem(
route = "mood", route = "emergency",
title = "Настроение", title = "Экстренное",
icon = Icons.Default.Favorite icon = Icons.Default.Emergency
) )
data object Analytics : BottomNavItem( data object Analytics : BottomNavItem(
@@ -47,6 +47,6 @@ sealed class BottomNavItem(
) )
companion object { companion object {
val items = listOf(Cycle, Body, Mood, Analytics, Profile) val items = listOf(Cycle, Body, Emergency, Analytics, Profile)
} }
} }

View File

@@ -49,7 +49,7 @@ fun BottomNavigation(
val backgroundColor = when (item) { val backgroundColor = when (item) {
BottomNavItem.Cycle -> CycleTabColor BottomNavItem.Cycle -> CycleTabColor
BottomNavItem.Body -> BodyTabColor BottomNavItem.Body -> BodyTabColor
BottomNavItem.Mood -> MoodTabColor BottomNavItem.Emergency -> ErrorRed
BottomNavItem.Analytics -> AnalyticsTabColor BottomNavItem.Analytics -> AnalyticsTabColor
BottomNavItem.Profile -> ProfileTabColor BottomNavItem.Profile -> ProfileTabColor
} }

View File

@@ -59,10 +59,8 @@ fun SettingsScreen(
NotificationSettingsCard( NotificationSettingsCard(
isWaterReminderEnabled = uiState.isWaterReminderEnabled, isWaterReminderEnabled = uiState.isWaterReminderEnabled,
isCycleReminderEnabled = uiState.isCycleReminderEnabled, isCycleReminderEnabled = uiState.isCycleReminderEnabled,
isSleepReminderEnabled = uiState.isSleepReminderEnabled, onWaterReminderToggle = viewModel::updateWaterReminder,
onWaterReminderToggle = viewModel::toggleWaterReminder, onCycleReminderToggle = viewModel::updateCycleReminder
onCycleReminderToggle = viewModel::toggleCycleReminder,
onSleepReminderToggle = viewModel::toggleSleepReminder
) )
} }
@@ -79,24 +77,22 @@ fun SettingsScreen(
GoalsSettingsCard( GoalsSettingsCard(
waterGoal = uiState.waterGoal, waterGoal = uiState.waterGoal,
stepsGoal = uiState.stepsGoal, stepsGoal = uiState.stepsGoal,
sleepGoal = uiState.sleepGoal,
onWaterGoalChange = viewModel::updateWaterGoal, onWaterGoalChange = viewModel::updateWaterGoal,
onStepsGoalChange = viewModel::updateStepsGoal, onStepsGoalChange = viewModel::updateStepsGoal
onSleepGoalChange = viewModel::updateSleepGoal
) )
} }
item { item {
AppearanceSettingsCard( AppearanceSettingsCard(
isDarkTheme = uiState.isDarkTheme, isDarkTheme = uiState.isDarkTheme,
onThemeToggle = viewModel::toggleTheme onThemeToggle = viewModel::updateTheme
) )
} }
item { item {
DataManagementCard( DataManagementCard(
onExportData = viewModel::exportData, onExportData = viewModel::exportData,
onImportData = viewModel::importData, onImportData = { viewModel.importData(it) },
onClearData = viewModel::clearAllData onClearData = viewModel::clearAllData
) )
} }
@@ -155,10 +151,8 @@ private fun SettingsHeader(
private fun NotificationSettingsCard( private fun NotificationSettingsCard(
isWaterReminderEnabled: Boolean, isWaterReminderEnabled: Boolean,
isCycleReminderEnabled: Boolean, isCycleReminderEnabled: Boolean,
isSleepReminderEnabled: Boolean,
onWaterReminderToggle: (Boolean) -> Unit, onWaterReminderToggle: (Boolean) -> Unit,
onCycleReminderToggle: (Boolean) -> Unit, onCycleReminderToggle: (Boolean) -> Unit,
onSleepReminderToggle: (Boolean) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
SettingsCard( SettingsCard(
@@ -181,15 +175,6 @@ private fun NotificationSettingsCard(
isChecked = isCycleReminderEnabled, isChecked = isCycleReminderEnabled,
onCheckedChange = onCycleReminderToggle 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( private fun GoalsSettingsCard(
waterGoal: Float, waterGoal: Float,
stepsGoal: Int, stepsGoal: Int,
sleepGoal: Float,
onWaterGoalChange: (Float) -> Unit, onWaterGoalChange: (Float) -> Unit,
onStepsGoalChange: (Int) -> Unit, onStepsGoalChange: (Int) -> Unit,
onSleepGoalChange: (Float) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
SettingsCard( SettingsCard(
@@ -266,18 +249,6 @@ private fun GoalsSettingsCard(
}, },
suffix = "шагов" 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 @Composable
private fun DataManagementCard( private fun DataManagementCard(
onExportData: () -> Unit, onExportData: () -> Unit,
onImportData: () -> Unit, onImportData: (String) -> Unit,
onClearData: () -> Unit, onClearData: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@@ -326,7 +297,7 @@ private fun DataManagementCard(
title = "Импорт данных", title = "Импорт данных",
subtitle = "Загрузить данные из файла", subtitle = "Загрузить данные из файла",
icon = Icons.Default.Upload, icon = Icons.Default.Upload,
onClick = onImportData onClick = { onImportData("") }
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))

View File

@@ -14,12 +14,10 @@ import javax.inject.Inject
data class SettingsUiState( data class SettingsUiState(
val isWaterReminderEnabled: Boolean = true, val isWaterReminderEnabled: Boolean = true,
val isCycleReminderEnabled: Boolean = true, val isCycleReminderEnabled: Boolean = true,
val isSleepReminderEnabled: Boolean = true,
val cycleLength: Int = 28, val cycleLength: Int = 28,
val periodLength: Int = 5, val periodLength: Int = 5,
val waterGoal: Float = 2.5f, val waterGoal: Float = 2.5f,
val stepsGoal: Int = 10000, val stepsGoal: Int = 10000,
val sleepGoal: Float = 8.0f,
val isDarkTheme: Boolean = false, val isDarkTheme: Boolean = false,
val isLoading: Boolean = false, val isLoading: Boolean = false,
val error: String? = null val error: String? = null
@@ -38,23 +36,17 @@ class SettingsViewModel @Inject constructor(
_uiState.value = _uiState.value.copy(isLoading = true) _uiState.value = _uiState.value.copy(isLoading = true)
try { try {
repository.getSettings().catch { e -> // TODO: Временно используем заглушки до реализации методов в repository
repository.getAppSettings().catch { e ->
_uiState.value = _uiState.value.copy( _uiState.value = _uiState.value.copy(
isLoading = false, isLoading = false,
error = e.message error = e.message
) )
}.collect { settings -> }.collect { settings ->
_uiState.value = _uiState.value.copy( _uiState.value = _uiState.value.copy(
isWaterReminderEnabled = settings.isWaterReminderEnabled, isDarkTheme = settings.darkModeEnabled,
isCycleReminderEnabled = settings.isCycleReminderEnabled, isLoading = false,
isSleepReminderEnabled = settings.isSleepReminderEnabled, error = null
cycleLength = settings.cycleLength,
periodLength = settings.periodLength,
waterGoal = settings.waterGoal,
stepsGoal = settings.stepsGoal,
sleepGoal = settings.sleepGoal,
isDarkTheme = settings.isDarkTheme,
isLoading = false
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -66,11 +58,11 @@ class SettingsViewModel @Inject constructor(
} }
} }
// Уведомления // Обновление настроек уведомлений
fun toggleWaterReminder(enabled: Boolean) { fun updateWaterReminder(enabled: Boolean) {
viewModelScope.launch { viewModelScope.launch {
try { try {
repository.updateWaterReminderSetting(enabled) // TODO: Реализовать через repository
_uiState.value = _uiState.value.copy(isWaterReminderEnabled = enabled) _uiState.value = _uiState.value.copy(isWaterReminderEnabled = enabled)
} catch (e: Exception) { } catch (e: Exception) {
_uiState.value = _uiState.value.copy(error = e.message) _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 { viewModelScope.launch {
try { try {
repository.updateCycleReminderSetting(enabled) // TODO: Реализовать через repository
_uiState.value = _uiState.value.copy(isCycleReminderEnabled = enabled) _uiState.value = _uiState.value.copy(isCycleReminderEnabled = enabled)
} catch (e: Exception) { } catch (e: Exception) {
_uiState.value = _uiState.value.copy(error = e.message) _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) { fun updateCycleLength(length: Int) {
if (length in 21..35) { if (length in 21..35) {
viewModelScope.launch { viewModelScope.launch {
try { try {
repository.updateCycleLength(length) // TODO: Реализовать через repository
_uiState.value = _uiState.value.copy(cycleLength = length) _uiState.value = _uiState.value.copy(cycleLength = length)
} catch (e: Exception) { } catch (e: Exception) {
_uiState.value = _uiState.value.copy(error = e.message) _uiState.value = _uiState.value.copy(error = e.message)
@@ -115,10 +96,10 @@ class SettingsViewModel @Inject constructor(
} }
fun updatePeriodLength(length: Int) { fun updatePeriodLength(length: Int) {
if (length in 3..8) { if (length in 3..7) {
viewModelScope.launch { viewModelScope.launch {
try { try {
repository.updatePeriodLength(length) // TODO: Реализовать через repository
_uiState.value = _uiState.value.copy(periodLength = length) _uiState.value = _uiState.value.copy(periodLength = length)
} catch (e: Exception) { } catch (e: Exception) {
_uiState.value = _uiState.value.copy(error = e.message) _uiState.value = _uiState.value.copy(error = e.message)
@@ -127,12 +108,12 @@ class SettingsViewModel @Inject constructor(
} }
} }
// Цели // Обновление целей
fun updateWaterGoal(goal: Float) { fun updateWaterGoal(goal: Float) {
if (goal in 1.5f..4.0f) { if (goal > 0) {
viewModelScope.launch { viewModelScope.launch {
try { try {
repository.updateWaterGoal(goal) // TODO: Реализовать через repository
_uiState.value = _uiState.value.copy(waterGoal = goal) _uiState.value = _uiState.value.copy(waterGoal = goal)
} catch (e: Exception) { } catch (e: Exception) {
_uiState.value = _uiState.value.copy(error = e.message) _uiState.value = _uiState.value.copy(error = e.message)
@@ -142,10 +123,10 @@ class SettingsViewModel @Inject constructor(
} }
fun updateStepsGoal(goal: Int) { fun updateStepsGoal(goal: Int) {
if (goal in 5000..20000) { if (goal > 0) {
viewModelScope.launch { viewModelScope.launch {
try { try {
repository.updateStepsGoal(goal) // TODO: Реализовать через repository
_uiState.value = _uiState.value.copy(stepsGoal = goal) _uiState.value = _uiState.value.copy(stepsGoal = goal)
} catch (e: Exception) { } catch (e: Exception) {
_uiState.value = _uiState.value.copy(error = e.message) _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) { fun updateTheme(isDark: Boolean) {
viewModelScope.launch { viewModelScope.launch {
try { try {
repository.updateSleepGoal(goal) // TODO: Реализовать через repository
_uiState.value = _uiState.value.copy(sleepGoal = goal)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(error = e.message)
}
}
}
}
// Внешний вид
fun toggleTheme(isDark: Boolean) {
viewModelScope.launch {
try {
repository.updateThemeSetting(isDark)
_uiState.value = _uiState.value.copy(isDarkTheme = isDark) _uiState.value = _uiState.value.copy(isDarkTheme = isDark)
} catch (e: Exception) { } catch (e: Exception) {
_uiState.value = _uiState.value.copy(error = e.message) _uiState.value = _uiState.value.copy(error = e.message)
@@ -179,35 +147,33 @@ class SettingsViewModel @Inject constructor(
} }
} }
// Управление данными // Экспорт данных
fun exportData() { fun exportData() {
viewModelScope.launch { viewModelScope.launch {
try { try {
repository.exportUserData() // TODO: Реализовать экспорт данных
// Показать сообщение об успехе
} catch (e: Exception) { } catch (e: Exception) {
_uiState.value = _uiState.value.copy(error = e.message) _uiState.value = _uiState.value.copy(error = e.message)
} }
} }
} }
fun importData() { // Импорт данных
fun importData(data: String) {
viewModelScope.launch { viewModelScope.launch {
try { try {
repository.importUserData() // TODO: Реализовать импорт данных
loadSettings() // Перезагрузить настройки
} catch (e: Exception) { } catch (e: Exception) {
_uiState.value = _uiState.value.copy(error = e.message) _uiState.value = _uiState.value.copy(error = e.message)
} }
} }
} }
// Очистка данных
fun clearAllData() { fun clearAllData() {
viewModelScope.launch { viewModelScope.launch {
try { try {
repository.clearAllUserData() // TODO: Реализовать очистку данных
// Сбросить на дефолтные значения
_uiState.value = SettingsUiState()
} catch (e: Exception) { } catch (e: Exception) {
_uiState.value = _uiState.value.copy(error = e.message) _uiState.value = _uiState.value.copy(error = e.message)
} }

View File

@@ -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
)
}
}

View File

@@ -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 // Возвращаем значение по умолчанию
}
}

View File

@@ -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)
}
}

View File

@@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<network-security-config> <network-security-config>
<!-- Разрешаем незащищенное HTTP-соединение с IP-адресом 192.168.0.112 --> <!-- Разрешаем незащищенное HTTP-соединение -->
<domain-config cleartextTrafficPermitted="true"> <domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">192.168.0.112</domain> <domain includeSubdomains="true">192.168.0.112</domain>
<domain includeSubdomains="true">192.168.219.108</domain>
</domain-config> </domain-config>
<!-- Настройки по умолчанию - запрещаем незащищенный HTTP-трафик для других адресов --> <!-- Настройки по умолчанию - запрещаем незащищенный HTTP-трафик для других адресов -->

View File

@@ -15,8 +15,7 @@ class CycleAnalyticsTest {
endDate = LocalDate.now().minusDays(23), endDate = LocalDate.now().minusDays(23),
cycleLength = 28, cycleLength = 28,
flow = "medium", flow = "medium",
symptoms = emptyList(), symptoms = emptyList()
mood = "neutral"
), ),
CyclePeriodEntity( CyclePeriodEntity(
id = 1, id = 1,
@@ -24,8 +23,7 @@ class CycleAnalyticsTest {
endDate = LocalDate.now().minusDays(51), endDate = LocalDate.now().minusDays(51),
cycleLength = 28, cycleLength = 28,
flow = "medium", flow = "medium",
symptoms = emptyList(), symptoms = emptyList()
mood = "neutral"
) )
) )
@@ -44,8 +42,7 @@ class CycleAnalyticsTest {
endDate = LocalDate.now().minusDays(23), endDate = LocalDate.now().minusDays(23),
cycleLength = 28, cycleLength = 28,
flow = "medium", flow = "medium",
symptoms = emptyList(), symptoms = emptyList()
mood = "neutral"
), ),
CyclePeriodEntity( CyclePeriodEntity(
id = 1, id = 1,
@@ -53,8 +50,7 @@ class CycleAnalyticsTest {
endDate = LocalDate.now().minusDays(51), endDate = LocalDate.now().minusDays(51),
cycleLength = 28, cycleLength = 28,
flow = "medium", flow = "medium",
symptoms = emptyList(), symptoms = emptyList()
mood = "neutral"
) )
) )
@@ -72,8 +68,7 @@ class CycleAnalyticsTest {
endDate = LocalDate.now().minusDays(23), endDate = LocalDate.now().minusDays(23),
cycleLength = 28, cycleLength = 28,
flow = "medium", flow = "medium",
symptoms = emptyList(), symptoms = emptyList()
mood = "neutral"
) )
) )

View File

@@ -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)
}
}

View File

@@ -21,3 +21,5 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies, # resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library # 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/

View File

@@ -10,6 +10,7 @@ activityCompose = "1.8.2"
composeBom = "2024.02.02" composeBom = "2024.02.02"
hilt = "2.48" hilt = "2.48"
composeCompiler = "1.5.14" composeCompiler = "1.5.14"
material = "1.13.0"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } 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" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", 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] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }