import React, { useState, useEffect, useCallback, useMemo } from 'react' import { useAuth } from './auth/AuthContext' import './TaskDetail.css' const API_URL = '/api/tasks' // Функция для проверки, является ли период нулевым const isZeroPeriod = (intervalStr) => { if (!intervalStr) return false const trimmed = intervalStr.trim() const parts = trimmed.split(/\s+/) if (parts.length < 1) return false const value = parseInt(parts[0], 10) return !isNaN(value) && value === 0 } // Функция для проверки, является ли repetition_date нулевым const isZeroDate = (dateStr) => { if (!dateStr) return false const trimmed = dateStr.trim() const parts = trimmed.split(/\s+/) if (parts.length < 2) return false const value = parts[0] const numValue = parseInt(value, 10) return !isNaN(numValue) && numValue === 0 } // Функция для форматирования числа как %.4g в Go (до 4 значащих цифр) const formatScore = (num) => { if (num === 0) return '0' // Используем toPrecision(4) для получения до 4 значащих цифр let str = num.toPrecision(4) // Убираем лишние нули в конце (но оставляем точку если есть цифры после неё) str = str.replace(/\.?0+$/, '') // Если получилась экспоненциальная нотация для больших чисел, конвертируем обратно if (str.includes('e+') || str.includes('e-')) { const numValue = parseFloat(str) // Для чисел >= 10000 используем экспоненциальную нотацию if (Math.abs(numValue) >= 10000) { return str } // Для остальных конвертируем в обычное число return numValue.toString().replace(/\.?0+$/, '') } return str } // Функция для формирования сообщения Telegram в реальном времени const formatTelegramMessage = (task, rewards, subtasks, selectedSubtasks, progressionValue) => { if (!task) return '' // Вычисляем score для каждой награды основной задачи const rewardStrings = {} const progressionBase = task.progression_base const hasProgression = progressionBase != null // Если прогрессия не введена - используем progression_base const value = progressionValue && progressionValue.trim() !== '' ? parseFloat(progressionValue) : (hasProgression ? progressionBase : null) rewards.forEach(reward => { let score = reward.value if (reward.use_progression && hasProgression) { if (value !== null && !isNaN(value)) { score = (value / progressionBase) * reward.value } else { // Если прогрессия не введена, используем progression_base (score = reward.value) score = reward.value } } const scoreStr = score >= 0 ? `**${reward.project_name}+${formatScore(score)}**` : `**${reward.project_name}-${formatScore(Math.abs(score))}**` rewardStrings[reward.position] = scoreStr }) // Функция для замены плейсхолдеров const replacePlaceholders = (message, rewardStrings) => { let result = message // Сначала защищаем экранированные плейсхолдеры const escapedMarkers = {} for (let i = 0; i < 100; i++) { const escaped = `\\$${i}` const marker = `__ESCAPED_DOLLAR_${i}__` if (result.includes(escaped)) { escapedMarkers[marker] = escaped result = result.replace(new RegExp(escaped.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), marker) } } // Заменяем ${0}, ${1}, и т.д. for (let i = 0; i < 100; i++) { const placeholder = `\${${i}}` if (rewardStrings[i]) { result = result.replace(new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), rewardStrings[i]) } } // Заменяем $0, $1, и т.д. (с конца, чтобы не заменить $1 в $10) for (let i = 99; i >= 0; i--) { if (rewardStrings[i]) { const searchStr = `$${i}` const regex = new RegExp(`\\$${i}(?!\\d)`, 'g') result = result.replace(regex, rewardStrings[i]) } } // Восстанавливаем экранированные Object.entries(escapedMarkers).forEach(([marker, escaped]) => { result = result.replace(new RegExp(marker, 'g'), escaped) }) return result } // Формируем сообщение основной задачи let mainTaskMessage = task.reward_message && task.reward_message.trim() !== '' ? replacePlaceholders(task.reward_message, rewardStrings) : task.name // Формируем сообщения подзадач const subtaskMessages = [] subtasks.forEach(subtask => { if (!selectedSubtasks.has(subtask.task.id)) return if (!subtask.task.reward_message || subtask.task.reward_message.trim() === '') return // Вычисляем score для наград подзадачи const subtaskRewardStrings = {} subtask.rewards.forEach(reward => { let score = reward.value const subtaskProgressionBase = subtask.task.progression_base if (reward.use_progression) { if (subtaskProgressionBase != null && value !== null && !isNaN(value)) { score = (value / subtaskProgressionBase) * reward.value } else if (hasProgression && value !== null && !isNaN(value)) { score = (value / progressionBase) * reward.value } else if (subtaskProgressionBase != null) { // Если прогрессия не введена, используем progression_base подзадачи (score = reward.value) score = reward.value } else if (hasProgression) { // Если у подзадачи нет progression_base, используем основной (score = reward.value) score = reward.value } } const scoreStr = score >= 0 ? `**${reward.project_name}+${formatScore(score)}**` : `**${reward.project_name}-${formatScore(Math.abs(score))}**` subtaskRewardStrings[reward.position] = scoreStr }) const subtaskMessage = replacePlaceholders(subtask.task.reward_message, subtaskRewardStrings) subtaskMessages.push(subtaskMessage) }) // Формируем итоговое сообщение let finalMessage = mainTaskMessage subtaskMessages.forEach(subtaskMsg => { finalMessage += '\n + ' + subtaskMsg }) return finalMessage } function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) { const { authFetch } = useAuth() const [taskDetail, setTaskDetail] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [selectedSubtasks, setSelectedSubtasks] = useState(new Set()) const [progressionValue, setProgressionValue] = useState('') const [isCompleting, setIsCompleting] = useState(false) const fetchTaskDetail = useCallback(async () => { try { setLoading(true) setError(null) const response = await authFetch(`${API_URL}/${taskId}`) if (!response.ok) { throw new Error('Ошибка загрузки задачи') } const data = await response.json() setTaskDetail(data) } catch (err) { setError(err.message) console.error('Error fetching task detail:', err) } finally { setLoading(false) } }, [taskId, authFetch]) useEffect(() => { if (taskId) { fetchTaskDetail() } else { // Сбрасываем состояние при закрытии модального окна setTaskDetail(null) setLoading(true) setError(null) setSelectedSubtasks(new Set()) setProgressionValue('') } }, [taskId, fetchTaskDetail]) const handleSubtaskToggle = (subtaskId) => { setSelectedSubtasks(prev => { const newSet = new Set(prev) if (newSet.has(subtaskId)) { newSet.delete(subtaskId) } else { newSet.add(subtaskId) } return newSet }) } const handleComplete = async (shouldDelete = false) => { if (!taskDetail) return // Если прогрессия не введена, используем 0 (валидация не требуется) setIsCompleting(true) try { const payload = { children_task_ids: Array.from(selectedSubtasks) } // Если есть прогрессия, отправляем значение (или progression_base, если не введено) if (taskDetail.task.progression_base != null) { if (progressionValue.trim()) { payload.value = parseFloat(progressionValue) if (isNaN(payload.value)) { throw new Error('Неверное значение') } } else { // Если прогрессия не введена - используем progression_base payload.value = taskDetail.task.progression_base } } // Используем единую ручку для выполнения и удаления const endpoint = shouldDelete ? `${API_URL}/${taskId}/complete-and-delete` : `${API_URL}/${taskId}/complete` const response = await authFetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }) if (!response.ok) { const errorData = await response.json().catch(() => ({})) throw new Error(errorData.message || 'Ошибка при выполнении задачи') } // Показываем уведомление о выполнении if (onTaskCompleted) { onTaskCompleted() } // Обновляем список и закрываем модальное окно if (onRefresh) { onRefresh() } if (onClose) { onClose() } } catch (err) { console.error('Error completing task:', err) alert(err.message || 'Ошибка при выполнении задачи') } finally { setIsCompleting(false) } } if (!taskId) return null const { task, rewards, subtasks } = taskDetail || {} const hasProgression = task?.progression_base != null // Кнопка всегда активна (если прогрессия не введена, используем 0) const canComplete = true // Определяем, является ли задача одноразовой // Одноразовая задача: когда оба поля null/undefined (из бэкенда видно, что в этом случае задача помечается как deleted) // Бесконечная задача: когда хотя бы одно поле равно "0 day" или "0 week" и т.д. // Повторяющаяся задача: когда есть значение (не null и не 0) // Кнопка "Закрыть" показывается для задач, которые НЕ одноразовые (имеют повторение, даже если оно равно 0) // Проверяем, что оба поля отсутствуют (null или undefined) const isOneTime = (task?.repetition_period == null || task?.repetition_period === undefined) && (task?.repetition_date == null || task?.repetition_date === undefined) // Формируем сообщение для Telegram в реальном времени const telegramMessage = useMemo(() => { if (!taskDetail) return '' return formatTelegramMessage(task, rewards || [], subtasks || [], selectedSubtasks, progressionValue) }, [taskDetail, task, rewards, subtasks, selectedSubtasks, progressionValue]) return (