5.12.0: Желания в карточках проектов на неделе
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m21s

This commit is contained in:
poignatov
2026-03-04 19:07:39 +03:00
parent 20773a29b7
commit 2b7b056562
6 changed files with 288 additions and 51 deletions

View File

@@ -1 +1 @@
5.11.2 5.12.0

View File

@@ -144,6 +144,7 @@ type TestConfigsAndDictionariesResponse struct {
} }
type WeeklyProjectStats struct { type WeeklyProjectStats struct {
ProjectID int `json:"project_id"`
ProjectName string `json:"project_name"` ProjectName string `json:"project_name"`
TotalScore float64 `json:"total_score"` TotalScore float64 `json:"total_score"`
MinGoalScore float64 `json:"min_goal_score"` MinGoalScore float64 `json:"min_goal_score"`
@@ -166,6 +167,7 @@ type WeeklyStatsResponse struct {
GroupProgress2 *float64 `json:"group_progress_2,omitempty"` GroupProgress2 *float64 `json:"group_progress_2,omitempty"`
GroupProgress0 *float64 `json:"group_progress_0,omitempty"` GroupProgress0 *float64 `json:"group_progress_0,omitempty"`
Projects []WeeklyProjectStats `json:"projects"` Projects []WeeklyProjectStats `json:"projects"`
Wishes []WishlistItem `json:"wishes,omitempty"`
} }
type MessagePostRequest struct { type MessagePostRequest struct {
@@ -2866,6 +2868,8 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
project.ProjectID = projectID
// Объединяем данные: если есть данные текущей недели, используем их вместо MV // Объединяем данные: если есть данные текущей недели, используем их вместо MV
if currentWeekScore, exists := currentWeekScores[projectID]; exists { if currentWeekScore, exists := currentWeekScores[projectID]; exists {
project.TotalScore = currentWeekScore project.TotalScore = currentWeekScore
@@ -2955,12 +2959,20 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) {
// Вычисляем общий процент выполнения // Вычисляем общий процент выполнения
total := calculateOverallProgress(groupsProgress, groups) 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{ response := WeeklyStatsResponse{
Total: total, Total: total,
GroupProgress1: groupsProgress.Group1, GroupProgress1: groupsProgress.Group1,
GroupProgress2: groupsProgress.Group2, GroupProgress2: groupsProgress.Group2,
GroupProgress0: groupsProgress.Group0, GroupProgress0: groupsProgress.Group0,
Projects: projects, Projects: projects,
Wishes: wishes,
} }
w.Header().Set("Content-Type", "application/json") 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) return nil, fmt.Errorf("error scanning weekly stats row: %w", err)
} }
project.ProjectID = projectID
// Объединяем данные: если есть данные текущей недели, используем их вместо MV // Объединяем данные: если есть данные текущей недели, используем их вместо MV
if currentWeekScore, exists := currentWeekScores[projectID]; exists { if currentWeekScore, exists := currentWeekScores[projectID]; exists {
project.TotalScore = currentWeekScore 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) return nil, fmt.Errorf("error scanning weekly stats row: %w", err)
} }
project.ProjectID = projectID
// Объединяем данные: если есть данные текущей недели, используем их вместо MV // Объединяем данные: если есть данные текущей недели, используем их вместо MV
if currentWeekScore, exists := currentWeekScores[projectID]; exists { if currentWeekScore, exists := currentWeekScores[projectID]; exists {
project.TotalScore = currentWeekScore project.TotalScore = currentWeekScore
@@ -3844,12 +3860,20 @@ func (a *App) getWeeklyStatsDataForUser(userID int) (*WeeklyStatsResponse, error
// Вычисляем общий процент выполнения // Вычисляем общий процент выполнения
total := calculateOverallProgress(groupsProgress, groups) 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{ response := WeeklyStatsResponse{
Total: total, Total: total,
GroupProgress1: groupsProgress.Group1, GroupProgress1: groupsProgress.Group1,
GroupProgress2: groupsProgress.Group2, GroupProgress2: groupsProgress.Group2,
GroupProgress0: groupsProgress.Group0, GroupProgress0: groupsProgress.Group0,
Projects: projects, Projects: projects,
Wishes: wishes,
} }
return &response, nil return &response, nil
@@ -12284,6 +12308,17 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
condition.CurrentPoints = &totalScore condition.CurrentPoints = &totalScore
conditionMet = totalScore >= requiredPoints 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) return nil, fmt.Errorf("error scanning weekly stats row: %w", err)
} }
project.ProjectID = projectID
// Объединяем данные: если это текущая неделя и есть данные, используем их вместо MV // Объединяем данные: если это текущая неделя и есть данные, используем их вместо MV
if isCurrentWeek { if isCurrentWeek {
if currentWeekScore, exists := currentWeekScores[projectID]; exists { if currentWeekScore, exists := currentWeekScores[projectID]; exists {

View File

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

View File

@@ -427,12 +427,16 @@ function AppContent() {
groupProgress0 = jsonData.group_progress_0 !== undefined ? jsonData.group_progress_0 : null groupProgress0 = jsonData.group_progress_0 !== undefined ? jsonData.group_progress_0 : null
} }
// Получаем желания из ответа
const wishes = jsonData?.wishes || []
setCurrentWeekData({ setCurrentWeekData({
projects: Array.isArray(projects) ? projects : [], projects: Array.isArray(projects) ? projects : [],
total: total, total: total,
group_progress_1: groupProgress1, group_progress_1: groupProgress1,
group_progress_2: groupProgress2, group_progress_2: groupProgress2,
group_progress_0: groupProgress0 group_progress_0: groupProgress0,
wishes: wishes
}) })
} catch (err) { } catch (err) {
setCurrentWeekError(err.message) setCurrentWeekError(err.message)

View File

@@ -174,3 +174,96 @@
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; 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;
}

View File

@@ -4,6 +4,7 @@ import { useAuth } from './auth/AuthContext'
import ProjectProgressBar from './ProjectProgressBar' import ProjectProgressBar from './ProjectProgressBar'
import LoadingError from './LoadingError' import LoadingError from './LoadingError'
import Toast from './Toast' import Toast from './Toast'
import WishlistDetail from './WishlistDetail'
import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils' import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils'
import { CircularProgressbar, buildStyles } from 'react-circular-progressbar' import { CircularProgressbar, buildStyles } from 'react-circular-progressbar'
import 'react-circular-progressbar/dist/styles.css' 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 (
<div className="mini-wish-card" onClick={handleClick}>
<div className="mini-wish-image">
{wish.image_url ? (
<img src={wish.image_url} alt={wish.name} />
) : (
<div className="mini-wish-placeholder">🎁</div>
)}
<div className="mini-wish-overlay"></div>
</div>
</div>
)
}
// Компонент карточки проекта с круглым прогрессбаром // Компонент карточки проекта с круглым прогрессбаром
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 const { project_name, total_score, min_goal_score, max_goal_score, priority, today_change } = project
// Вычисляем прогресс по оригинальной логике из ProjectProgressBar // Вычисляем прогресс по оригинальной логике из ProjectProgressBar
@@ -176,10 +200,13 @@ function ProjectCard({ project, projectColor, onProjectClick }) {
} }
} }
const hasWishes = wishes && wishes.length > 0
return ( return (
<div className="project-card-wrapper">
<div <div
onClick={handleClick} onClick={handleClick}
className="bg-white rounded-3xl py-3 px-4 shadow-sm hover:shadow-md transition-all duration-300 cursor-pointer border border-gray-200 hover:border-indigo-300" className="bg-white rounded-3xl py-3 px-4 transition-all duration-300 cursor-pointer"
> >
{/* Верхняя часть с названием и прогрессом */} {/* Верхняя часть с названием и прогрессом */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -217,11 +244,25 @@ function ProjectCard({ project, projectColor, onProjectClick }) {
</div> </div>
</div> </div>
</div> </div>
{/* Горизонтальный список желаний */}
{hasWishes && (
<div className="project-wishes-scroll">
{wishes.map((wish) => (
<MiniWishCard
key={wish.id}
wish={wish}
onClick={onWishClick}
/>
))}
</div>
)}
</div>
) )
} }
// Компонент группы проектов по приоритету // Компонент группы проектов по приоритету
function PriorityGroup({ title, subtitle, projects, allProjects, onProjectClick }) { function PriorityGroup({ title, subtitle, projects, allProjects, onProjectClick, getWishesForProject, onWishClick }) {
if (projects.length === 0) return null if (projects.length === 0) return null
return ( return (
@@ -239,6 +280,7 @@ function PriorityGroup({ title, subtitle, projects, allProjects, onProjectClick
if (!project || !project.project_name) return null if (!project || !project.project_name) return null
const projectColor = getProjectColor(project.project_name, allProjects, project.color) const projectColor = getProjectColor(project.project_name, allProjects, project.color)
const projectWishes = getWishesForProject ? getWishesForProject(project.project_id) : []
return ( return (
<ProjectCard <ProjectCard
@@ -246,6 +288,8 @@ function PriorityGroup({ title, subtitle, projects, allProjects, onProjectClick
project={project} project={project}
projectColor={projectColor} projectColor={projectColor}
onProjectClick={onProjectClick} onProjectClick={onProjectClick}
wishes={projectWishes}
onWishClick={onWishClick}
/> />
) )
})} })}
@@ -509,6 +553,49 @@ function CurrentWeek({ onProjectClick, data, loading, error, onRetry, allProject
const { authFetch } = useAuth() const { authFetch } = useAuth()
const [isAddModalOpen, setIsAddModalOpen] = useState(false) const [isAddModalOpen, setIsAddModalOpen] = useState(false)
const [toastMessage, setToastMessage] = useState(null) 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 // Экспортируем функцию открытия модала для использования из App.jsx
useEffect(() => { useEffect(() => {
@@ -667,6 +754,8 @@ function CurrentWeek({ onProjectClick, data, loading, error, onRetry, allProject
projects={priorityGroups.main} projects={priorityGroups.main}
allProjects={allProjects} allProjects={allProjects}
onProjectClick={onProjectClick} onProjectClick={onProjectClick}
getWishesForProject={getWishesForProject}
onWishClick={handleWishClick}
/> />
<PriorityGroup <PriorityGroup
@@ -675,6 +764,8 @@ function CurrentWeek({ onProjectClick, data, loading, error, onRetry, allProject
projects={priorityGroups.important} projects={priorityGroups.important}
allProjects={allProjects} allProjects={allProjects}
onProjectClick={onProjectClick} onProjectClick={onProjectClick}
getWishesForProject={getWishesForProject}
onWishClick={handleWishClick}
/> />
<PriorityGroup <PriorityGroup
@@ -683,9 +774,21 @@ function CurrentWeek({ onProjectClick, data, loading, error, onRetry, allProject
projects={priorityGroups.others} projects={priorityGroups.others}
allProjects={allProjects} allProjects={allProjects}
onProjectClick={onProjectClick} onProjectClick={onProjectClick}
getWishesForProject={getWishesForProject}
onWishClick={handleWishClick}
/> />
</div> </div>
{/* Модальное окно детализации желания */}
{selectedWishlistId && (
<WishlistDetail
wishlistId={selectedWishlistId}
onNavigate={onNavigate}
onClose={handleCloseWishDetail}
onRefresh={refreshData}
/>
)}
{/* Модальное окно добавления записи */} {/* Модальное окно добавления записи */}
{isAddModalOpen && ( {isAddModalOpen && (
<AddEntryModal <AddEntryModal