import React, { useState, useEffect, useRef } from 'react' import { createPortal } from 'react-dom' import LoadingError from './LoadingError' import { useAuth } from './auth/AuthContext' import TaskDetail from './TaskDetail' // Функция для форматирования скорa (аналогично formatScore из TaskDetail) const formatScore = (num) => { if (num === 0) return '0' let str = num.toPrecision(4) str = str.replace(/\.?0+$/, '') if (str.includes('e+') || str.includes('e-')) { const numValue = parseFloat(str) if (Math.abs(numValue) >= 10000) { return str } return numValue.toString().replace(/\.?0+$/, '') } return str } // Функция для форматирования текста с заменой плейсхолдеров на nodes const formatEntryText = (text, nodes) => { if (!text || !nodes || nodes.length === 0) { return text } // Создаем map для быстрого доступа к nodes по индексу const nodesMap = {} nodes.forEach(node => { nodesMap[node.index] = node }) // Создаем массив для хранения частей текста и React элементов const parts = [] let lastIndex = 0 let currentText = text // Сначала защищаем экранированные плейсхолдеры const escapedMarkers = {} for (let i = 0; i < 100; i++) { const escaped = `\\$${i}` const marker = `__ESCAPED_DOLLAR_${i}__` if (currentText.includes(escaped)) { escapedMarkers[marker] = escaped currentText = currentText.replace(new RegExp(escaped.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), marker) } } // Заменяем ${0}, ${1}, и т.д. for (let i = 0; i < 100; i++) { const placeholder = `\${${i}}` const regex = new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g') if (nodesMap[i] && currentText.includes(placeholder)) { const node = nodesMap[i] const scoreStr = node.score >= 0 ? `${node.project_name}+${formatScore(node.score)}` : `${node.project_name}-${formatScore(Math.abs(node.score))}` currentText = currentText.replace(regex, `__NODE_${i}__`) // Сохраняем информацию о замене if (!escapedMarkers[`__NODE_${i}__`]) { escapedMarkers[`__NODE_${i}__`] = { type: 'node', text: scoreStr } } } } // Заменяем $0, $1, и т.д. (с конца, чтобы не заменить $1 в $10) for (let i = 99; i >= 0; i--) { if (nodesMap[i]) { const node = nodesMap[i] const scoreStr = node.score >= 0 ? `${node.project_name}+${formatScore(node.score)}` : `${node.project_name}-${formatScore(Math.abs(node.score))}` const regex = new RegExp(`\\$${i}(?!\\d)`, 'g') if (currentText.match(regex)) { currentText = currentText.replace(regex, `__NODE_${i}__`) if (!escapedMarkers[`__NODE_${i}__`]) { escapedMarkers[`__NODE_${i}__`] = { type: 'node', text: scoreStr } } } } } // Разбиваем текст на части и создаем React элементы const result = [] let searchIndex = 0 while (searchIndex < currentText.length) { // Ищем следующий маркер let foundMarker = null let markerIndex = currentText.length // Ищем все маркеры for (const marker in escapedMarkers) { const index = currentText.indexOf(marker, searchIndex) if (index !== -1 && index < markerIndex) { markerIndex = index foundMarker = marker } } // Если нашли маркер if (foundMarker) { // Добавляем текст до маркера if (markerIndex > searchIndex) { result.push(currentText.substring(searchIndex, markerIndex)) } // Добавляем элемент для маркера const markerData = escapedMarkers[foundMarker] if (markerData && markerData.type === 'node') { result.push( {markerData.text} ) } else if (typeof markerData === 'string') { // Это экранированный плейсхолдер result.push(markerData) } searchIndex = markerIndex + foundMarker.length } else { // Больше маркеров нет, добавляем оставшийся текст if (searchIndex < currentText.length) { result.push(currentText.substring(searchIndex)) } break } } return result.length > 0 ? result : currentText } // Модальное окно редактирования записи function EditEntryModal({ entry, onClose, onSuccess, authFetch }) { const [message, setMessage] = useState(() => { // Заменяем ${N} на $N для удобного отображения return (entry.text || '').replace(/\$\{(\d+)\}/g, '$$$1') }) const [rewards, setRewards] = useState(() => { if (!entry.nodes || entry.nodes.length === 0) return [] const sorted = [...entry.nodes].sort((a, b) => a.index - b.index) return sorted.map((node, idx) => ({ position: idx, project_name: node.project_name, value: String(node.score) })) }) const [projects, setProjects] = useState([]) const [isSending, setIsSending] = useState(false) const [error, setError] = useState(null) const debounceTimer = useRef(null) useEffect(() => { const loadProjects = async () => { try { const response = await authFetch('/projects') if (response.ok) { const data = await response.json() setProjects(Array.isArray(data) ? data : []) } } catch (err) { console.error('Error loading projects:', err) } } loadProjects() }, [authFetch]) const findMaxPlaceholderIndex = (msg) => { if (!msg) return -1 const indices = [] const matchesCurly = msg.match(/\$\{(\d+)\}/g) || [] matchesCurly.forEach(match => { const numMatch = match.match(/\d+/) if (numMatch) indices.push(parseInt(numMatch[0])) }) let searchIndex = 0 while (true) { const index = msg.indexOf('$', searchIndex) if (index === -1) break if (index === 0 || msg[index - 1] !== '\\') { const afterDollar = msg.substring(index + 1) const digitMatch = afterDollar.match(/^(\d+)/) if (digitMatch) { indices.push(parseInt(digitMatch[0])) } } searchIndex = index + 1 } return indices.length > 0 ? Math.max(...indices) : -1 } // Пересчет rewards при изменении сообщения useEffect(() => { if (debounceTimer.current) clearTimeout(debounceTimer.current) debounceTimer.current = setTimeout(() => { const maxIndex = findMaxPlaceholderIndex(message) setRewards(prevRewards => { const currentRewards = [...prevRewards] while (currentRewards.length > maxIndex + 1) currentRewards.pop() while (currentRewards.length < maxIndex + 1) { currentRewards.push({ position: currentRewards.length, project_name: '', value: '0' }) } return currentRewards }) }, 500) return () => { if (debounceTimer.current) clearTimeout(debounceTimer.current) } }, [message]) const handleRewardChange = (index, field, value) => { const newRewards = [...rewards] newRewards[index] = { ...newRewards[index], [field]: value } setRewards(newRewards) } const buildFinalMessage = () => { let result = message const rewardStrings = {} rewards.forEach((reward, index) => { const score = parseFloat(reward.value) || 0 const projectName = reward.project_name.trim() if (!projectName) return const scoreStr = score >= 0 ? `**${projectName}+${score}**` : `**${projectName}${score}**` rewardStrings[index] = scoreStr }) for (let i = 0; i < 100; i++) { const placeholder = `\${${i}}` if (rewardStrings[i]) result = result.split(placeholder).join(rewardStrings[i]) } for (let i = 99; i >= 0; i--) { if (rewardStrings[i]) { const regex = new RegExp(`\\$${i}(?!\\d)`, 'g') result = result.replace(regex, rewardStrings[i]) } } return result } const isFormValid = () => { if (rewards.length === 0) return true return rewards.every(reward => { const projectName = reward.project_name?.trim() || '' const value = reward.value?.toString().trim() || '' return projectName !== '' && value !== '' }) } const handleSubmit = async () => { for (const reward of rewards) { if (!reward.project_name.trim()) { setError('Заполните все проекты') return } } const finalMessage = buildFinalMessage() if (!finalMessage.trim()) { setError('Введите сообщение') return } setIsSending(true) setError(null) try { const response = await authFetch(`/api/entries/${entry.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: finalMessage }) }) if (!response.ok) throw new Error('Ошибка при сохранении') onSuccess() } catch (err) { console.error('Error updating entry:', err) setError(err.message || 'Ошибка при сохранении') } finally { setIsSending(false) } } const modalContent = (