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"`
|
LinkedTask *LinkedTask `json:"linked_task,omitempty"`
|
||||||
TasksCount int `json:"tasks_count,omitempty"` // Количество задач для этого желания
|
TasksCount int `json:"tasks_count,omitempty"` // Количество задач для этого желания
|
||||||
GroupName *string `json:"group_name,omitempty"` // Название группы желания
|
GroupName *string `json:"group_name,omitempty"` // Название группы желания
|
||||||
|
IsReady bool `json:"is_ready,omitempty"` // Желание готово к разблокировке (для экрана прогресса)
|
||||||
}
|
}
|
||||||
|
|
||||||
type UnlockConditionDisplay struct {
|
type UnlockConditionDisplay struct {
|
||||||
@@ -2961,12 +2962,21 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
total := calculateOverallProgress(groupsProgress, groups)
|
total := calculateOverallProgress(groupsProgress, groups)
|
||||||
|
|
||||||
// Загружаем желания пользователя
|
// Загружаем желания пользователя
|
||||||
wishes, err := a.getWishlistItemsWithConditions(userID, false)
|
allWishes, err := a.getWishlistItemsWithConditions(userID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error getting wishlist items for weekly stats: %v", err)
|
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
|
pendingByProject := draftPendingScores
|
||||||
if pendingByProject == nil {
|
if pendingByProject == nil {
|
||||||
pendingByProject = make(map[int]float64)
|
pendingByProject = make(map[int]float64)
|
||||||
@@ -3867,12 +3877,21 @@ func (a *App) getWeeklyStatsDataForUser(userID int) (*WeeklyStatsResponse, error
|
|||||||
total := calculateOverallProgress(groupsProgress, groups)
|
total := calculateOverallProgress(groupsProgress, groups)
|
||||||
|
|
||||||
// Загружаем желания пользователя
|
// Загружаем желания пользователя
|
||||||
wishes, err := a.getWishlistItemsWithConditions(userID, false)
|
allWishes, err := a.getWishlistItemsWithConditions(userID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error getting wishlist items for weekly stats: %v", err)
|
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{
|
response := WeeklyStatsResponse{
|
||||||
Total: total,
|
Total: total,
|
||||||
GroupProgress1: groupsProgress.Group1,
|
GroupProgress1: groupsProgress.Group1,
|
||||||
@@ -11925,6 +11944,205 @@ func isConditionLocked(cond UnlockConditionDisplay) bool {
|
|||||||
return false
|
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 возвращает количество недель для разблокировки условия
|
// getConditionUnlockWeeks возвращает количество недель для разблокировки условия
|
||||||
// Используется для сортировки заблокированных условий по баллам
|
// Используется для сортировки заблокированных условий по баллам
|
||||||
func (a *App) getConditionUnlockWeeks(cond UnlockConditionDisplay, userID int) float64 {
|
func (a *App) getConditionUnlockWeeks(cond UnlockConditionDisplay, userID int) float64 {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "play-life-web",
|
"name": "play-life-web",
|
||||||
"version": "6.2.0",
|
"version": "6.3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -405,3 +405,14 @@
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: #6b7280;
|
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 isPointsCondition = cond?.type === 'project_points'
|
||||||
const required = cond?.required_points ?? 0
|
const required = cond?.required_points ?? 0
|
||||||
const current = cond?.current_points ?? 0
|
const current = cond?.current_points ?? 0
|
||||||
@@ -119,14 +125,14 @@ function MiniWishCard({ wish, onClick, pendingScoresByProject = {} }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mini-wish-card" onClick={handleClick}>
|
<div className="mini-wish-card" onClick={handleClick}>
|
||||||
<div className="mini-wish-image">
|
<div className={`mini-wish-image ${isReady ? 'ready' : ''}`}>
|
||||||
{wish.image_url ? (
|
{wish.image_url ? (
|
||||||
<img src={wish.image_url} alt={wish.name} />
|
<img src={wish.image_url} alt={wish.name} />
|
||||||
) : (
|
) : (
|
||||||
<div className="mini-wish-placeholder">🎁</div>
|
<div className="mini-wish-placeholder">🎁</div>
|
||||||
)}
|
)}
|
||||||
<div className="mini-wish-overlay" aria-hidden="true" />
|
{!isReady && <div className="mini-wish-overlay" aria-hidden="true" />}
|
||||||
{showUnlockPoints && (
|
{showUnlockPoints && !isReady && (
|
||||||
<div
|
<div
|
||||||
className="mini-wish-unlock-points"
|
className="mini-wish-unlock-points"
|
||||||
style={{ fontSize: `${fontSizePx}px` }}
|
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 isPointsCondition = cond?.type === 'project_points'
|
||||||
const required = cond?.required_points ?? 0
|
const required = cond?.required_points ?? 0
|
||||||
const current = cond?.current_points ?? 0
|
const current = cond?.current_points ?? 0
|
||||||
@@ -176,6 +188,9 @@ function WishRowCard({ wish, onClick, pendingScoresByProject = {}, position, min
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getUnlockText = () => {
|
const getUnlockText = () => {
|
||||||
|
if (isReady) {
|
||||||
|
return 'Готово!'
|
||||||
|
}
|
||||||
if (remaining <= 0) {
|
if (remaining <= 0) {
|
||||||
return 'скоро'
|
return 'скоро'
|
||||||
}
|
}
|
||||||
@@ -199,11 +214,11 @@ function WishRowCard({ wish, onClick, pendingScoresByProject = {}, position, min
|
|||||||
) : (
|
) : (
|
||||||
<div className="wish-row-placeholder">🎁</div>
|
<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>
|
||||||
<div className="wish-row-info">
|
<div className="wish-row-info">
|
||||||
<div className="wish-row-title">{wish.name}</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -683,19 +698,31 @@ function CurrentWeek({ onProjectClick, data, loading, error, onRetry, allProject
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Функция фильтрации желаний для проекта
|
// Функция фильтрации желаний для проекта
|
||||||
|
// Фильтрация уже выполнена на бэкенде, здесь только группируем по проекту
|
||||||
const getWishesForProject = (projectId) => {
|
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 => {
|
const filtered = wishes.filter(wish => {
|
||||||
if (wish.unlocked || wish.completed) return false
|
return getWishProjectId(wish) === projectId
|
||||||
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 filtered.sort((a, b) => {
|
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 weeksA = getWeeksValue(a.first_locked_condition?.weeks_text)
|
||||||
const weeksB = getWeeksValue(b.first_locked_condition?.weeks_text)
|
const weeksB = getWeeksValue(b.first_locked_condition?.weeks_text)
|
||||||
return weeksA - weeksB
|
return weeksA - weeksB
|
||||||
|
|||||||
Reference in New Issue
Block a user