5.12.0: Желания в карточках проектов на неделе
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m21s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m21s
This commit is contained in:
@@ -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 (
|
||||
<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
|
||||
|
||||
// Вычисляем прогресс по оригинальной логике из ProjectProgressBar
|
||||
@@ -176,52 +200,69 @@ function ProjectCard({ project, projectColor, onProjectClick }) {
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
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"
|
||||
>
|
||||
{/* Верхняя часть с названием и прогрессом */}
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Левая часть - текст (название, баллы, целевая зона) */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-base font-semibold text-gray-600 leading-normal truncate mb-0.5">
|
||||
{project_name}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mb-0.5">
|
||||
<div className="text-3xl font-bold text-black leading-normal">
|
||||
{total_score?.toFixed(1) || '0.0'}
|
||||
</div>
|
||||
{today_change !== null && today_change !== undefined && today_change !== 0 && (
|
||||
<div className="text-base font-medium text-gray-400 leading-normal">
|
||||
({formatTodayChange(today_change)})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 leading-normal">
|
||||
Целевая зона: {getTargetZone()}
|
||||
</div>
|
||||
</div>
|
||||
const hasWishes = wishes && wishes.length > 0
|
||||
|
||||
{/* Правая часть - круглый прогрессбар */}
|
||||
<div className="flex-shrink-0 ml-3">
|
||||
<CircularProgressBar
|
||||
progress={visualProgress}
|
||||
size={80}
|
||||
strokeWidth={8}
|
||||
textSize="small"
|
||||
displayProgress={goalProgress}
|
||||
textPosition="lower"
|
||||
projectColor={projectColor}
|
||||
/>
|
||||
return (
|
||||
<div className="project-card-wrapper">
|
||||
<div
|
||||
onClick={handleClick}
|
||||
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-1 min-w-0">
|
||||
<div className="text-base font-semibold text-gray-600 leading-normal truncate mb-0.5">
|
||||
{project_name}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mb-0.5">
|
||||
<div className="text-3xl font-bold text-black leading-normal">
|
||||
{total_score?.toFixed(1) || '0.0'}
|
||||
</div>
|
||||
{today_change !== null && today_change !== undefined && today_change !== 0 && (
|
||||
<div className="text-base font-medium text-gray-400 leading-normal">
|
||||
({formatTodayChange(today_change)})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 leading-normal">
|
||||
Целевая зона: {getTargetZone()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Правая часть - круглый прогрессбар */}
|
||||
<div className="flex-shrink-0 ml-3">
|
||||
<CircularProgressBar
|
||||
progress={visualProgress}
|
||||
size={80}
|
||||
strokeWidth={8}
|
||||
textSize="small"
|
||||
displayProgress={goalProgress}
|
||||
textPosition="lower"
|
||||
projectColor={projectColor}
|
||||
/>
|
||||
</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
|
||||
|
||||
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 (
|
||||
<ProjectCard
|
||||
@@ -246,6 +288,8 @@ function PriorityGroup({ title, subtitle, projects, allProjects, onProjectClick
|
||||
project={project}
|
||||
projectColor={projectColor}
|
||||
onProjectClick={onProjectClick}
|
||||
wishes={projectWishes}
|
||||
onWishClick={onWishClick}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
<PriorityGroup
|
||||
@@ -675,6 +764,8 @@ function CurrentWeek({ onProjectClick, data, loading, error, onRetry, allProject
|
||||
projects={priorityGroups.important}
|
||||
allProjects={allProjects}
|
||||
onProjectClick={onProjectClick}
|
||||
getWishesForProject={getWishesForProject}
|
||||
onWishClick={handleWishClick}
|
||||
/>
|
||||
|
||||
<PriorityGroup
|
||||
@@ -683,9 +774,21 @@ function CurrentWeek({ onProjectClick, data, loading, error, onRetry, allProject
|
||||
projects={priorityGroups.others}
|
||||
allProjects={allProjects}
|
||||
onProjectClick={onProjectClick}
|
||||
getWishesForProject={getWishesForProject}
|
||||
onWishClick={handleWishClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Модальное окно детализации желания */}
|
||||
{selectedWishlistId && (
|
||||
<WishlistDetail
|
||||
wishlistId={selectedWishlistId}
|
||||
onNavigate={onNavigate}
|
||||
onClose={handleCloseWishDetail}
|
||||
onRefresh={refreshData}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Модальное окно добавления записи */}
|
||||
{isAddModalOpen && (
|
||||
<AddEntryModal
|
||||
|
||||
Reference in New Issue
Block a user