From 2b7b0565623849d14da68a5ae382833c6d57ac1a Mon Sep 17 00:00:00 2001 From: poignatov Date: Wed, 4 Mar 2026 19:07:39 +0300 Subject: [PATCH] =?UTF-8?q?5.12.0:=20=D0=96=D0=B5=D0=BB=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B2=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA?= =?UTF-8?q?=D0=B0=D1=85=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=BD=D0=B0=20=D0=BD=D0=B5=D0=B4=D0=B5=D0=BB=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION | 2 +- play-life-backend/main.go | 55 +++++- play-life-web/package.json | 2 +- play-life-web/src/App.jsx | 6 +- play-life-web/src/components/CurrentWeek.css | 93 ++++++++++ play-life-web/src/components/CurrentWeek.jsx | 181 +++++++++++++++---- 6 files changed, 288 insertions(+), 51 deletions(-) diff --git a/VERSION b/VERSION index a87467f..dd0ad7a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.11.2 +5.12.0 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index 4eda985..bbdc039 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -144,6 +144,7 @@ type TestConfigsAndDictionariesResponse struct { } type WeeklyProjectStats struct { + ProjectID int `json:"project_id"` ProjectName string `json:"project_name"` TotalScore float64 `json:"total_score"` MinGoalScore float64 `json:"min_goal_score"` @@ -166,6 +167,7 @@ type WeeklyStatsResponse struct { GroupProgress2 *float64 `json:"group_progress_2,omitempty"` GroupProgress0 *float64 `json:"group_progress_0,omitempty"` Projects []WeeklyProjectStats `json:"projects"` + Wishes []WishlistItem `json:"wishes,omitempty"` } type MessagePostRequest struct { @@ -197,9 +199,9 @@ type WeeklyGoalSetup struct { type ProjectScoreSampleMvRow struct { ProjectID int `json:"project_id"` Score float64 `json:"score"` - EntryMessage string `json:"entry_message"` - UserID *int `json:"user_id,omitempty"` - CreatedDate time.Time `json:"created_date"` + EntryMessage string `json:"entry_message"` + UserID *int `json:"user_id,omitempty"` + CreatedDate time.Time `json:"created_date"` } type Project struct { @@ -338,12 +340,12 @@ type Task struct { RewardPolicy *string `json:"reward_policy,omitempty"` // "personal" или "general" для задач, связанных с желаниями Position *int `json:"position,omitempty"` // Position for subtasks // Дополнительные поля для списка задач (без omitempty чтобы всегда передавались) - ProjectNames []string `json:"project_names"` - GroupName *string `json:"group_name,omitempty"` // Название группы задачи - SubtasksCount int `json:"subtasks_count"` - HasProgression bool `json:"has_progression"` - AutoComplete bool `json:"auto_complete"` - DraftProgressionValue *float64 `json:"draft_progression_value,omitempty"` + ProjectNames []string `json:"project_names"` + GroupName *string `json:"group_name,omitempty"` // Название группы задачи + SubtasksCount int `json:"subtasks_count"` + HasProgression bool `json:"has_progression"` + AutoComplete bool `json:"auto_complete"` + DraftProgressionValue *float64 `json:"draft_progression_value,omitempty"` } type Reward struct { @@ -2866,6 +2868,8 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) { return } + project.ProjectID = projectID + // Объединяем данные: если есть данные текущей недели, используем их вместо MV if currentWeekScore, exists := currentWeekScores[projectID]; exists { project.TotalScore = currentWeekScore @@ -2955,12 +2959,20 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) { // Вычисляем общий процент выполнения total := calculateOverallProgress(groupsProgress, groups) + // Загружаем желания пользователя + wishes, err := a.getWishlistItemsWithConditions(userID, false) + if err != nil { + log.Printf("Error getting wishlist items for weekly stats: %v", err) + wishes = []WishlistItem{} + } + response := WeeklyStatsResponse{ Total: total, GroupProgress1: groupsProgress.Group1, GroupProgress2: groupsProgress.Group2, GroupProgress0: groupsProgress.Group0, Projects: projects, + Wishes: wishes, } w.Header().Set("Content-Type", "application/json") @@ -3585,6 +3597,8 @@ func (a *App) getWeeklyStatsData() (*WeeklyStatsResponse, error) { return nil, fmt.Errorf("error scanning weekly stats row: %w", err) } + project.ProjectID = projectID + // Объединяем данные: если есть данные текущей недели, используем их вместо MV if currentWeekScore, exists := currentWeekScores[projectID]; exists { project.TotalScore = currentWeekScore @@ -3756,6 +3770,8 @@ func (a *App) getWeeklyStatsDataForUser(userID int) (*WeeklyStatsResponse, error return nil, fmt.Errorf("error scanning weekly stats row: %w", err) } + project.ProjectID = projectID + // Объединяем данные: если есть данные текущей недели, используем их вместо MV if currentWeekScore, exists := currentWeekScores[projectID]; exists { project.TotalScore = currentWeekScore @@ -3844,12 +3860,20 @@ func (a *App) getWeeklyStatsDataForUser(userID int) (*WeeklyStatsResponse, error // Вычисляем общий процент выполнения total := calculateOverallProgress(groupsProgress, groups) + // Загружаем желания пользователя + wishes, err := a.getWishlistItemsWithConditions(userID, false) + if err != nil { + log.Printf("Error getting wishlist items for weekly stats: %v", err) + wishes = []WishlistItem{} + } + response := WeeklyStatsResponse{ Total: total, GroupProgress1: groupsProgress.Group1, GroupProgress2: groupsProgress.Group2, GroupProgress0: groupsProgress.Group0, Projects: projects, + Wishes: wishes, } return &response, nil @@ -12284,6 +12308,17 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool) condition.CurrentPoints = &totalScore conditionMet = totalScore >= requiredPoints } + // Рассчитываем и форматируем срок разблокировки для заблокированных условий + if condition.ProjectID != nil && condition.RequiredPoints != nil { + weeks := a.calculateProjectUnlockWeeks( + projectID, + requiredPoints, + startDate, + conditionOwnerID, + ) + weeksText := formatWeeksText(weeks) + condition.WeeksText = &weeksText + } } } @@ -16763,6 +16798,8 @@ func (a *App) getWeeklyStatsDataForUserAndWeek(userID int, year int, week int) ( return nil, fmt.Errorf("error scanning weekly stats row: %w", err) } + project.ProjectID = projectID + // Объединяем данные: если это текущая неделя и есть данные, используем их вместо MV if isCurrentWeek { if currentWeekScore, exists := currentWeekScores[projectID]; exists { diff --git a/play-life-web/package.json b/play-life-web/package.json index ef503f3..e98a718 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "5.11.2", + "version": "5.12.0", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/App.jsx b/play-life-web/src/App.jsx index c647012..e33867f 100644 --- a/play-life-web/src/App.jsx +++ b/play-life-web/src/App.jsx @@ -426,13 +426,17 @@ function AppContent() { groupProgress2 = jsonData.group_progress_2 !== undefined ? jsonData.group_progress_2 : null groupProgress0 = jsonData.group_progress_0 !== undefined ? jsonData.group_progress_0 : null } + + // Получаем желания из ответа + const wishes = jsonData?.wishes || [] setCurrentWeekData({ projects: Array.isArray(projects) ? projects : [], total: total, group_progress_1: groupProgress1, group_progress_2: groupProgress2, - group_progress_0: groupProgress0 + group_progress_0: groupProgress0, + wishes: wishes }) } catch (err) { setCurrentWeekError(err.message) diff --git a/play-life-web/src/components/CurrentWeek.css b/play-life-web/src/components/CurrentWeek.css index f48cab9..aaf25f1 100644 --- a/play-life-web/src/components/CurrentWeek.css +++ b/play-life-web/src/components/CurrentWeek.css @@ -174,3 +174,96 @@ opacity: 0.5; cursor: not-allowed; } + +/* Внешний контейнер для карточки проекта */ +.project-card-wrapper { + border: 1px solid #e0e4ed; + border-radius: 1.5rem; + transition: all 0.3s; + box-shadow: 0 1px 3px 0 rgb(99 102 241 / 0.08); + background-color: #eef0f7; +} + +.project-card-wrapper:hover { + box-shadow: 0 2px 6px 0 rgb(99 102 241 / 0.12); +} + +/* Стили для горизонтального скролла желаний в карточке проекта */ +.project-wishes-scroll { + display: flex; + gap: 0.5rem; + overflow-x: auto; + overflow-y: hidden; + padding: 0.5rem 1rem 0.75rem 1rem; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.project-wishes-scroll::-webkit-scrollbar { + display: none; +} + +/* Мини-карточка желания */ +.mini-wish-card { + flex-shrink: 0; + width: 50px; + cursor: pointer; + transition: transform 0.2s, opacity 0.2s; +} + +.mini-wish-card:hover { + transform: scale(1.05); +} + +.mini-wish-card:active { + transform: scale(0.95); +} + +.mini-wish-image { + width: 50px; + height: 60px; + background: #f0f0f0; + border-radius: 8px; + overflow: hidden; + position: relative; +} + +.mini-wish-image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.mini-wish-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.4); + pointer-events: none; +} + +.mini-wish-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; + background: white; + border: 1px solid #e5e7eb; + border-radius: 8px; +} + +.mini-wish-name { + font-size: 0.625rem; + font-weight: 500; + color: #6b7280; + margin-top: 0.125rem; + line-height: 1.1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: center; +} diff --git a/play-life-web/src/components/CurrentWeek.jsx b/play-life-web/src/components/CurrentWeek.jsx index 8cc10a6..86f8035 100644 --- a/play-life-web/src/components/CurrentWeek.jsx +++ b/play-life-web/src/components/CurrentWeek.jsx @@ -4,6 +4,7 @@ import { useAuth } from './auth/AuthContext' import ProjectProgressBar from './ProjectProgressBar' import LoadingError from './LoadingError' import Toast from './Toast' +import WishlistDetail from './WishlistDetail' import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils' import { CircularProgressbar, buildStyles } from 'react-circular-progressbar' import 'react-circular-progressbar/dist/styles.css' @@ -94,8 +95,31 @@ function CircularProgressBar({ progress, size = 120, strokeWidth = 8, showCheckm ) } +// Компонент мини-карточки желания для отображения внутри карточки проекта +function MiniWishCard({ wish, onClick }) { + const handleClick = (e) => { + e.stopPropagation() + if (onClick) { + onClick(wish) + } + } + + return ( +
+
+ {wish.image_url ? ( + {wish.name} + ) : ( +
🎁
+ )} +
+
+
+ ) +} + // Компонент карточки проекта с круглым прогрессбаром -function ProjectCard({ project, projectColor, onProjectClick }) { +function ProjectCard({ project, projectColor, onProjectClick, wishes = [], onWishClick }) { const { project_name, total_score, min_goal_score, max_goal_score, priority, today_change } = project // Вычисляем прогресс по оригинальной логике из ProjectProgressBar @@ -176,52 +200,69 @@ function ProjectCard({ project, projectColor, onProjectClick }) { } } - return ( -
- {/* Верхняя часть с названием и прогрессом */} -
- {/* Левая часть - текст (название, баллы, целевая зона) */} -
-
- {project_name} -
-
-
- {total_score?.toFixed(1) || '0.0'} -
- {today_change !== null && today_change !== undefined && today_change !== 0 && ( -
- ({formatTodayChange(today_change)}) -
- )} -
-
- Целевая зона: {getTargetZone()} -
-
+ const hasWishes = wishes && wishes.length > 0 - {/* Правая часть - круглый прогрессбар */} -
- + return ( +
+
+ {/* Верхняя часть с названием и прогрессом */} +
+ {/* Левая часть - текст (название, баллы, целевая зона) */} +
+
+ {project_name} +
+
+
+ {total_score?.toFixed(1) || '0.0'} +
+ {today_change !== null && today_change !== undefined && today_change !== 0 && ( +
+ ({formatTodayChange(today_change)}) +
+ )} +
+
+ Целевая зона: {getTargetZone()} +
+
+ + {/* Правая часть - круглый прогрессбар */} +
+ +
+ + {/* Горизонтальный список желаний */} + {hasWishes && ( +
+ {wishes.map((wish) => ( + + ))} +
+ )}
) } // Компонент группы проектов по приоритету -function PriorityGroup({ title, subtitle, projects, allProjects, onProjectClick }) { +function PriorityGroup({ title, subtitle, projects, allProjects, onProjectClick, getWishesForProject, onWishClick }) { if (projects.length === 0) return null return ( @@ -239,6 +280,7 @@ function PriorityGroup({ title, subtitle, projects, allProjects, onProjectClick if (!project || !project.project_name) return null const projectColor = getProjectColor(project.project_name, allProjects, project.color) + const projectWishes = getWishesForProject ? getWishesForProject(project.project_id) : [] return ( ) })} @@ -509,6 +553,49 @@ function CurrentWeek({ onProjectClick, data, loading, error, onRetry, allProject const { authFetch } = useAuth() const [isAddModalOpen, setIsAddModalOpen] = useState(false) const [toastMessage, setToastMessage] = useState(null) + const [selectedWishlistId, setSelectedWishlistId] = useState(null) + + // Желания приходят вместе с данными проектов + const wishes = data?.wishes || [] + + // Функция для получения числового значения срока из текста + const getWeeksValue = (weeksText) => { + if (!weeksText) return Infinity + if (weeksText === '<1 недели') return 0 + if (weeksText === '1 неделя') return 1 + const match = weeksText.match(/(\d+)/) + return match ? parseInt(match[1], 10) : Infinity + } + + // Функция фильтрации желаний для проекта + const getWishesForProject = (projectId) => { + 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 filtered.sort((a, b) => { + const weeksA = getWeeksValue(a.first_locked_condition?.weeks_text) + const weeksB = getWeeksValue(b.first_locked_condition?.weeks_text) + return weeksA - weeksB + }) + } + + // Обработчик клика на желание + const handleWishClick = (wish) => { + setSelectedWishlistId(wish.id) + } + + // Закрытие модального окна детализации желания + const handleCloseWishDetail = () => { + setSelectedWishlistId(null) + } // Экспортируем функцию открытия модала для использования из App.jsx useEffect(() => { @@ -667,6 +754,8 @@ function CurrentWeek({ onProjectClick, data, loading, error, onRetry, allProject projects={priorityGroups.main} allProjects={allProjects} onProjectClick={onProjectClick} + getWishesForProject={getWishesForProject} + onWishClick={handleWishClick} />
+ {/* Модальное окно детализации желания */} + {selectedWishlistId && ( + + )} + {/* Модальное окно добавления записи */} {isAddModalOpen && (