4.2.0: Драфты задач и автовыполнение
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 2m29s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 2m29s
This commit is contained in:
@@ -317,3 +317,77 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Dropdown styles */
|
||||
.dropdown-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-button {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: #6366f1;
|
||||
border: 2px solid #6366f1;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: calc(0.75rem * 2 + 1.2rem + 4px);
|
||||
height: calc(0.75rem * 2 + 1.2rem + 4px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dropdown-button:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.dropdown-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: fixed;
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||
min-width: 240px;
|
||||
z-index: 1800;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
width: 100%;
|
||||
padding: 0.5rem 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
font-size: 0.95rem;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.dropdown-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.dropdown-item:hover:not(:disabled) {
|
||||
background-color: #f9fafb;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.dropdown-item:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
319
play-life-web/src/components/TaskDetail.css.bak
Normal file
319
play-life-web/src/components/TaskDetail.css.bak
Normal file
@@ -0,0 +1,319 @@
|
||||
/* Модальное окно */
|
||||
.task-detail-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1700;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.task-detail-modal {
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.task-detail-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.task-detail-close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.task-detail-close-button:hover {
|
||||
background: #f3f4f6;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.task-detail-modal-content {
|
||||
padding: 0 1.5rem 1.5rem 1.5rem;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.task-detail-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.task-reward-message {
|
||||
margin-bottom: 2rem;
|
||||
padding: 1rem;
|
||||
background: #f9fafb;
|
||||
border-radius: 0.375rem;
|
||||
border-left: 3px solid #6366f1;
|
||||
}
|
||||
|
||||
.reward-message-text {
|
||||
color: #374151;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.reward-message-text strong {
|
||||
color: #1f2937;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.task-subtasks {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.subtasks-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.subtask-item {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.subtask-checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.subtask-checkbox {
|
||||
flex-shrink: 0;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.subtask-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.subtask-name {
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.subtask-reward-message {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: white;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.progression-section {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.progression-label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.progression-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.progression-input:focus {
|
||||
outline: none;
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.task-detail-divider {
|
||||
height: 1px;
|
||||
background: #e5e7eb;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.telegram-message-preview {
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
background: #f9fafb;
|
||||
border-radius: 0.375rem;
|
||||
border-left: 3px solid #6366f1;
|
||||
}
|
||||
|
||||
.telegram-message-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.telegram-message-text {
|
||||
color: #1f2937;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.telegram-message-text strong {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.task-actions-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.task-actions-buttons {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.complete-button {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: linear-gradient(to right, #6366f1, #8b5cf6);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.complete-button:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.complete-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.close-button-outline {
|
||||
padding: 0.75rem;
|
||||
background: transparent;
|
||||
color: #6366f1;
|
||||
border: 2px solid #6366f1;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
}
|
||||
|
||||
.close-button-outline:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.close-button-outline:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.next-task-date-info {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
text-align: left;
|
||||
margin-top: -0.125rem;
|
||||
margin-bottom: -0.5rem;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error-message {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.task-wishlist-link {
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 0.75rem;
|
||||
background-color: #f0f9ff;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #bae6fd;
|
||||
}
|
||||
|
||||
.task-wishlist-link-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.task-wishlist-link-info svg {
|
||||
color: #6366f1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.task-wishlist-link-label {
|
||||
font-size: 0.9rem;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.task-wishlist-link-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #6366f1;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
text-decoration: underline;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.task-wishlist-link-button:hover {
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
||||
import { useAuth } from './auth/AuthContext'
|
||||
import LoadingError from './LoadingError'
|
||||
import Toast from './Toast'
|
||||
@@ -331,20 +331,10 @@ const formatTelegramMessage = (task, rewards, subtasks, selectedSubtasks, progre
|
||||
|
||||
// Формируем сообщения подзадач
|
||||
const subtaskMessages = []
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7243/ingest/dd59cdcd-2e10-41ef-b65f-ebbaae0d7424',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'TaskDetail.jsx:333',message:'Starting subtask messages processing',data:{subtasksCount:subtasks.length,selectedSubtasksCount:selectedSubtasks.size,selectedSubtasks:Array.from(selectedSubtasks)},timestamp:Date.now(),sessionId:'debug-session',runId:'run2',hypothesisId:'B'})}).catch(()=>{});
|
||||
// #endregion
|
||||
subtasks.forEach(subtask => {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7243/ingest/dd59cdcd-2e10-41ef-b65f-ebbaae0d7424',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'TaskDetail.jsx:336',message:'Checking subtask',data:{subtaskId:subtask.task.id,isSelected:selectedSubtasks.has(subtask.task.id),hasRewardMessage:!!(subtask.task.reward_message && subtask.task.reward_message.trim() !== ''),rewardsCount:subtask.rewards?.length||0},timestamp:Date.now(),sessionId:'debug-session',runId:'run2',hypothesisId:'B'})}).catch(()=>{});
|
||||
// #endregion
|
||||
if (!selectedSubtasks.has(subtask.task.id)) return
|
||||
if (!subtask.task.reward_message || subtask.task.reward_message.trim() === '') return
|
||||
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7243/ingest/dd59cdcd-2e10-41ef-b65f-ebbaae0d7424',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'TaskDetail.jsx:340',message:'Processing subtask for message',data:{subtaskId:subtask.task.id,subtaskName:subtask.task.name,rewardsCount:subtask.rewards?.length||0,rewards:subtask.rewards,rewardMessage:subtask.task.reward_message},timestamp:Date.now(),sessionId:'debug-session',runId:'run2',hypothesisId:'B'})}).catch(()=>{});
|
||||
// #endregion
|
||||
|
||||
// Вычисляем score для наград подзадачи
|
||||
const subtaskRewardStrings = {}
|
||||
subtask.rewards.forEach(reward => {
|
||||
@@ -393,6 +383,10 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
|
||||
const [isCompleting, setIsCompleting] = useState(false)
|
||||
const [toastMessage, setToastMessage] = useState(null)
|
||||
const [wishlistInfo, setWishlistInfo] = useState(null)
|
||||
const [showDropdown, setShowDropdown] = useState(false)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, right: 0 })
|
||||
const dropdownButtonRef = useRef(null)
|
||||
|
||||
const fetchTaskDetail = useCallback(async () => {
|
||||
try {
|
||||
@@ -403,9 +397,6 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
|
||||
throw new Error('Ошибка загрузки задачи')
|
||||
}
|
||||
const data = await response.json()
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7243/ingest/dd59cdcd-2e10-41ef-b65f-ebbaae0d7424',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'TaskDetail.jsx:395',message:'TaskDetail data loaded',data:{taskId:taskId,subtasksCount:data.subtasks?.length||0,subtasks:data.subtasks?.map(st=>({id:st.task?.id,name:st.task?.name,rewardsCount:st.rewards?.length||0,rewards:st.rewards}))},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'A'})}).catch(()=>{});
|
||||
// #endregion
|
||||
setTaskDetail(data)
|
||||
|
||||
// Используем информацию о wishlist из ответа API
|
||||
@@ -418,6 +409,26 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
|
||||
} else {
|
||||
setWishlistInfo(null)
|
||||
}
|
||||
|
||||
// Предзаполнение данных из драфта
|
||||
if (data.draft_progression_value != null) {
|
||||
setProgressionValue(data.draft_progression_value.toString())
|
||||
}
|
||||
|
||||
if (data.draft_subtasks && data.draft_subtasks.length > 0) {
|
||||
// Создаем Set из ID подзадач из драфта
|
||||
const draftSubtaskIDs = new Set(data.draft_subtasks.map(ds => ds.subtask_id))
|
||||
// Фильтруем только те подзадачи, которые существуют в текущих подзадачах задачи
|
||||
const validSubtaskIDs = new Set()
|
||||
if (data.subtasks) {
|
||||
data.subtasks.forEach(subtask => {
|
||||
if (draftSubtaskIDs.has(subtask.task.id)) {
|
||||
validSubtaskIDs.add(subtask.task.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
setSelectedSubtasks(validSubtaskIDs)
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
console.error('Error fetching task detail:', err)
|
||||
@@ -451,6 +462,83 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveDraft = async (autoComplete = false) => {
|
||||
if (!taskDetail) return
|
||||
|
||||
setIsSaving(true)
|
||||
try {
|
||||
const payload = {
|
||||
auto_complete: autoComplete,
|
||||
children_task_ids: Array.from(selectedSubtasks)
|
||||
}
|
||||
|
||||
// Если есть прогрессия, отправляем значение (или progression_base, если не введено)
|
||||
if (taskDetail.task.progression_base != null) {
|
||||
if (progressionValue.trim()) {
|
||||
const parsedValue = parseFloat(progressionValue)
|
||||
if (isNaN(parsedValue)) {
|
||||
throw new Error('Неверное значение')
|
||||
}
|
||||
payload.progression_value = parsedValue
|
||||
} else {
|
||||
// Если прогрессия не введена - используем progression_base
|
||||
payload.progression_value = taskDetail.task.progression_base
|
||||
}
|
||||
} else {
|
||||
// Если нет progression_base, но пользователь ввел значение - отправляем его
|
||||
if (progressionValue.trim()) {
|
||||
const parsedValue = parseFloat(progressionValue)
|
||||
if (!isNaN(parsedValue)) {
|
||||
payload.progression_value = parsedValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint = autoComplete
|
||||
? `${API_URL}/${taskId}/complete-at-end-of-day`
|
||||
: `${API_URL}/${taskId}/draft`
|
||||
|
||||
const response = await authFetch(endpoint, {
|
||||
method: autoComplete ? 'POST' : 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
throw new Error(errorData.message || 'Ошибка при сохранении драфта')
|
||||
}
|
||||
|
||||
setToastMessage({
|
||||
text: autoComplete
|
||||
? 'Задача будет выполнена в конце дня'
|
||||
: 'Драфт сохранен',
|
||||
type: 'success'
|
||||
})
|
||||
|
||||
// Обновляем данные задачи
|
||||
if (onRefresh) {
|
||||
onRefresh()
|
||||
}
|
||||
|
||||
// Закрываем модальное окно после успешного сохранения
|
||||
if (onClose) {
|
||||
onClose()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error saving draft:', err)
|
||||
setToastMessage({ text: err.message || 'Ошибка при сохранении драфта', type: 'error' })
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCompleteAtEndOfDay = async () => {
|
||||
await handleSaveDraft(true)
|
||||
}
|
||||
|
||||
const handleComplete = async (shouldDelete = false) => {
|
||||
if (!taskDetail) return
|
||||
|
||||
@@ -564,8 +652,27 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
|
||||
return formatTelegramMessage(task, rewards || [], subtasks || [], selectedSubtasks, progressionValue)
|
||||
}, [taskDetail, task, rewards, subtasks, selectedSubtasks, progressionValue])
|
||||
|
||||
|
||||
// Закрываем dropdown при клике вне его
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (showDropdown && !event.target.closest('.dropdown-container') && !event.target.closest('.dropdown-menu')) {
|
||||
setShowDropdown(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [showDropdown])
|
||||
|
||||
return (
|
||||
<div className="task-detail-modal-overlay" onClick={onClose}>
|
||||
<div className="task-detail-modal-overlay" onClick={(e) => {
|
||||
// Закрываем модальное окно только если клик был не на dropdown
|
||||
if (!e.target.closest('.dropdown-container') && !e.target.closest('.dropdown-menu')) {
|
||||
onClose()
|
||||
}
|
||||
}}>
|
||||
<div className="task-detail-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="task-detail-modal-header">
|
||||
<h2 className="task-detail-title">
|
||||
@@ -634,9 +741,6 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
|
||||
<div className="task-subtasks">
|
||||
{subtasks.map((subtask) => {
|
||||
const subtaskName = subtask.task.name || 'Подзадача'
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7243/ingest/dd59cdcd-2e10-41ef-b65f-ebbaae0d7424',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'TaskDetail.jsx:622',message:'Rendering subtask',data:{subtaskId:subtask.task.id,subtaskName:subtaskName,rewardsCount:subtask.rewards?.length||0,rewards:subtask.rewards},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
|
||||
// #endregion
|
||||
return (
|
||||
<div key={subtask.task.id} className="subtask-item">
|
||||
<label className="subtask-checkbox-label">
|
||||
@@ -691,19 +795,95 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
|
||||
)}
|
||||
{isCompleting ? 'Выполнение...' : 'Выполнить'}
|
||||
</button>
|
||||
{!isOneTime && canComplete && (
|
||||
<button
|
||||
onClick={() => handleComplete(true)}
|
||||
disabled={isCompleting || !canComplete}
|
||||
className="close-button-outline"
|
||||
title="Выполнить и закрыть"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 7L7 11L15 3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M3 11L7 15L15 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{/* Кнопка многоточие с dropdown */}
|
||||
{(() => {
|
||||
// Определяем доступные опции
|
||||
const hasProgressionOrSubtasks = hasProgression || (subtasks && subtasks.length > 0)
|
||||
const showCompleteFinally = !isOneTime && canComplete
|
||||
const showCompleteAtEndOfDay = hasProgressionOrSubtasks && canComplete
|
||||
const showSaveDraft = hasProgressionOrSubtasks && canComplete
|
||||
|
||||
// Если нет доступных опций - не показываем кнопку
|
||||
if (!showCompleteFinally && !showCompleteAtEndOfDay && !showSaveDraft) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dropdown-container" style={{ position: 'relative', zIndex: 1800 }}>
|
||||
<button
|
||||
ref={dropdownButtonRef}
|
||||
onClick={() => {
|
||||
const newShowState = !showDropdown
|
||||
if (newShowState && dropdownButtonRef.current) {
|
||||
// Вычисляем позицию синхронно перед открытием
|
||||
const buttonRect = dropdownButtonRef.current.getBoundingClientRect()
|
||||
setDropdownPosition({
|
||||
top: buttonRect.bottom + 8, // 8px = margin-top: 0.5rem
|
||||
right: window.innerWidth - buttonRect.right
|
||||
})
|
||||
}
|
||||
setShowDropdown(newShowState)
|
||||
}}
|
||||
disabled={isCompleting || isSaving || !canComplete}
|
||||
className="dropdown-button"
|
||||
title="Дополнительные действия"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="9" cy="4.5" r="1.5" fill="currentColor"/>
|
||||
<circle cx="9" cy="9" r="1.5" fill="currentColor"/>
|
||||
<circle cx="9" cy="13.5" r="1.5" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
{showDropdown && (
|
||||
<div
|
||||
className="dropdown-menu"
|
||||
style={{
|
||||
zIndex: 1800,
|
||||
position: 'fixed',
|
||||
top: `${dropdownPosition.top}px`,
|
||||
right: `${dropdownPosition.right}px`
|
||||
}}
|
||||
>
|
||||
{showCompleteFinally && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowDropdown(false)
|
||||
handleComplete(true)
|
||||
}}
|
||||
className="dropdown-item"
|
||||
>
|
||||
Выполнить окончательно
|
||||
</button>
|
||||
)}
|
||||
{showCompleteAtEndOfDay && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowDropdown(false)
|
||||
handleCompleteAtEndOfDay()
|
||||
}}
|
||||
className="dropdown-item"
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? 'Сохранение...' : 'Выполнить в конце дня'}
|
||||
</button>
|
||||
)}
|
||||
{showSaveDraft && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowDropdown(false)
|
||||
handleSaveDraft(false)
|
||||
}}
|
||||
className="dropdown-item"
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? 'Сохранение...' : 'Сохранить без автовыполнения'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
{!isOneTime && nextTaskDate && (
|
||||
<div className="next-task-date-info">
|
||||
|
||||
@@ -313,9 +313,6 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
||||
// Для задач-тестов не загружаем подзадачи
|
||||
setSubtasks([])
|
||||
} else {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7243/ingest/dd59cdcd-2e10-41ef-b65f-ebbaae0d7424',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'TaskForm.jsx:316',message:'Loading subtasks in TaskForm',data:{subtasksCount:data.subtasks?.length||0,subtasks:data.subtasks?.map(st=>({id:st.task?.id,name:st.task?.name,rewardsCount:st.rewards?.length||0,rewards:st.rewards}))},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'D'})}).catch(()=>{});
|
||||
// #endregion
|
||||
setSubtasks(data.subtasks.map(st => ({
|
||||
id: st.task.id,
|
||||
name: st.task.name || '',
|
||||
@@ -1108,9 +1105,6 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
||||
{subtask.rewards && subtask.rewards.length > 0 && (
|
||||
<div className="subtask-rewards">
|
||||
{subtask.rewards.map((reward, rIndex) => {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7243/ingest/dd59cdcd-2e10-41ef-b65f-ebbaae0d7424',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'TaskForm.jsx:1107',message:'Rendering subtask reward in form',data:{subtaskIndex:index,rewardIndex:rIndex,reward:reward},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'E'})}).catch(()=>{});
|
||||
// #endregion
|
||||
return (
|
||||
<div key={rIndex} className="reward-item">
|
||||
<span className="reward-number">{rIndex}</span>
|
||||
|
||||
Reference in New Issue
Block a user