6.3.0: Готовые желания на экране прогресса недели
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m19s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m19s
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "play-life-web",
|
||||
"version": "6.2.0",
|
||||
"version": "6.3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user