6.3.0: Готовые желания на экране прогресса недели
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m19s

This commit is contained in:
poignatov
2026-03-05 19:41:43 +03:00
parent 25317997e5
commit 7f51411175
5 changed files with 277 additions and 21 deletions

View File

@@ -1 +1 @@
6.2.0
6.3.0

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "play-life-web",
"version": "6.2.0",
"version": "6.3.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

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

View File

@@ -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 (
<div className="mini-wish-card" onClick={handleClick}>
<div className="mini-wish-image">
<div className={`mini-wish-image ${isReady ? 'ready' : ''}`}>
{wish.image_url ? (
<img src={wish.image_url} alt={wish.name} />
) : (
<div className="mini-wish-placeholder">🎁</div>
)}
<div className="mini-wish-overlay" aria-hidden="true" />
{showUnlockPoints && (
{!isReady && <div className="mini-wish-overlay" aria-hidden="true" />}
{showUnlockPoints && !isReady && (
<div
className="mini-wish-unlock-points"
style={{ fontSize: `${fontSizePx}px` }}
@@ -149,7 +155,13 @@ function WishRowCard({ wish, onClick, pendingScoresByProject = {}, position, min
}
}
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
@@ -176,6 +188,9 @@ function WishRowCard({ wish, onClick, pendingScoresByProject = {}, position, min
}
const getUnlockText = () => {
if (isReady) {
return 'Готово!'
}
if (remaining <= 0) {
return 'скоро'
}
@@ -199,11 +214,11 @@ function WishRowCard({ wish, onClick, pendingScoresByProject = {}, position, min
) : (
<div className="wish-row-placeholder">🎁</div>
)}
<div className="wish-row-overlay" aria-hidden="true" />
{!isReady && <div className="wish-row-overlay" aria-hidden="true" />}
</div>
<div className="wish-row-info">
<div className="wish-row-title">{wish.name}</div>
<div className="wish-row-unlock">{getUnlockText()}</div>
<div className={`wish-row-unlock ${isReady ? 'ready' : ''}`}>{getUnlockText()}</div>
</div>
</div>
)
@@ -683,19 +698,31 @@ function CurrentWeek({ onProjectClick, data, loading, error, onRetry, allProject
}
// Функция фильтрации желаний для проекта
// Фильтрация уже выполнена на бэкенде, здесь только группируем по проекту
const getWishesForProject = (projectId) => {
// Вспомогательная функция для получения projectId из желания
const getWishProjectId = (wish) => {
// Сначала пробуем first_locked_condition
if (wish.first_locked_condition?.project_id) {
return wish.first_locked_condition.project_id
}
// Иначе ищем в unlock_conditions (для готовых/разблокированных желаний)
if (wish.unlock_conditions) {
const cond = wish.unlock_conditions.find(c => c.type === 'project_points')
return cond?.project_id
}
return null
}
const filtered = wishes.filter(wish => {
if (wish.unlocked || wish.completed) return false
if (wish.locked_conditions_count !== 1) return false
const condition = wish.first_locked_condition
if (!condition || condition.project_id !== projectId) return false
const weeksText = condition.weeks_text
if (!weeksText) return false
return weeksText === '1 неделя' || weeksText === '<1 недели'
return getWishProjectId(wish) === projectId
})
// Сортируем по сроку разблокировки (от меньшего к большему)
// Сортируем: готовые желания первыми, затем по сроку разблокировки
return filtered.sort((a, b) => {
// Готовые желания показываем первыми
if (a.is_ready && !b.is_ready) return -1
if (!a.is_ready && b.is_ready) return 1
const weeksA = getWeeksValue(a.first_locked_condition?.weeks_text)
const weeksB = getWeeksValue(b.first_locked_condition?.weeks_text)
return weeksA - weeksB