4.15.0: Добавлена принадлежность желаний к проектам
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m33s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m33s
This commit is contained in:
@@ -93,6 +93,53 @@
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.wishlist-project-group {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.wishlist-project-group-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.wishlist-project-group-items {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
gap: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.wishlist-project-group-items::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.wishlist-project-group-items::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.wishlist-project-group-items::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.wishlist-project-group-items::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.wishlist-project-group-items .wishlist-card {
|
||||
flex: 0 0 150px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.wishlist-no-project {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.wishlist-card {
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react'
|
||||
import { useAuth } from './auth/AuthContext'
|
||||
import BoardSelector from './BoardSelector'
|
||||
import LoadingError from './LoadingError'
|
||||
import WishlistDetail from './WishlistDetail'
|
||||
import { sortProjectsLikeCurrentWeek } from '../utils/projectUtils'
|
||||
import './Wishlist.css'
|
||||
|
||||
const API_URL = '/api/wishlist'
|
||||
@@ -45,6 +46,7 @@ function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false, initialBoa
|
||||
const [completedLoading, setCompletedLoading] = useState(false)
|
||||
const [selectedItem, setSelectedItem] = useState(null)
|
||||
const [selectedWishlistForDetail, setSelectedWishlistForDetail] = useState(null)
|
||||
const [currentWeekData, setCurrentWeekData] = useState(null)
|
||||
const fetchingRef = useRef(false)
|
||||
const fetchingCompletedRef = useRef(false)
|
||||
const initialFetchDoneRef = useRef(false)
|
||||
@@ -234,6 +236,24 @@ function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false, initialBoa
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка данных текущей недели для сортировки проектов
|
||||
const fetchCurrentWeek = async () => {
|
||||
try {
|
||||
const response = await authFetch('/api/current-week')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
// Обрабатываем ответ: приходит массив с одним объектом [{total: ..., projects: [...]}]
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
setCurrentWeekData(data[0])
|
||||
} else if (data && typeof data === 'object') {
|
||||
setCurrentWeekData(data)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading current week data:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Первая инициализация
|
||||
useEffect(() => {
|
||||
if (!initialFetchDoneRef.current) {
|
||||
@@ -247,6 +267,9 @@ function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false, initialBoa
|
||||
|
||||
// Загружаем доски с сервера
|
||||
fetchBoards()
|
||||
|
||||
// Загружаем данные текущей недели для сортировки проектов
|
||||
fetchCurrentWeek()
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -557,6 +580,60 @@ function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false, initialBoa
|
||||
)
|
||||
}
|
||||
|
||||
// Группируем желания по проектам
|
||||
const groupedItems = useMemo(() => {
|
||||
const groups = {}
|
||||
const noProjectItems = []
|
||||
|
||||
items.forEach(item => {
|
||||
if (item.project_id && item.project_name) {
|
||||
const projectId = item.project_id
|
||||
if (!groups[projectId]) {
|
||||
groups[projectId] = {
|
||||
projectId: projectId,
|
||||
projectName: item.project_name,
|
||||
items: []
|
||||
}
|
||||
}
|
||||
groups[projectId].items.push(item)
|
||||
} else {
|
||||
noProjectItems.push(item)
|
||||
}
|
||||
})
|
||||
|
||||
// Сортируем группы проектов
|
||||
const projectIds = Object.keys(groups)
|
||||
if (currentWeekData && projectIds.length > 0) {
|
||||
const projectNames = projectIds.map(id => groups[id].projectName)
|
||||
const sortedProjectNames = sortProjectsLikeCurrentWeek(projectNames, currentWeekData)
|
||||
|
||||
// Создаем отсортированный массив групп
|
||||
const sortedGroups = []
|
||||
sortedProjectNames.forEach(projectName => {
|
||||
const group = Object.values(groups).find(g => g.projectName === projectName)
|
||||
if (group) {
|
||||
sortedGroups.push(group)
|
||||
}
|
||||
})
|
||||
|
||||
// Добавляем группы, которых нет в currentWeekData (если есть)
|
||||
Object.values(groups).forEach(group => {
|
||||
if (!sortedProjectNames.includes(group.projectName)) {
|
||||
sortedGroups.push(group)
|
||||
}
|
||||
})
|
||||
|
||||
return { groups: sortedGroups, noProjectItems }
|
||||
}
|
||||
|
||||
// Если нет данных текущей недели, сортируем по алфавиту
|
||||
const sortedGroups = Object.values(groups).sort((a, b) =>
|
||||
a.projectName.localeCompare(b.projectName)
|
||||
)
|
||||
|
||||
return { groups: sortedGroups, noProjectItems }
|
||||
}, [items, currentWeekData])
|
||||
|
||||
const renderItem = (item) => {
|
||||
const isFaded = (!item.unlocked && !item.completed) || item.completed
|
||||
|
||||
@@ -646,9 +723,24 @@ function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false, initialBoa
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="wishlist-grid">
|
||||
{items.map(renderItem)}
|
||||
</div>
|
||||
{/* Группы проектов */}
|
||||
{groupedItems.groups.map(group => (
|
||||
<div key={group.projectId} className="wishlist-project-group">
|
||||
<div className="wishlist-project-group-title">{group.projectName}</div>
|
||||
<div className="wishlist-project-group-items">
|
||||
{group.items.map(renderItem)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Желания без проекта */}
|
||||
{groupedItems.noProjectItems.length > 0 && (
|
||||
<div className="wishlist-no-project">
|
||||
<div className="wishlist-grid">
|
||||
{groupedItems.noProjectItems.map(renderItem)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Завершённые */}
|
||||
{completedCount > 0 && (
|
||||
|
||||
@@ -25,6 +25,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
||||
const [editingConditionIndex, setEditingConditionIndex] = useState(null)
|
||||
const [tasks, setTasks] = useState([])
|
||||
const [projects, setProjects] = useState([])
|
||||
const [selectedProjectId, setSelectedProjectId] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [toastMessage, setToastMessage] = useState(null)
|
||||
@@ -86,6 +87,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
||||
setPrice(data.price ? String(data.price) : '')
|
||||
setLink(data.link || '')
|
||||
setImageUrl(data.image_url || null)
|
||||
setSelectedProjectId(data.project_id ? String(data.project_id) : '')
|
||||
if (data.unlock_conditions) {
|
||||
setUnlockConditions(data.unlock_conditions.map((cond, idx) => ({
|
||||
id: cond.id || null,
|
||||
@@ -242,6 +244,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
||||
setLink(data.link || '')
|
||||
setImageUrl(data.image_url || null)
|
||||
setImageFile(null) // Сбрасываем imageFile при загрузке существующего желания
|
||||
setSelectedProjectId(data.project_id ? String(data.project_id) : '')
|
||||
if (data.unlock_conditions) {
|
||||
setUnlockConditions(data.unlock_conditions.map((cond, idx) => ({
|
||||
id: cond.id || null,
|
||||
@@ -268,6 +271,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
||||
setLink(data.link || '')
|
||||
setImageUrl(data.image_url || null)
|
||||
setImageFile(null)
|
||||
setSelectedProjectId(data.project_id ? String(data.project_id) : '')
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
@@ -283,6 +287,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
||||
setImageUrl(null)
|
||||
setImageFile(null)
|
||||
setUnlockConditions([])
|
||||
setSelectedProjectId('')
|
||||
setError('')
|
||||
setShowCropper(false)
|
||||
setCrop({ x: 0, y: 0 })
|
||||
@@ -553,6 +558,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
||||
name: name.trim(),
|
||||
price: price ? parseFloat(price) : null,
|
||||
link: link.trim() || null,
|
||||
project_id: selectedProjectId ? parseInt(selectedProjectId, 10) : null,
|
||||
unlock_conditions: unlockConditions.map(cond => ({
|
||||
id: cond.id || null,
|
||||
type: cond.type,
|
||||
@@ -841,6 +847,23 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="project">Принадлежность к проекту</label>
|
||||
<select
|
||||
id="project"
|
||||
value={selectedProjectId}
|
||||
onChange={(e) => setSelectedProjectId(e.target.value)}
|
||||
className="form-input"
|
||||
>
|
||||
<option value="">Не выбран</option>
|
||||
{projects.map(project => (
|
||||
<option key={project.project_id} value={project.project_id}>
|
||||
{project.project_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
|
||||
<div className="form-actions">
|
||||
|
||||
Reference in New Issue
Block a user