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

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 {
@@ -48,6 +50,7 @@ android {
buildFeatures {
compose = true
viewBinding = true
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.14"
@@ -65,6 +68,7 @@ dependencies {
implementation(libs.androidx.compose.material3)
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
implementation(libs.hilt.android)
implementation(libs.material)
kapt(libs.hilt.compiler)
implementation("androidx.room:room-runtime:2.6.1")
kapt("androidx.room:room-compiler:2.6.1")

View File

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

View File

@@ -5,27 +5,6 @@ import kotlinx.coroutines.flow.Flow
import kr.smartsoltech.wellshe.data.entity.*
import java.time.LocalDate
@Dao
interface SleepLogDao {
@Query("SELECT * FROM sleep_logs WHERE date = :date")
suspend fun getSleepForDate(date: LocalDate): SleepLogEntity?
@Query("SELECT * FROM sleep_logs ORDER BY date DESC LIMIT 7")
fun getRecentSleepLogs(): Flow<List<SleepLogEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertSleepLog(sleepLog: SleepLogEntity)
@Update
suspend fun updateSleepLog(sleepLog: SleepLogEntity)
@Delete
suspend fun deleteSleepLog(sleepLog: SleepLogEntity)
@Query("SELECT * FROM sleep_logs WHERE date BETWEEN :startDate AND :endDate ORDER BY date DESC")
fun getSleepLogsForPeriod(startDate: LocalDate, endDate: LocalDate): Flow<List<SleepLogEntity>>
}
@Dao
interface WorkoutDao {
@Query("SELECT * FROM workouts WHERE date = :date ORDER BY id DESC")

View File

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

View File

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

View File

@@ -13,18 +13,6 @@ data class WaterLogEntity(
val timestamp: Long = System.currentTimeMillis()
)
@Entity(tableName = "sleep_logs")
data class SleepLogEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val date: LocalDate,
val bedTime: String, // HH:mm
val wakeTime: String, // HH:mm
val duration: Float, // часы
val quality: String = "good", // poor, fair, good, excellent
val notes: String = ""
)
@Entity(tableName = "workouts")
data class WorkoutEntity(
@PrimaryKey(autoGenerate = true)
@@ -76,5 +64,10 @@ data class UserProfileEntity(
val cycleLength: Int = 28,
val periodLength: Int = 5,
val lastPeriodDate: LocalDate? = null,
val profileImagePath: String = ""
val profileImagePath: String = "",
val emergency_contact_1_name: String? = null,
val emergency_contact_1_phone: String? = null,
val emergency_contact_2_name: String? = null,
val emergency_contact_2_phone: String? = null,
val emergency_notifications_enabled: Boolean? = false
)

View File

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

View File

@@ -12,7 +12,7 @@ import java.util.concurrent.TimeUnit
* Класс для настройки и создания API-клиентов
*/
object ApiClient {
private const val BASE_URL = "http://192.168.0.112:8000/api/v1/"
private const val BASE_URL = "http://192.168.219.108:8000/api/v1/"
private const val CONNECT_TIMEOUT = 15L
private const val READ_TIMEOUT = 15L
private const val WRITE_TIMEOUT = 15L

View File

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

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.WorkoutSession
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import javax.inject.Inject
import javax.inject.Singleton
@@ -21,7 +20,6 @@ import javax.inject.Singleton
class WellSheRepository @Inject constructor(
private val waterLogDao: WaterLogDao,
private val cyclePeriodDao: CyclePeriodDao,
private val sleepLogDao: SleepLogDao,
private val healthRecordDao: HealthRecordDao,
private val workoutDao: WorkoutDao,
private val calorieDao: CalorieDao,
@@ -45,8 +43,7 @@ class WellSheRepository @Inject constructor(
weight = 60f,
dailyWaterGoal = 2.5f,
dailyStepsGoal = 10000,
dailyCaloriesGoal = 2000,
dailySleepGoal = 8.0f
dailyCaloriesGoal = 2000
)
)
}
@@ -157,231 +154,89 @@ class WellSheRepository @Inject constructor(
// TODO: Реализовать окончание тренировки
}
// =================
// СОН
// =================
suspend fun getSleepForDate(date: LocalDate): SleepLogEntity? {
return sleepLogDao.getSleepForDate(date)
}
fun getRecentSleepLogs(): Flow<List<SleepLogEntity>> {
return sleepLogDao.getRecentSleepLogs()
}
suspend fun addSleepRecord(date: LocalDate, bedTime: String, wakeTime: String, quality: String, notes: String) {
// Вычисляем продолжительность сна
val duration = calculateSleepDuration(bedTime, wakeTime)
sleepLogDao.insertSleepLog(
SleepLogEntity(
date = date,
bedTime = bedTime,
wakeTime = wakeTime,
duration = duration,
quality = quality,
notes = notes
)
)
}
private fun calculateSleepDuration(bedTime: String, wakeTime: String): Float {
// TODO: Реализовать правильный расчет продолжительности сна
return 8.0f
}
// =================
// МЕНСТРУАЛЬНЫЙ ЦИКЛ
// =================
suspend fun addPeriod(startDate: LocalDate, endDate: LocalDate?, flow: String, symptoms: List<String>, mood: String) {
suspend fun addPeriod(startDate: LocalDate, endDate: LocalDate?, flow: String, symptoms: List<String>) {
val period = CyclePeriodEntity(
startDate = startDate,
endDate = endDate,
flow = flow,
symptoms = symptoms,
mood = mood
symptoms = symptoms
)
cyclePeriodDao.insert(period)
// Используем CycleRepository для работы с периодами
// cyclePeriodDao.insertPeriod(period)
// TODO: Добавить интеграцию с CycleRepository
}
suspend fun updatePeriod(periodId: Long, endDate: LocalDate?, flow: String, symptoms: List<String>, mood: String) {
val periods = cyclePeriodDao.getAll()
val existingPeriod = periods.firstOrNull { it.id == periodId }
if (existingPeriod != null) {
val updatedPeriod = existingPeriod.copy(
endDate = endDate,
flow = flow,
symptoms = symptoms,
mood = mood
)
cyclePeriodDao.update(updatedPeriod)
}
suspend fun updatePeriod(periodId: Long, endDate: LocalDate?, flow: String, symptoms: List<String>) {
// TODO: Реализовать через CycleRepository
// val existingPeriod = cyclePeriodDao.getPeriodById(periodId)
// existingPeriod?.let {
// val updatedPeriod = it.copy(
// endDate = endDate,
// flow = flow,
// symptoms = symptoms
// )
// cyclePeriodDao.updatePeriod(updatedPeriod)
// }
}
suspend fun getRecentPeriods(): List<CyclePeriodEntity> {
return cyclePeriodDao.getAll().take(6)
fun getPeriods(): Flow<List<CyclePeriodEntity>> {
// TODO: Реализовать через CycleRepository
return flowOf(emptyList())
// return cyclePeriodDao.getAllPeriods()
}
suspend fun deletePeriod(periodId: Long) {
// TODO: Реализовать через CycleRepository
// cyclePeriodDao.deletePeriodById(periodId)
}
// =================
// НАСТРОЙКИ
// =================
fun getSettings(): Flow<AppSettings> {
fun getAppSettings(): Flow<AppSettings> {
// TODO: Реализовать получение настроек из БД
return flowOf(
AppSettings(
isWaterReminderEnabled = true,
isCycleReminderEnabled = true,
isSleepReminderEnabled = true,
cycleLength = 28,
periodLength = 5,
waterGoal = 2.5f,
stepsGoal = 10000,
sleepGoal = 8.0f,
isDarkTheme = false
notificationsEnabled = true,
darkModeEnabled = false
)
)
}
suspend fun updateWaterReminderSetting(enabled: Boolean) {
// TODO: Реализовать обновление настройки напоминаний о воде
}
suspend fun updateCycleReminderSetting(enabled: Boolean) {
// TODO: Реализовать обновление настройки напоминаний о цикле
}
suspend fun updateSleepReminderSetting(enabled: Boolean) {
// TODO: Реализовать обновление настройки напоминаний о сне
}
suspend fun updateCycleLength(length: Int) {
// TODO: Реализовать обновление длины цикла
}
suspend fun updatePeriodLength(length: Int) {
// TODO: Реализовать обновление длины менструации
}
suspend fun updateStepsGoal(goal: Int) {
// TODO: Реализовать обновление цели по шагам
}
suspend fun updateSleepGoal(goal: Float) {
// TODO: Реализовать обновление цели по сну
}
suspend fun updateThemeSetting(isDark: Boolean) {
// TODO: Реализовать обновление темы
suspend fun updateAppSettings(settings: AppSettings) {
// TODO: Реализовать обновление настроек
}
// =================
// УПРАВЛЕНИЕ ДАННЫМИ
// АНАЛИТИКА И ОТЧЕТЫ
// =================
suspend fun exportUserData() {
// TODO: Реализовать экспорт данных пользователя
}
suspend fun importUserData() {
// TODO: Реализовать импорт данных пользователя
}
suspend fun clearAllUserData() {
// TODO: Реализовать очистку всех данных пользователя
}
// =================
// ЗДОРОВЬЕ
// =================
fun getTodayHealthData(): kotlinx.coroutines.flow.Flow<HealthRecordEntity?> {
val today = LocalDate.now()
return healthRecordDao.getByDateFlow(today)
}
fun getAllHealthRecords(): kotlinx.coroutines.flow.Flow<List<HealthRecordEntity>> {
return healthRecordDao.getAllFlow()
}
fun getRecentHealthRecords(limit: Int = 10): kotlinx.coroutines.flow.Flow<List<HealthRecordEntity>> {
return healthRecordDao.getAllFlow().map { records: List<HealthRecordEntity> ->
records.sortedByDescending { r -> r.date }.take(limit)
}
}
suspend fun saveHealthRecord(record: HealthRecordEntity) {
if (record.id != 0L) {
healthRecordDao.update(record)
} else {
healthRecordDao.insert(record)
}
}
suspend fun deleteHealthRecord(recordId: Long) {
val record = healthRecordDao.getAll().firstOrNull { it.id == recordId }
if (record != null) {
healthRecordDao.delete(record)
}
}
// =================
// DASHBOARD
// =================
fun getDashboardData(): Flow<DashboardData> {
// TODO: Реализовать получение данных для главного экрана
return flowOf(
DashboardData(
user = User(),
todayHealth = null,
sleepData = null,
cycleData = null,
recentWorkouts = emptyList()
)
)
}
// =================
// УСТАРЕВШИЕ МЕТОДЫ (для совместимости)
// =================
suspend fun addWater(amount: Int, date: LocalDate = LocalDate.now()) {
waterLogDao.insertWaterLog(
WaterLogEntity(date = date, amount = amount)
)
}
suspend fun getTodayWaterIntake(date: LocalDate = LocalDate.now()): Int {
return waterLogDao.getTotalWaterForDate(date) ?: 0
}
fun getWaterLogsForDate(date: LocalDate): Flow<List<WaterLogEntity>> {
fun getDashboardData(date: LocalDate): Flow<DashboardData> {
return flow {
emit(waterLogDao.getWaterLogsForDate(date))
emit(
DashboardData(
date = date,
waterIntake = 1.2f,
steps = 6500,
calories = 1850,
workouts = 1,
cycleDay = null
)
)
}
}
}
// Вспомогательные data классы
data class DashboardData(
val user: User,
val todayHealth: HealthRecord?,
val sleepData: SleepLogEntity?,
val cycleData: CyclePeriodEntity?,
val recentWorkouts: List<WorkoutSession>
)
data class HealthRecord(
val id: Long = 0,
val date: LocalDate,
val bloodPressureSystolic: Int = 0,
val bloodPressureDiastolic: Int = 0,
val heartRate: Int = 0,
val weight: Float = 0f,
val mood: String = "neutral", // Добавляем поле настроения
val energyLevel: Int = 5, // Добавляем уровень энергии (1-10)
val stressLevel: Int = 5, // Добавляем уровень стресса (1-10)
val notes: String = ""
val waterIntake: Float,
val steps: Int,
val calories: Int,
val workouts: Int,
val cycleDay: Int?
)

View File

@@ -8,21 +8,8 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kr.smartsoltech.wellshe.data.AppDatabase
import kr.smartsoltech.wellshe.data.datastore.DataStoreManager
import kr.smartsoltech.wellshe.data.dao.*
import kr.smartsoltech.wellshe.data.repo.DrinkLogger
import kr.smartsoltech.wellshe.data.repo.WeightRepository
import kr.smartsoltech.wellshe.data.repo.WorkoutService
import kr.smartsoltech.wellshe.data.MIGRATION_1_2
import kr.smartsoltech.wellshe.data.MIGRATION_2_3
import kr.smartsoltech.wellshe.data.MIGRATION_3_4
import kr.smartsoltech.wellshe.data.MIGRATION_4_5
import kr.smartsoltech.wellshe.data.MIGRATION_5_6
import kr.smartsoltech.wellshe.data.MIGRATION_6_7
import kr.smartsoltech.wellshe.data.MIGRATION_7_8
import kr.smartsoltech.wellshe.data.MIGRATION_8_9
import kr.smartsoltech.wellshe.data.MIGRATION_9_10
import kr.smartsoltech.wellshe.data.MIGRATION_10_11
import kr.smartsoltech.wellshe.data.repo.*
import javax.inject.Singleton
@Module
@@ -31,34 +18,18 @@ object AppModule {
@Provides
@Singleton
fun provideDataStoreManager(@ApplicationContext context: Context): DataStoreManager =
DataStoreManager(context)
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase =
Room.databaseBuilder(
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"well_she_db"
)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10, MIGRATION_10_11)
.fallbackToDestructiveMigration()
.build()
"wellshe_database"
).build()
}
// DAO providers
// DAO Providers
@Provides
fun provideWaterLogDao(database: AppDatabase): WaterLogDao = database.waterLogDao()
@Provides
fun provideCyclePeriodDao(database: AppDatabase): CyclePeriodDao = database.cyclePeriodDao()
@Provides
fun provideSleepLogDao(database: AppDatabase): SleepLogDao = database.sleepLogDao()
@Provides
fun provideHealthRecordDao(database: AppDatabase): HealthRecordDao = database.healthRecordDao()
@Provides
fun provideWorkoutDao(database: AppDatabase): WorkoutDao = database.workoutDao()
@@ -71,7 +42,12 @@ object AppModule {
@Provides
fun provideUserProfileDao(database: AppDatabase): UserProfileDao = database.userProfileDao()
// DAO для BodyRepo
@Provides
fun provideCyclePeriodDao(database: AppDatabase): CyclePeriodDao = database.cyclePeriodDao()
@Provides
fun provideHealthRecordDao(database: AppDatabase): HealthRecordDao = database.healthRecordDao()
@Provides
fun provideBeverageLogDao(database: AppDatabase): BeverageLogDao = database.beverageLogDao()
@@ -102,7 +78,28 @@ object AppModule {
@Provides
fun provideExerciseFormulaVarDao(database: AppDatabase): ExerciseFormulaVarDao = database.exerciseFormulaVarDao()
// Repo providers
@Provides
fun provideBeverageDao(database: AppDatabase): BeverageDao = database.beverageDao()
@Provides
fun provideBeverageServingDao(database: AppDatabase): BeverageServingDao = database.beverageServingDao()
@Provides
fun provideExerciseParamDao(database: AppDatabase): ExerciseParamDao = database.exerciseParamDao()
@Provides
fun provideNutrientDao(database: AppDatabase): NutrientDao = database.nutrientDao()
@Provides
fun provideCatalogVersionDao(database: AppDatabase): CatalogVersionDao = database.catalogVersionDao()
// Repository/Service Providers
@Provides
@Singleton
fun provideWeightRepository(weightLogDao: WeightLogDao): WeightRepository {
return WeightRepository(weightLogDao)
}
@Provides
@Singleton
fun provideDrinkLogger(
@@ -110,12 +107,9 @@ object AppModule {
beverageLogDao: BeverageLogDao,
beverageLogNutrientDao: BeverageLogNutrientDao,
servingNutrientDao: BeverageServingNutrientDao
): DrinkLogger = DrinkLogger(waterLogDao, beverageLogDao, beverageLogNutrientDao, servingNutrientDao)
@Provides
@Singleton
fun provideWeightRepository(weightLogDao: WeightLogDao): WeightRepository =
WeightRepository(weightLogDao)
): DrinkLogger {
return DrinkLogger(waterLogDao, beverageLogDao, beverageLogNutrientDao, servingNutrientDao)
}
@Provides
@Singleton
@@ -127,23 +121,27 @@ object AppModule {
formulaDao: ExerciseFormulaDao,
formulaVarDao: ExerciseFormulaVarDao,
exerciseDao: ExerciseDao
): WorkoutService = WorkoutService(sessionDao, paramDao, eventDao, weightRepo, formulaDao, formulaVarDao, exerciseDao)
): WorkoutService {
return WorkoutService(sessionDao, paramDao, eventDao, weightRepo, formulaDao, formulaVarDao, exerciseDao)
}
// Repository
@Provides
@Singleton
fun provideWellSheRepository(
waterLogDao: WaterLogDao,
cyclePeriodDao: CyclePeriodDao,
sleepLogDao: SleepLogDao,
healthRecordDao: HealthRecordDao,
workoutDao: WorkoutDao,
calorieDao: CalorieDao,
stepsDao: StepsDao,
userProfileDao: UserProfileDao
): kr.smartsoltech.wellshe.data.repository.WellSheRepository =
kr.smartsoltech.wellshe.data.repository.WellSheRepository(
waterLogDao, cyclePeriodDao, sleepLogDao, healthRecordDao,
workoutDao, calorieDao, stepsDao, userProfileDao
)
fun provideBeverageCatalogRepository(
beverageDao: BeverageDao,
servingDao: BeverageServingDao,
servingNutrientDao: BeverageServingNutrientDao
): BeverageCatalogRepository {
return BeverageCatalogRepository(beverageDao, servingDao, servingNutrientDao)
}
@Provides
@Singleton
fun provideExerciseCatalogRepository(
exerciseDao: ExerciseDao,
paramDao: ExerciseParamDao,
formulaDao: ExerciseFormulaDao
): ExerciseCatalogRepository {
return ExerciseCatalogRepository(exerciseDao, paramDao, formulaDao)
}
}

View File

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

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
data class AppSettings(
val id: Long = 0,
val isWaterReminderEnabled: Boolean = true,
val waterReminderInterval: Int = 2, // часы
val isCycleReminderEnabled: Boolean = true,
val isSleepReminderEnabled: Boolean = true,
val sleepReminderTime: String = "22:00",
val wakeUpReminderTime: String = "07:00",
val cycleLength: Int = 28,
val periodLength: Int = 5,
val waterGoal: Float = 2.5f,
val stepsGoal: Int = 10000,
val sleepGoal: Float = 8.0f,
val isDarkTheme: Boolean = false,
val language: String = "ru",
val isFirstLaunch: Boolean = true
val notificationsEnabled: Boolean = true,
val darkModeEnabled: Boolean = false
)

View File

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

View File

@@ -83,13 +83,6 @@ fun DashboardScreen(
)
}
item {
SleepCard(
sleepData = uiState.sleepData,
onClick = { onNavigate("sleep") }
)
}
item {
RecentWorkoutsCard(
workouts = uiState.recentWorkouts,
@@ -404,26 +397,26 @@ private fun HealthOverviewCard(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
HealthMetric(
label = "Пульс",
value = "${healthData.heartRate}",
unit = "bpm",
icon = Icons.Default.Favorite
)
HealthMetric(
label = "Настроение",
value = getMoodEmoji(healthData.mood),
unit = "",
icon = Icons.Default.Mood
)
HealthMetric(
label = "Энергия",
value = "${healthData.energyLevel}",
unit = "/10",
icon = Icons.Default.Battery6Bar
)
HealthMetric(
label = "Симптомы",
value = "${healthData.symptoms.size}",
unit = "",
icon = Icons.Default.HealthAndSafety
)
HealthMetric(
label = "Заметки",
value = if (healthData.notes.isNotEmpty()) "" else "",
unit = "",
icon = Icons.Default.Notes
)
}
}
}
@@ -479,63 +472,6 @@ private fun HealthMetric(
}
}
@Composable
private fun SleepCard(
sleepData: SleepData,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.clickable { onClick() },
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
colors = CardDefaults.cardColors(containerColor = NeutralWhite),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Bedtime,
contentDescription = null,
tint = PrimaryPink,
modifier = Modifier.size(40.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Сон",
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.SemiBold,
color = TextPrimary
)
)
Text(
text = "${sleepData.sleepDuration}ч • ${getSleepQualityText(sleepData.sleepQuality)}",
style = MaterialTheme.typography.bodyMedium.copy(
color = TextSecondary
)
)
}
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = null,
tint = NeutralGray
)
}
}
}
@Composable
private fun RecentWorkoutsCard(
workouts: List<WorkoutData>,
@@ -605,7 +541,7 @@ private fun WorkoutItem(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = getWorkoutIcon(workout.type),
imageVector = Icons.Default.FitnessCenter,
contentDescription = null,
tint = PrimaryPink,
modifier = Modifier.size(20.dp)
@@ -617,7 +553,7 @@ private fun WorkoutItem(
modifier = Modifier.weight(1f)
) {
Text(
text = getWorkoutTypeText(workout.type),
text = workout.name,
style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = FontWeight.Medium,
color = TextPrimary
@@ -662,12 +598,12 @@ private val quickActions = listOf(
textColor = SecondaryBlue
),
QuickAction(
title = "Отметить сон",
icon = Icons.Default.Bedtime,
route = "sleep",
backgroundColor = AccentPurpleLight,
iconColor = AccentPurple,
textColor = AccentPurple
title = "Экстренная помощь",
icon = Icons.Default.Emergency,
route = "emergency",
backgroundColor = ErrorRedLight,
iconColor = ErrorRed,
textColor = ErrorRed
)
)

View File

@@ -8,19 +8,14 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.launch
import kr.smartsoltech.wellshe.data.entity.CyclePeriodEntity
import kr.smartsoltech.wellshe.data.entity.SleepLogEntity
import kr.smartsoltech.wellshe.data.entity.HealthRecordEntity
import kr.smartsoltech.wellshe.data.repository.WellSheRepository
import kr.smartsoltech.wellshe.domain.model.*
import javax.inject.Inject
import java.time.LocalDate
import java.time.temporal.ChronoUnit
data class DashboardUiState(
val user: User = User(),
val todayHealth: HealthData = HealthData(),
val sleepData: SleepData = SleepData(),
val cycleData: CycleData = CycleData(),
val recentWorkouts: List<WorkoutData> = emptyList(),
val todaySteps: Int = 0,
@@ -53,37 +48,13 @@ class DashboardViewModel @Inject constructor(
_uiState.value = _uiState.value.copy(user = user)
}
// Загружаем данные о здоровье
repository.getTodayHealthData().catch {
// Игнорируем ошибки, используем дефолтные данные
}.collect { healthEntity: HealthRecordEntity? ->
val healthData = healthEntity?.let { convertHealthEntityToModel(it) } ?: HealthData()
_uiState.value = _uiState.value.copy(todayHealth = healthData)
}
// TODO: Временно используем заглушки для данных о здоровье
val healthData = HealthData()
_uiState.value = _uiState.value.copy(todayHealth = healthData)
// Загружаем данные о сне
loadSleepData()
// Загружаем данные о цикле
repository.getRecentPeriods().let { periods ->
val cycleEntity = periods.firstOrNull()
val cycleData = cycleEntity?.let { convertCycleEntityToModel(it) } ?: CycleData()
_uiState.value = _uiState.value.copy(cycleData = cycleData)
}
// Загружаем тренировки
repository.getRecentWorkouts().catch {
// Игнорируем ошибки
}.collect { workoutEntities: List<WorkoutSession> ->
val workouts = workoutEntities.map { convertWorkoutEntityToModel(it) }
_uiState.value = _uiState.value.copy(recentWorkouts = workouts)
}
// Загружаем шаги за сегодня
loadTodayFitnessData()
// Загружаем воду за сегодня
loadTodayWaterData()
// TODO: Временно используем заглушки для данных о цикле
val cycleData = CycleData()
_uiState.value = _uiState.value.copy(cycleData = cycleData)
_uiState.value = _uiState.value.copy(isLoading = false)
@@ -96,136 +67,28 @@ class DashboardViewModel @Inject constructor(
}
}
private suspend fun loadSleepData() {
try {
val yesterday = LocalDate.now().minusDays(1)
val sleepEntity = repository.getSleepForDate(yesterday)
val sleepData = sleepEntity?.let { convertSleepEntityToModel(it) } ?: SleepData()
_uiState.value = _uiState.value.copy(sleepData = sleepData)
} catch (_: Exception) {
// Игнорируем ошибки загрузки сна
}
}
private suspend fun loadTodayFitnessData() {
try {
val today = LocalDate.now()
repository.getFitnessDataForDate(today).catch {
// Игнорируем ошибки
}.collect { fitnessData: FitnessData ->
_uiState.value = _uiState.value.copy(todaySteps = fitnessData.steps)
}
} catch (_: Exception) {
// Игнорируем ошибки загрузки фитнеса
}
}
private suspend fun loadTodayWaterData() {
try {
val today = LocalDate.now()
repository.getWaterIntakeForDate(today).catch {
// Игнорируем ошибки
}.collect { waterIntakes: List<WaterIntake> ->
val totalAmount = waterIntakes.sumOf { it.amount.toDouble() }.toFloat()
_uiState.value = _uiState.value.copy(todayWater = totalAmount)
}
} catch (_: Exception) {
// Игнорируем ошибки загрузки воды
}
}
fun clearError() {
_uiState.value = _uiState.value.copy(error = null)
}
// Функции преобразования Entity -> Model
private fun convertHealthEntityToModel(entity: HealthRecordEntity): HealthData {
return HealthData(
id = entity.id.toString(),
userId = "current_user",
date = entity.date,
weight = entity.weight ?: 0f,
heartRate = entity.heartRate ?: 70,
bloodPressureSystolic = entity.bloodPressureS ?: 120,
bloodPressureDiastolic = entity.bloodPressureD ?: 80,
mood = convertMoodStringToEnum(entity.mood ?: "neutral"),
energyLevel = entity.energyLevel ?: 5,
stressLevel = entity.stressLevel ?: 5,
symptoms = entity.symptoms ?: emptyList()
)
}
private fun convertSleepEntityToModel(entity: SleepLogEntity): SleepData {
return SleepData(
id = entity.id.toString(),
userId = "current_user",
date = entity.date,
bedTime = java.time.LocalTime.parse(entity.bedTime),
wakeTime = java.time.LocalTime.parse(entity.wakeTime),
sleepDuration = entity.duration,
sleepQuality = convertSleepQualityStringToEnum(entity.quality)
)
}
private fun convertCycleEntityToModel(entity: CyclePeriodEntity): CycleData {
return CycleData(
id = entity.id.toString(),
userId = "current_user",
cycleLength = entity.cycleLength ?: 28,
periodLength = entity.endDate?.let {
ChronoUnit.DAYS.between(entity.startDate, it).toInt() + 1
} ?: 5,
lastPeriodDate = entity.startDate,
nextPeriodDate = entity.startDate.plusDays((entity.cycleLength ?: 28).toLong()),
ovulationDate = entity.startDate.plusDays(((entity.cycleLength ?: 28) / 2).toLong())
)
}
private fun convertWorkoutEntityToModel(entity: kr.smartsoltech.wellshe.domain.model.WorkoutSession): WorkoutData {
return WorkoutData(
id = entity.id.toString(),
userId = "current_user",
date = entity.date,
type = convertWorkoutTypeStringToEnum(entity.type),
duration = entity.duration,
intensity = WorkoutIntensity.MODERATE, // По умолчанию, так как в WorkoutSession нет intensity
caloriesBurned = entity.caloriesBurned
)
}
// Вспомогательные функции преобразования
private fun convertMoodStringToEnum(mood: String): Mood {
return when (mood.lowercase()) {
"very_sad" -> Mood.VERY_SAD
"sad" -> Mood.SAD
"neutral" -> Mood.NEUTRAL
"happy" -> Mood.HAPPY
"very_happy" -> Mood.VERY_HAPPY
else -> Mood.NEUTRAL
}
}
private fun convertSleepQualityStringToEnum(quality: String): SleepQuality {
return when (quality.lowercase()) {
"poor" -> SleepQuality.POOR
"fair" -> SleepQuality.FAIR
"good" -> SleepQuality.GOOD
"excellent" -> SleepQuality.EXCELLENT
else -> SleepQuality.GOOD
}
}
private fun convertWorkoutTypeStringToEnum(type: String): WorkoutType {
return when (type.lowercase()) {
"кардио", "cardio" -> WorkoutType.CARDIO
"силовая", "strength" -> WorkoutType.STRENGTH
"йога", "yoga" -> WorkoutType.YOGA
"пилатес", "pilates" -> WorkoutType.PILATES
"бег", "running" -> WorkoutType.RUNNING
"ходьба", "walking" -> WorkoutType.WALKING
"велосипед", "cycling" -> WorkoutType.CYCLING
"плавание", "swimming" -> WorkoutType.SWIMMING
else -> WorkoutType.CARDIO
}
}
}
// Упрощенные модели данных для Dashboard
data class HealthData(
val energyLevel: Int = 5,
val symptoms: List<String> = emptyList(),
val notes: String = ""
)
data class CycleData(
val currentDay: Int = 1,
val nextPeriodDate: LocalDate? = null,
val cycleLength: Int = 28
)
data class WorkoutData(
val id: Long = 0,
val name: String = "",
val duration: Int = 0,
val caloriesBurned: Int = 0,
val date: LocalDate = LocalDate.now()
)

View File

@@ -93,9 +93,7 @@ fun HealthOverviewScreen(
TodayHealthCard(
uiState = uiState,
onUpdateVitals = viewModel::updateVitals,
onUpdateMood = viewModel::updateMood,
onUpdateEnergy = viewModel::updateEnergyLevel,
onUpdateStress = viewModel::updateStressLevel
onUpdateEnergy = viewModel::updateEnergyLevel
)
}
@@ -133,9 +131,7 @@ fun HealthOverviewScreen(
private fun TodayHealthCard(
uiState: HealthUiState,
onUpdateVitals: (Float?, Int?, Int?, Int?, Float?) -> Unit,
onUpdateMood: (String) -> Unit,
onUpdateEnergy: (Int) -> Unit,
onUpdateStress: (Int) -> Unit,
modifier: Modifier = Modifier
) {
var weight by remember { mutableStateOf(uiState.todayRecord?.weight?.toString() ?: "") }
@@ -269,16 +265,7 @@ private fun TodayHealthCard(
Spacer(modifier = Modifier.height(16.dp))
// Настроение
MoodSection(
currentMood = uiState.todayRecord?.mood ?: "neutral",
onMoodChange = onUpdateMood,
isEditMode = uiState.isEditMode
)
Spacer(modifier = Modifier.height(16.dp))
// Уровень энергии и стресса
// Уровень энергии
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
@@ -288,18 +275,9 @@ private fun TodayHealthCard(
value = uiState.todayRecord?.energyLevel ?: 5,
onValueChange = onUpdateEnergy,
isEditMode = uiState.isEditMode,
modifier = Modifier.weight(1f),
modifier = Modifier.fillMaxWidth(),
color = WarningOrange
)
LevelSlider(
label = "Стресс",
value = uiState.todayRecord?.stressLevel ?: 5,
onValueChange = onUpdateStress,
isEditMode = uiState.isEditMode,
modifier = Modifier.weight(1f),
color = ErrorRed
)
}
}
}
@@ -352,68 +330,6 @@ private fun VitalMetric(
}
}
@Composable
private fun MoodSection(
currentMood: String,
onMoodChange: (String) -> Unit,
isEditMode: Boolean,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
Text(
text = "Настроение",
style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = FontWeight.Medium,
color = TextPrimary
)
)
Spacer(modifier = Modifier.height(8.dp))
if (isEditMode) {
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(healthMoods) { mood ->
FilterChip(
onClick = { onMoodChange(mood.key) },
label = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(mood.emoji)
Spacer(modifier = Modifier.width(4.dp))
Text(mood.name)
}
},
selected = currentMood == mood.key,
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = SuccessGreenLight,
selectedLabelColor = SuccessGreen
)
)
}
}
} else {
val currentMoodData = healthMoods.find { it.key == currentMood } ?: healthMoods[2]
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = currentMoodData.emoji,
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = currentMoodData.name,
style = MaterialTheme.typography.bodyLarge.copy(
fontWeight = FontWeight.Medium,
color = TextPrimary
)
)
}
}
}
}
@Composable
private fun LevelSlider(
label: String,
@@ -679,18 +595,8 @@ private fun NotesCard(
}
// Данные для UI
private data class HealthMoodData(val key: String, val name: String, val emoji: String)
private val healthMoods = listOf(
HealthMoodData("very_sad", "Очень плохо", "😢"),
HealthMoodData("sad", "Плохо", "😔"),
HealthMoodData("neutral", "Нормально", "😐"),
HealthMoodData("happy", "Хорошо", "😊"),
HealthMoodData("very_happy", "Отлично", "😄")
)
private val healthSymptoms = listOf(
"Головная боль", "Усталость", "Тошнота", "Головокружение",
"Боль в спине", "Боль в суставах", "Бессонница", "Стресс",
"Боль в спине", "Боль в суставах", "Бессонница",
"Простуда", "Аллергия", "Боль в животе", "Другое"
)

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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"?>
<network-security-config>
<!-- Разрешаем незащищенное HTTP-соединение с IP-адресом 192.168.0.112 -->
<!-- Разрешаем незащищенное HTTP-соединение -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">192.168.0.112</domain>
<domain includeSubdomains="true">192.168.219.108</domain>
</domain-config>
<!-- Настройки по умолчанию - запрещаем незащищенный HTTP-трафик для других адресов -->

View File

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

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,
# thereby reducing the size of the R class for that library
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"
hilt = "2.48"
composeCompiler = "1.5.14"
material = "1.13.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -28,6 +29,7 @@ androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-te
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }