fix: исправлен импорт TaskForm с явным расширением .jsx, версия 2.9.1
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 39s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 39s
This commit is contained in:
744
play-life-web/src/components/TaskForm.jsx
Normal file
744
play-life-web/src/components/TaskForm.jsx
Normal file
@@ -0,0 +1,744 @@
|
||||
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 (
|
||||
<div className="task-form">
|
||||
<div className="loading">Загрузка...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="task-form">
|
||||
<button className="close-x-button" onClick={handleCancel}>
|
||||
✕
|
||||
</button>
|
||||
<h2>{taskId ? 'Редактировать задачу' : 'Новая задача'}</h2>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">Название задачи *</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
minLength={1}
|
||||
className="form-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="progression_base">Прогрессия</label>
|
||||
<input
|
||||
id="progression_base"
|
||||
type="number"
|
||||
step="any"
|
||||
value={progressionBase}
|
||||
onChange={(e) => setProgressionBase(e.target.value)}
|
||||
placeholder="Базовое значение"
|
||||
className="form-input"
|
||||
/>
|
||||
<small style={{ color: '#666', fontSize: '0.9em' }}>
|
||||
Оставьте пустым, если прогрессия не используется
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="repetition_period">Период повторения</label>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<input
|
||||
id="repetition_period"
|
||||
type="number"
|
||||
min="0"
|
||||
value={repetitionPeriodValue}
|
||||
onChange={(e) => setRepetitionPeriodValue(e.target.value)}
|
||||
placeholder="Число"
|
||||
className="form-input"
|
||||
style={{ flex: '1' }}
|
||||
/>
|
||||
{repetitionPeriodValue && repetitionPeriodValue.trim() !== '' && parseInt(repetitionPeriodValue.trim(), 10) !== 0 && (
|
||||
<select
|
||||
value={repetitionPeriodType}
|
||||
onChange={(e) => setRepetitionPeriodType(e.target.value)}
|
||||
className="form-input"
|
||||
style={{ width: '120px' }}
|
||||
>
|
||||
<option value="minute">Минута</option>
|
||||
<option value="hour">Час</option>
|
||||
<option value="day">День</option>
|
||||
<option value="week">Неделя</option>
|
||||
<option value="month">Месяц</option>
|
||||
<option value="year">Год</option>
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
<small style={{ color: '#666', fontSize: '0.9em' }}>
|
||||
Оставьте пустым, если задача не повторяется. Введите 0, если задача никогда не переносится в выполненные.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="reward_message">Сообщение награды</label>
|
||||
<textarea
|
||||
id="reward_message"
|
||||
value={rewardMessage}
|
||||
onChange={(e) => setRewardMessage(e.target.value)}
|
||||
placeholder="Используйте ${0}, ${1} для указания проектов"
|
||||
className="form-textarea"
|
||||
rows={3}
|
||||
/>
|
||||
{rewards.length > 0 && (
|
||||
<div className="rewards-container">
|
||||
{rewards.map((reward, index) => (
|
||||
<div key={index} className="reward-item">
|
||||
<span className="reward-number">{index}</span>
|
||||
<input
|
||||
type="text"
|
||||
value={reward.project_name}
|
||||
onChange={(e) => handleRewardChange(index, 'project_name', e.target.value)}
|
||||
placeholder="Проект"
|
||||
className="form-input reward-project-input"
|
||||
list={`projects-list-${index}`}
|
||||
/>
|
||||
<datalist id={`projects-list-${index}`}>
|
||||
{projects.map(p => (
|
||||
<option key={p.project_id} value={p.project_name} />
|
||||
))}
|
||||
</datalist>
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
value={reward.value}
|
||||
onChange={(e) => handleRewardChange(index, 'value', e.target.value)}
|
||||
placeholder="Score"
|
||||
className="form-input reward-score-input"
|
||||
/>
|
||||
{progressionBase && (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
className={`progression-button ${reward.use_progression ? 'progression-button-filled' : 'progression-button-outlined'}`}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
handleRewardProgressionToggle(index, !reward.use_progression)
|
||||
}}
|
||||
title={reward.use_progression ? 'Отключить прогрессию' : 'Включить прогрессию'}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="subtasks-header">
|
||||
<label>Подзадачи</label>
|
||||
<button type="button" onClick={handleAddSubtask} className="add-subtask-button" title="Добавить подзадачу">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{subtasks.map((subtask, index) => (
|
||||
<div key={index} className="subtask-form-item">
|
||||
<div className="subtask-header-row">
|
||||
<input
|
||||
type="text"
|
||||
value={subtask.name}
|
||||
onChange={(e) => handleSubtaskChange(index, 'name', e.target.value)}
|
||||
placeholder="Название подзадачи"
|
||||
className="form-input subtask-name-input"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveSubtask(index)}
|
||||
className="remove-subtask-button"
|
||||
title="Удалить подзадачу"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M3 6h18"></path>
|
||||
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
|
||||
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
|
||||
<line x1="10" y1="11" x2="10" y2="17"></line>
|
||||
<line x1="14" y1="11" x2="14" y2="17"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
value={subtask.reward_message}
|
||||
onChange={(e) => handleSubtaskRewardMessageChange(index, e.target.value)}
|
||||
placeholder="Сообщение награды (опционально)"
|
||||
className="form-textarea"
|
||||
rows={2}
|
||||
/>
|
||||
{subtask.rewards && subtask.rewards.length > 0 && (
|
||||
<div className="subtask-rewards">
|
||||
{subtask.rewards.map((reward, rIndex) => (
|
||||
<div key={rIndex} className="reward-item">
|
||||
<span className="reward-number">{rIndex}</span>
|
||||
<input
|
||||
type="text"
|
||||
value={reward.project_name}
|
||||
onChange={(e) => {
|
||||
const newSubtasks = [...subtasks]
|
||||
newSubtasks[index].rewards[rIndex].project_name = e.target.value
|
||||
setSubtasks(newSubtasks)
|
||||
}}
|
||||
placeholder="Проект"
|
||||
className="form-input reward-project-input"
|
||||
list={`subtask-projects-${index}-${rIndex}`}
|
||||
/>
|
||||
<datalist id={`subtask-projects-${index}-${rIndex}`}>
|
||||
{projects.map(p => (
|
||||
<option key={p.project_id} value={p.project_name} />
|
||||
))}
|
||||
</datalist>
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
value={reward.value}
|
||||
onChange={(e) => {
|
||||
const newSubtasks = [...subtasks]
|
||||
newSubtasks[index].rewards[rIndex].value = e.target.value
|
||||
setSubtasks(newSubtasks)
|
||||
}}
|
||||
placeholder="Score"
|
||||
className="form-input reward-score-input"
|
||||
/>
|
||||
{progressionBase && (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
className={`progression-button progression-button-subtask ${reward.use_progression ? 'progression-button-filled' : 'progression-button-outlined'}`}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
const newSubtasks = [...subtasks]
|
||||
newSubtasks[index].rewards[rIndex].use_progression = !newSubtasks[index].rewards[rIndex].use_progression
|
||||
setSubtasks(newSubtasks)
|
||||
}}
|
||||
title={reward.use_progression ? 'Отключить прогрессию' : 'Включить прогрессию'}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
|
||||
<div className="form-actions">
|
||||
<button type="submit" disabled={loading || isDeleting} className="submit-button">
|
||||
{loading ? 'Сохранение...' : 'Сохранить'}
|
||||
</button>
|
||||
{taskId && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
className="delete-button"
|
||||
disabled={isDeleting || loading}
|
||||
title="Удалить задачу"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<span>...</span>
|
||||
) : (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M3 6h18"></path>
|
||||
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
|
||||
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
|
||||
<line x1="10" y1="11" x2="10" y2="17"></line>
|
||||
<line x1="14" y1="11" x2="14" y2="17"></line>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TaskForm
|
||||
|
||||
Reference in New Issue
Block a user