import React, { useState, useEffect, useRef } from 'react' import { useAuth } from './auth/AuthContext' import './TaskForm.css' const API_URL = '/api/tasks' const PROJECTS_API_URL = '/projects' function TaskForm({ onNavigate, taskId }) { const { authFetch } = useAuth() const [name, setName] = useState('') const [progressionBase, setProgressionBase] = useState('') const [rewardMessage, setRewardMessage] = useState('') const [repetitionPeriodValue, setRepetitionPeriodValue] = useState('') const [repetitionPeriodType, setRepetitionPeriodType] = useState('day') const [rewards, setRewards] = useState([]) const [subtasks, setSubtasks] = useState([]) const [projects, setProjects] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [loadingTask, setLoadingTask] = useState(false) const [isDeleting, setIsDeleting] = useState(false) const debounceTimer = useRef(null) // Загрузка проектов для автокомплита useEffect(() => { const loadProjects = async () => { try { const response = await authFetch(PROJECTS_API_URL) if (response.ok) { const data = await response.json() setProjects(Array.isArray(data) ? data : []) } } catch (err) { console.error('Error loading projects:', err) } } loadProjects() }, []) // Функция сброса формы const resetForm = () => { setName('') setRewardMessage('') setProgressionBase('') setRepetitionPeriodValue('') setRepetitionPeriodType('day') setRewards([]) setSubtasks([]) setError('') setLoadingTask(false) if (debounceTimer.current) { clearTimeout(debounceTimer.current) debounceTimer.current = null } } // Загрузка задачи при редактировании или сброс формы при создании новой useEffect(() => { if (taskId !== undefined && taskId !== null) { loadTask() } else { // Сбрасываем форму при создании новой задачи resetForm() } }, [taskId]) const loadTask = async () => { setLoadingTask(true) try { const response = await authFetch(`${API_URL}/${taskId}`) if (!response.ok) { throw new Error('Ошибка загрузки задачи') } const data = await response.json() setName(data.task.name) setRewardMessage(data.task.reward_message || '') setProgressionBase(data.task.progression_base ? String(data.task.progression_base) : '') // Парсим repetition_period если он есть if (data.task.repetition_period) { const periodStr = data.task.repetition_period.trim() console.log('Parsing repetition_period:', periodStr, 'Full task data:', data.task) // Отладка // PostgreSQL может возвращать INTERVAL в разных форматах: // - "1 day" / "1 days" / "10 days" // - "02:00:00" (часы в формате времени) // - "21 days" (недели преобразуются в дни) // - "1 month" / "1 months" / "1 mon" let parsed = false // Пробуем парсить формат "N unit" или "N units" // Используем более гибкий regex для парсинга const match = periodStr.match(/^(\d+)\s+(minute|minutes|hour|hours|day|days|week|weeks|month|months|mon|year|years)/i) if (match) { const value = parseInt(match[1], 10) const unit = match[2].toLowerCase() console.log('Matched value:', value, 'unit:', unit) // Отладка if (!isNaN(value) && value >= 0) { // Преобразуем единицы PostgreSQL в наш формат if (unit.startsWith('minute')) { setRepetitionPeriodValue(String(value)) setRepetitionPeriodType('minute') parsed = true } else if (unit.startsWith('hour')) { setRepetitionPeriodValue(String(value)) setRepetitionPeriodType('hour') parsed = true } else if (unit.startsWith('day')) { // Может быть "1 day" или "10 days" или "21 days" (для недель) // Если значение кратно 7, это может быть неделя if (value % 7 === 0 && value >= 7) { setRepetitionPeriodValue(String(value / 7)) setRepetitionPeriodType('week') } else { setRepetitionPeriodValue(String(value)) setRepetitionPeriodType('day') } parsed = true } else if (unit.startsWith('week')) { setRepetitionPeriodValue(String(value)) setRepetitionPeriodType('week') parsed = true } else if (unit.startsWith('month') || unit.startsWith('mon')) { // PostgreSQL возвращает "1 mon" для месяцев setRepetitionPeriodValue(String(value)) setRepetitionPeriodType('month') parsed = true } else if (unit.startsWith('year')) { setRepetitionPeriodValue(String(value)) setRepetitionPeriodType('year') parsed = true } } } else { // Если regex не сработал, пробуем старый способ через split const parts = periodStr.split(/\s+/) if (parts.length >= 2) { const value = parseInt(parts[0], 10) if (!isNaN(value) && value >= 0) { const unit = parts[1].toLowerCase() console.log('Fallback parsing - value:', value, 'unit:', unit) // Отладка if (unit.startsWith('minute')) { setRepetitionPeriodValue(String(value)) setRepetitionPeriodType('minute') parsed = true } else if (unit.startsWith('hour')) { setRepetitionPeriodValue(String(value)) setRepetitionPeriodType('hour') parsed = true } else if (unit.startsWith('day')) { if (value % 7 === 0 && value >= 7) { setRepetitionPeriodValue(String(value / 7)) setRepetitionPeriodType('week') } else { setRepetitionPeriodValue(String(value)) setRepetitionPeriodType('day') } parsed = true } else if (unit.startsWith('week')) { setRepetitionPeriodValue(String(value)) setRepetitionPeriodType('week') parsed = true } else if (unit.startsWith('month') || unit.startsWith('mon')) { setRepetitionPeriodValue(String(value)) setRepetitionPeriodType('month') parsed = true } else if (unit.startsWith('year')) { setRepetitionPeriodValue(String(value)) setRepetitionPeriodType('year') parsed = true } } } } // Если не удалось распарсить, пробуем формат времени "HH:MM:SS" if (!parsed && /^\d{1,2}:\d{2}:\d{2}/.test(periodStr)) { const timeParts = periodStr.split(':') if (timeParts.length >= 3) { const hours = parseInt(timeParts[0], 10) if (!isNaN(hours) && hours >= 0) { setRepetitionPeriodValue(String(hours)) setRepetitionPeriodType('hour') parsed = true } } } // Если не удалось распарсить, сбрасываем значения if (!parsed) { console.log('Failed to parse repetition_period:', periodStr) // Отладка setRepetitionPeriodValue('') setRepetitionPeriodType('day') } else { console.log('Successfully parsed repetition_period - value will be set') // Отладка } } else { console.log('No repetition_period in task data') // Отладка setRepetitionPeriodValue('') setRepetitionPeriodType('day') } // Загружаем rewards setRewards(data.rewards.map(r => ({ position: r.position, project_name: r.project_name, value: String(r.value), use_progression: r.use_progression }))) // Загружаем подзадачи setSubtasks(data.subtasks.map(st => ({ id: st.task.id, name: st.task.name || '', reward_message: st.task.reward_message || '', rewards: st.rewards.map(r => ({ position: r.position, project_name: r.project_name, value: String(r.value), use_progression: r.use_progression })) }))) } catch (err) { setError(err.message) } finally { setLoadingTask(false) } } // Пересчет rewards при изменении reward_message (debounce) useEffect(() => { if (debounceTimer.current) { clearTimeout(debounceTimer.current) } debounceTimer.current = setTimeout(() => { const maxIndex = findMaxPlaceholderIndex(rewardMessage) const currentRewards = [...rewards] // Удаляем лишние rewards while (currentRewards.length > maxIndex + 1) { currentRewards.pop() } // Добавляем недостающие rewards while (currentRewards.length < maxIndex + 1) { currentRewards.push({ position: currentRewards.length, project_name: '', value: '0', use_progression: false }) } setRewards(currentRewards) }, 500) return () => { if (debounceTimer.current) { clearTimeout(debounceTimer.current) } } }, [rewardMessage]) const findMaxPlaceholderIndex = (message) => { if (!message) return -1 const matches = message.match(/\$\{(\d+)\}/g) if (!matches) return -1 const indices = matches.map(m => parseInt(m.match(/\d+/)[0])) return Math.max(...indices) } const handleRewardChange = (index, field, value) => { const newRewards = [...rewards] newRewards[index] = { ...newRewards[index], [field]: value } setRewards(newRewards) } const handleRewardProgressionToggle = (index, checked) => { const newRewards = [...rewards] newRewards[index] = { ...newRewards[index], use_progression: checked } setRewards(newRewards) } const handleAddSubtask = () => { setSubtasks([...subtasks, { id: null, name: '', reward_message: '', rewards: [] }]) } const handleSubtaskChange = (index, field, value) => { const newSubtasks = [...subtasks] newSubtasks[index] = { ...newSubtasks[index], [field]: value } setSubtasks(newSubtasks) } const handleSubtaskRewardMessageChange = (index, value) => { const newSubtasks = [...subtasks] newSubtasks[index] = { ...newSubtasks[index], reward_message: value } // Пересчитываем rewards для подзадачи const maxIndex = findMaxPlaceholderIndex(value) const currentRewards = newSubtasks[index].rewards || [] const newRewards = [...currentRewards] while (newRewards.length < maxIndex + 1) { newRewards.push({ position: newRewards.length, project_name: '', value: '0', use_progression: false }) } while (newRewards.length > maxIndex + 1) { newRewards.pop() } newSubtasks[index] = { ...newSubtasks[index], rewards: newRewards } setSubtasks(newSubtasks) } const handleRemoveSubtask = (index) => { setSubtasks(subtasks.filter((_, i) => i !== index)) } const handleSubmit = async (e) => { e.preventDefault() setError('') setLoading(true) // Валидация if (!name.trim() || name.trim().length < 1) { setError('Название задачи обязательно (минимум 1 символ)') setLoading(false) return } // Проверяем, что все rewards заполнены for (const reward of rewards) { if (!reward.project_name.trim()) { setError('Все проекты в наградах должны быть заполнены') setLoading(false) return } } try { // Преобразуем период повторения в строку INTERVAL для PostgreSQL let repetitionPeriod = null if (repetitionPeriodValue && repetitionPeriodValue.trim() !== '') { const value = parseInt(repetitionPeriodValue.trim(), 10) if (!isNaN(value) && value >= 0) { const typeMap = { 'minute': 'minute', 'hour': 'hour', 'day': 'day', 'week': 'week', 'month': 'month', 'year': 'year' } const unit = typeMap[repetitionPeriodType] || 'day' repetitionPeriod = `${value} ${unit}` console.log('Sending repetition_period:', repetitionPeriod, 'from value:', repetitionPeriodValue, 'type:', repetitionPeriodType) } } else { console.log('No repetition_period to send (value:', repetitionPeriodValue, 'type:', repetitionPeriodType, ')') } const payload = { name: name.trim(), reward_message: rewardMessage.trim() || null, progression_base: progressionBase ? parseFloat(progressionBase) : null, repetition_period: repetitionPeriod, rewards: rewards.map(r => ({ position: r.position, project_name: r.project_name.trim(), value: parseFloat(r.value) || 0, use_progression: !!(progressionBase && r.use_progression) })), subtasks: subtasks.map(st => ({ id: st.id || undefined, name: st.name.trim() || null, reward_message: st.reward_message.trim() || null, rewards: st.rewards.map(r => ({ position: r.position, project_name: r.project_name.trim(), value: parseFloat(r.value) || 0, use_progression: !!(progressionBase && r.use_progression) })) })) } const url = taskId ? `${API_URL}/${taskId}` : API_URL const method = taskId ? 'PUT' : 'POST' const response = await authFetch(url, { method, headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }) if (!response.ok) { let errorMessage = 'Ошибка при сохранении задачи' try { const errorData = await response.json() errorMessage = errorData.message || errorData.error || errorMessage } catch (e) { // Если не удалось распарсить JSON, используем текст ответа const text = await response.text().catch(() => '') if (text) { errorMessage = text } } throw new Error(errorMessage) } // Возвращаемся к списку задач onNavigate?.('tasks') } catch (err) { setError(err.message) console.error('Error saving task:', err) } finally { setLoading(false) } } const handleCancel = () => { resetForm() onNavigate?.('tasks') } const handleDelete = async () => { if (!taskId) return if (!window.confirm(`Вы уверены, что хотите удалить задачу "${name}"?`)) { return } setIsDeleting(true) try { const response = await authFetch(`${API_URL}/${taskId}`, { method: 'DELETE', }) if (!response.ok) { throw new Error('Ошибка при удалении задачи') } // Возвращаемся к списку задач onNavigate?.('tasks') } catch (err) { console.error('Error deleting task:', err) setError('Ошибка при удалении задачи') setIsDeleting(false) } } if (loadingTask) { return (
Загрузка...
) } return (

{taskId ? 'Редактировать задачу' : 'Новая задача'}

setName(e.target.value)} required minLength={1} className="form-input" />
setProgressionBase(e.target.value)} placeholder="Базовое значение" className="form-input" /> Оставьте пустым, если прогрессия не используется
setRepetitionPeriodValue(e.target.value)} placeholder="Число" className="form-input" style={{ flex: '1' }} /> {repetitionPeriodValue && repetitionPeriodValue.trim() !== '' && parseInt(repetitionPeriodValue.trim(), 10) !== 0 && ( )}
Оставьте пустым, если задача не повторяется. Введите 0, если задача никогда не переносится в выполненные.