Добавлена связь задач с желаниями
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 58s

This commit is contained in:
poignatov
2026-01-12 18:58:52 +03:00
parent 9fbe2081ed
commit 72a6a3caf9
15 changed files with 983 additions and 73 deletions

View File

@@ -6,7 +6,7 @@ import './TaskForm.css'
const API_URL = '/api/tasks'
const PROJECTS_API_URL = '/projects'
function TaskForm({ onNavigate, taskId }) {
function TaskForm({ onNavigate, taskId, wishlistId }) {
const { authFetch } = useAuth()
const [name, setName] = useState('')
const [progressionBase, setProgressionBase] = useState('')
@@ -22,6 +22,8 @@ function TaskForm({ onNavigate, taskId }) {
const [toastMessage, setToastMessage] = useState(null)
const [loadingTask, setLoadingTask] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const [wishlistInfo, setWishlistInfo] = useState(null) // Информация о связанном желании
const [currentWishlistId, setCurrentWishlistId] = useState(null) // Текущий wishlist_id задачи
const debounceTimer = useRef(null)
// Загрузка проектов для автокомплита
@@ -65,8 +67,37 @@ function TaskForm({ onNavigate, taskId }) {
} else {
// Сбрасываем форму при создании новой задачи
resetForm()
if (wishlistId) {
// Преобразуем wishlistId в число
const wishlistIdNum = typeof wishlistId === 'string' ? parseInt(wishlistId, 10) : wishlistId
setCurrentWishlistId(wishlistIdNum)
// Загружаем данные желания здесь, чтобы они не сбросились
const loadWishlistData = async () => {
try {
const response = await authFetch(`/api/wishlist/${wishlistIdNum}`)
if (response.ok) {
const data = await response.json()
setWishlistInfo({ id: data.id, name: data.name })
// Предзаполняем название задачи названием желания
if (data.name) {
setName(data.name)
}
// Предзаполняем сообщение награды
if (data.name) {
setRewardMessage(`Выполнить желание: ${data.name}`)
}
}
} catch (err) {
console.error('Error loading wishlist:', err)
}
}
loadWishlistData()
} else {
setCurrentWishlistId(null)
setWishlistInfo(null)
}
}
}, [taskId])
}, [taskId, wishlistId, authFetch])
const loadTask = async () => {
setLoadingTask(true)
@@ -263,6 +294,28 @@ function TaskForm({ onNavigate, taskId }) {
use_progression: r.use_progression
}))
})))
// Загружаем информацию о связанном желании, если есть
if (data.task.wishlist_id) {
setCurrentWishlistId(data.task.wishlist_id)
try {
const wishlistResponse = await authFetch(`/api/wishlist/${data.task.wishlist_id}`)
if (wishlistResponse.ok) {
const wishlistData = await wishlistResponse.json()
setWishlistInfo({ id: wishlistData.id, name: wishlistData.name })
// Если задача привязана к желанию, очищаем поля повторения и прогрессии
setRepetitionPeriodValue('')
setRepetitionPeriodType('day')
setRepetitionMode('after')
setProgressionBase('')
}
} catch (err) {
console.error('Error loading wishlist info:', err)
}
} else {
setCurrentWishlistId(null)
setWishlistInfo(null)
}
} catch (err) {
setError(err.message)
} finally {
@@ -422,12 +475,31 @@ function TaskForm({ onNavigate, taskId }) {
}
}
// Проверяем, что задача с привязанным желанием не может быть периодической
const isLinkedToWishlist = wishlistInfo !== null || (taskId && currentWishlistId)
if (isLinkedToWishlist && repetitionPeriodValue && repetitionPeriodValue.trim() !== '') {
const value = parseInt(repetitionPeriodValue.trim(), 10)
if (!isNaN(value) && value !== 0) {
setError('Задачи, привязанные к желанию, не могут быть периодическими')
setLoading(false)
return
}
}
// Проверяем, что задача с привязанным желанием не может иметь прогрессию
if (isLinkedToWishlist && progressionBase && progressionBase.trim() !== '') {
setError('Задачи, привязанные к желанию, не могут иметь прогрессию')
setLoading(false)
return
}
try {
// Преобразуем период повторения в строку INTERVAL для PostgreSQL или repetition_date
let repetitionPeriod = null
let repetitionDate = null
if (repetitionPeriodValue && repetitionPeriodValue.trim() !== '') {
// Если задача привязана к желанию, не устанавливаем повторения
if (!isLinkedToWishlist && repetitionPeriodValue && repetitionPeriodValue.trim() !== '') {
const valueStr = repetitionPeriodValue.trim()
const value = parseInt(valueStr, 10)
@@ -482,9 +554,16 @@ function TaskForm({ onNavigate, taskId }) {
const payload = {
name: name.trim(),
reward_message: rewardMessage.trim() || null,
progression_base: progressionBase ? parseFloat(progressionBase) : null,
// Если задача привязана к желанию, не отправляем progression_base
progression_base: isLinkedToWishlist ? null : (progressionBase ? parseFloat(progressionBase) : null),
repetition_period: repetitionPeriod,
repetition_date: repetitionDate,
// При создании: отправляем currentWishlistId если указан (уже число)
// При редактировании: отправляем null только если была привязка (currentWishlistId) и пользователь отвязал (!wishlistInfo)
// Если не было привязки или привязка осталась - не отправляем поле (undefined)
wishlist_id: taskId
? (currentWishlistId && !wishlistInfo ? null : undefined)
: (currentWishlistId || undefined),
rewards: rewards.map(r => ({
position: r.position,
project_name: r.project_name.trim(),
@@ -543,6 +622,13 @@ function TaskForm({ onNavigate, taskId }) {
}
}
const handleUnlinkWishlist = () => {
if (window.confirm('Отвязать задачу от желания?')) {
setCurrentWishlistId(null)
setWishlistInfo(null)
}
}
const handleCancel = () => {
resetForm()
onNavigate?.('tasks')
@@ -608,6 +694,27 @@ function TaskForm({ onNavigate, taskId }) {
/>
</div>
{/* Информация о связанном желании */}
{wishlistInfo && (
<div className="form-group">
<div className="wishlist-link-info">
<span className="wishlist-link-text">
Связана с желанием: <strong>{wishlistInfo.name}</strong>
</span>
{taskId && currentWishlistId && (
<button
type="button"
onClick={handleUnlinkWishlist}
className="wishlist-unlink-x"
title="Отвязать от желания"
>
</button>
)}
</div>
</div>
)}
<div className="form-group">
<label htmlFor="progression_base">Прогрессия</label>
<input
@@ -615,18 +722,24 @@ function TaskForm({ onNavigate, taskId }) {
type="number"
step="any"
value={progressionBase}
onChange={(e) => setProgressionBase(e.target.value)}
onChange={(e) => {
if (!wishlistInfo) {
setProgressionBase(e.target.value)
}
}}
placeholder="Базовое значение"
className="form-input"
disabled={wishlistInfo !== null}
/>
<small style={{ color: '#666', fontSize: '0.9em' }}>
Оставьте пустым, если прогрессия не используется
<small style={{ color: wishlistInfo ? '#e74c3c' : '#666', fontSize: '0.9em' }}>
{wishlistInfo ? 'Задачи, привязанные к желанию, не могут иметь прогрессию' : 'Оставьте пустым, если прогрессия не используется'}
</small>
</div>
<div className="form-group">
<label htmlFor="repetition_period">Повторения</label>
{(() => {
const isLinkedToWishlist = wishlistInfo !== null
const hasValidValue = repetitionPeriodValue && repetitionPeriodValue.trim() !== '' && parseInt(repetitionPeriodValue.trim(), 10) !== 0
const isEachMode = hasValidValue && repetitionMode === 'each'
const isYearType = isEachMode && repetitionPeriodType === 'year'
@@ -634,7 +747,7 @@ function TaskForm({ onNavigate, taskId }) {
return (
<>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
{hasValidValue && (
{hasValidValue && !isLinkedToWishlist && (
<select
value={repetitionMode}
onChange={(e) => {
@@ -659,12 +772,17 @@ function TaskForm({ onNavigate, taskId }) {
type={isYearType ? 'text' : 'number'}
min="0"
value={repetitionPeriodValue}
onChange={(e) => setRepetitionPeriodValue(e.target.value)}
onChange={(e) => {
if (!isLinkedToWishlist) {
setRepetitionPeriodValue(e.target.value)
}
}}
placeholder={isYearType ? 'ММ-ДД' : 'Число'}
className="form-input"
style={{ flex: '1' }}
disabled={isLinkedToWishlist}
/>
{hasValidValue && (
{hasValidValue && !isLinkedToWishlist && (
<select
value={repetitionPeriodType}
onChange={(e) => setRepetitionPeriodType(e.target.value)}
@@ -691,7 +809,9 @@ function TaskForm({ onNavigate, taskId }) {
)}
</div>
<small style={{ color: '#666', fontSize: '0.9em' }}>
{isEachMode ? (
{isLinkedToWishlist ? (
<span style={{ color: '#e74c3c' }}>Задачи, привязанные к желанию, не могут быть периодическими</span>
) : isEachMode ? (
repetitionPeriodType === 'week' ? 'Номер дня недели (1-7, где 1 = понедельник)' :
repetitionPeriodType === 'month' ? 'Номер дня месяца (1-31)' :
'Дата в формате ММ-ДД (например, 02-01 для 1 февраля)'