diff --git a/VERSION b/VERSION index 6abaeb2..798e389 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.2.0 +6.3.0 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index ebe0c65..77abda2 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -491,6 +491,7 @@ type WishlistItem struct { LinkedTask *LinkedTask `json:"linked_task,omitempty"` TasksCount int `json:"tasks_count,omitempty"` // Количество задач для этого желания GroupName *string `json:"group_name,omitempty"` // Название группы желания + IsReady bool `json:"is_ready,omitempty"` // Желание готово к разблокировке (для экрана прогресса) } type UnlockConditionDisplay struct { @@ -2961,12 +2962,21 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) { total := calculateOverallProgress(groupsProgress, groups) // Загружаем желания пользователя - wishes, err := a.getWishlistItemsWithConditions(userID, false) + allWishes, err := a.getWishlistItemsWithConditions(userID, false) if err != nil { log.Printf("Error getting wishlist items for weekly stats: %v", err) - wishes = []WishlistItem{} + allWishes = []WishlistItem{} } + // Создаём map projectId -> minGoalScore для расчёта dailyScore + projectMinGoalScores := make(map[int]float64) + for _, p := range projects { + projectMinGoalScores[p.ProjectID] = p.MinGoalScore + } + + // Фильтруем желания для экрана прогресса недели + wishes := a.filterWishesForWeekProgress(allWishes, projectMinGoalScores) + pendingByProject := draftPendingScores if pendingByProject == nil { pendingByProject = make(map[int]float64) @@ -3867,12 +3877,21 @@ func (a *App) getWeeklyStatsDataForUser(userID int) (*WeeklyStatsResponse, error total := calculateOverallProgress(groupsProgress, groups) // Загружаем желания пользователя - wishes, err := a.getWishlistItemsWithConditions(userID, false) + allWishes, err := a.getWishlistItemsWithConditions(userID, false) if err != nil { log.Printf("Error getting wishlist items for weekly stats: %v", err) - wishes = []WishlistItem{} + allWishes = []WishlistItem{} } + // Создаём map projectId -> minGoalScore для расчёта dailyScore + projectMinGoalScores := make(map[int]float64) + for _, p := range projects { + projectMinGoalScores[p.ProjectID] = p.MinGoalScore + } + + // Фильтруем желания для экрана прогресса недели + wishes := a.filterWishesForWeekProgress(allWishes, projectMinGoalScores) + response := WeeklyStatsResponse{ Total: total, GroupProgress1: groupsProgress.Group1, @@ -11925,6 +11944,205 @@ func isConditionLocked(cond UnlockConditionDisplay) bool { return false } +// filterWishesForWeekProgress фильтрует желания для экрана прогресса недели +// Возвращает желания со сроком <=1 неделя или готовые (unlocked с перебором <=1 день) +// Для каждого проекта оставляет только одно готовое желание с минимальным перебором +func (a *App) filterWishesForWeekProgress(wishes []WishlistItem, projectMinGoalScores map[int]float64) []WishlistItem { + // Группируем желания по проектам + // Для каждого проекта собираем: обычные (срок <=1 неделя) и готовые (unlocked с перебором <=1 день) + type wishWithOverflow struct { + wish WishlistItem + overflow float64 + isReady bool + } + + projectWishes := make(map[int][]wishWithOverflow) + + log.Printf("filterWishesForWeekProgress: total wishes=%d, projectMinGoalScores=%v", len(wishes), projectMinGoalScores) + + for _, wish := range wishes { + if wish.Completed { + continue + } + + // Получаем условие по баллам + var condition *UnlockConditionDisplay + var projectID int + + if wish.Unlocked { + // Для разблокированных желаний ищем условие в unlock_conditions + for i := range wish.UnlockConditions { + if wish.UnlockConditions[i].Type == "project_points" && wish.UnlockConditions[i].ProjectID != nil { + condition = &wish.UnlockConditions[i] + projectID = *condition.ProjectID + break + } + } + } else { + // Для заблокированных желаний берём first_locked_condition + if wish.FirstLockedCondition != nil && wish.FirstLockedCondition.ProjectID != nil { + condition = wish.FirstLockedCondition + projectID = *condition.ProjectID + } else if wish.LockedConditionsCount == 0 { + // Если все условия выполнены но желание ещё не unlocked, ищем в unlock_conditions + for i := range wish.UnlockConditions { + if wish.UnlockConditions[i].Type == "project_points" && wish.UnlockConditions[i].ProjectID != nil { + condition = &wish.UnlockConditions[i] + projectID = *condition.ProjectID + break + } + } + } + } + + if condition == nil || projectID == 0 { + continue + } + + minGoalScore := projectMinGoalScores[projectID] + dailyScore := minGoalScore / 7.0 + + required := 0.0 + current := 0.0 + if condition.RequiredPoints != nil { + required = *condition.RequiredPoints + } + if condition.CurrentPoints != nil { + current = *condition.CurrentPoints + } + overflow := current - required + + log.Printf(" Wish id=%d name='%s' unlocked=%v lockedCount=%d projectID=%d required=%.2f current=%.2f overflow=%.2f dailyScore=%.2f", + wish.ID, wish.Name, wish.Unlocked, wish.LockedConditionsCount, projectID, required, current, overflow, dailyScore) + + if wish.Unlocked { + // Для разблокированных: показываем только если перебор >= 0 и <= 1 день + if overflow >= 0 && overflow <= dailyScore { + log.Printf(" -> ADDED as ready (unlocked)") + projectWishes[projectID] = append(projectWishes[projectID], wishWithOverflow{ + wish: wish, + overflow: overflow, + isReady: true, + }) + } else { + log.Printf(" -> SKIPPED (unlocked but overflow out of range)") + } + } else { + // Для заблокированных: должно быть только одно условие (или 0 если условие уже выполнено) + if wish.LockedConditionsCount > 1 { + log.Printf(" -> SKIPPED (lockedCount > 1)") + continue + } + + // Проверяем, выполнено ли условие (баллов достаточно с перебором <= 1 день) + // Такие желания показываем как готовые + if overflow >= 0 && overflow <= dailyScore { + log.Printf(" -> ADDED as ready (locked but condition met)") + projectWishes[projectID] = append(projectWishes[projectID], wishWithOverflow{ + wish: wish, + overflow: overflow, + isReady: true, + }) + continue + } + + // Иначе проверяем срок <=1 неделя (только если LockedConditionsCount == 1) + if wish.LockedConditionsCount != 1 { + log.Printf(" -> SKIPPED (lockedCount != 1 and not ready)") + continue + } + weeksText := "" + if condition.WeeksText != nil { + weeksText = *condition.WeeksText + } + log.Printf(" weeksText='%s'", weeksText) + if weeksText == "1 неделя" || weeksText == "<1 недели" { + log.Printf(" -> ADDED as normal (weeks match)") + projectWishes[projectID] = append(projectWishes[projectID], wishWithOverflow{ + wish: wish, + overflow: overflow, + isReady: false, + }) + } else { + log.Printf(" -> SKIPPED (weeks don't match)") + } + } + } + + // Собираем результат: добавляем все желания, помечая готовые + var result []WishlistItem + + for _, wishList := range projectWishes { + // Добавляем все желания в результат + for i := range wishList { + w := wishList[i] + if w.isReady { + w.wish.IsReady = true + } + result = append(result, w.wish) + } + } + + // Сортируем результат: готовые первыми, затем по сроку разблокировки, затем по алфавиту + sort.Slice(result, func(i, j int) bool { + // Готовые желания первыми + if result[i].IsReady && !result[j].IsReady { + return true + } + if !result[i].IsReady && result[j].IsReady { + return false + } + + // Получаем weeks_text для сортировки по сроку + getWeeksValue := func(w WishlistItem) float64 { + var weeksText string + if w.FirstLockedCondition != nil && w.FirstLockedCondition.WeeksText != nil { + weeksText = *w.FirstLockedCondition.WeeksText + } else { + // Для разблокированных желаний ищем в unlock_conditions + for _, cond := range w.UnlockConditions { + if cond.Type == "project_points" && cond.WeeksText != nil { + weeksText = *cond.WeeksText + break + } + } + } + if weeksText == "" { + return -1 // Готовые (пустой срок) идут первыми среди своей группы + } + if weeksText == "<1 недели" { + return 0.5 + } + if weeksText == "1 неделя" { + return 1 + } + return 99999 // Для остальных сроков + } + + weeksI := getWeeksValue(result[i]) + weeksJ := getWeeksValue(result[j]) + + if weeksI != weeksJ { + return weeksI < weeksJ + } + + // При одинаковом сроке сортируем по алфавиту + if result[i].Name != result[j].Name { + return result[i].Name < result[j].Name + } + + // При одинаковом имени сортируем по ID для стабильности + return result[i].ID < result[j].ID + }) + + log.Printf("filterWishesForWeekProgress: returning %d wishes", len(result)) + for _, w := range result { + log.Printf(" Result: id=%d name='%s' isReady=%v", w.ID, w.Name, w.IsReady) + } + + return result +} + // getConditionUnlockWeeks возвращает количество недель для разблокировки условия // Используется для сортировки заблокированных условий по баллам func (a *App) getConditionUnlockWeeks(cond UnlockConditionDisplay, userID int) float64 { diff --git a/play-life-web/package.json b/play-life-web/package.json index 0660101..1c8a005 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "6.2.0", + "version": "6.3.0", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/components/CurrentWeek.css b/play-life-web/src/components/CurrentWeek.css index 2db4bbb..bf28cd4 100644 --- a/play-life-web/src/components/CurrentWeek.css +++ b/play-life-web/src/components/CurrentWeek.css @@ -405,3 +405,14 @@ font-size: 0.875rem; color: #6b7280; } + +/* Текст "Готово!" синим для WishRowCard */ +.wish-row-unlock.ready { + color: #3b82f6; + font-weight: 600; +} + +/* Синяя обводка для готового MiniWishCard */ +.mini-wish-image.ready { + border: 2px solid #3b82f6; +} diff --git a/play-life-web/src/components/CurrentWeek.jsx b/play-life-web/src/components/CurrentWeek.jsx index b79d8cb..bf57ac0 100644 --- a/play-life-web/src/components/CurrentWeek.jsx +++ b/play-life-web/src/components/CurrentWeek.jsx @@ -104,7 +104,13 @@ function MiniWishCard({ wish, onClick, pendingScoresByProject = {} }) { } } - const cond = wish.first_locked_condition + // Желание помечено как готовое на бэкенде + const isReady = wish.is_ready === true + + // Для готовых желаний берём условие из unlock_conditions, иначе из first_locked_condition + const cond = isReady + ? wish.unlock_conditions?.find(c => c.type === 'project_points') + : wish.first_locked_condition const isPointsCondition = cond?.type === 'project_points' const required = cond?.required_points ?? 0 const current = cond?.current_points ?? 0 @@ -119,14 +125,14 @@ function MiniWishCard({ wish, onClick, pendingScoresByProject = {} }) { return (
-
+
{wish.image_url ? ( {wish.name} ) : (
🎁
)} -