4.2.0: Драфты задач и автовыполнение
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 2m29s

This commit is contained in:
poignatov
2026-01-28 20:19:53 +03:00
parent a886cf13e8
commit ba0f34c91b
10 changed files with 1275 additions and 376 deletions

View File

@@ -1 +1 @@
4.1.2 4.2.0

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
-- Migration: Remove task drafts tables
-- Date: 2026-01-26
--
-- This migration removes tables created for task drafts
DROP TABLE IF EXISTS task_draft_subtasks;
DROP TABLE IF EXISTS task_drafts;

View File

@@ -0,0 +1,45 @@
-- Migration: Add task drafts tables
-- Date: 2026-01-26
--
-- This migration creates tables for storing task drafts:
-- 1. task_drafts - main table for task drafts with progression value and auto_complete flag
-- 2. task_draft_subtasks - stores only checked subtask IDs for each draft
-- ============================================
-- Table: task_drafts
-- ============================================
CREATE TABLE task_drafts (
id SERIAL PRIMARY KEY,
task_id INTEGER REFERENCES tasks(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
progression_value NUMERIC(10,4),
auto_complete BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(task_id)
);
CREATE INDEX idx_task_drafts_task_id ON task_drafts(task_id);
CREATE INDEX idx_task_drafts_user_id ON task_drafts(user_id);
CREATE INDEX idx_task_drafts_auto_complete ON task_drafts(auto_complete) WHERE auto_complete = TRUE;
COMMENT ON TABLE task_drafts IS 'Stores draft states for tasks with progression value and auto-complete flag';
COMMENT ON COLUMN task_drafts.progression_value IS 'Saved progression value from user input';
COMMENT ON COLUMN task_drafts.auto_complete IS 'Flag indicating task should be auto-completed at end of day (23:55)';
COMMENT ON COLUMN task_drafts.task_id IS 'Reference to task. UNIQUE constraint ensures one draft per task';
-- ============================================
-- Table: task_draft_subtasks
-- ============================================
CREATE TABLE task_draft_subtasks (
id SERIAL PRIMARY KEY,
task_draft_id INTEGER REFERENCES task_drafts(id) ON DELETE CASCADE,
subtask_id INTEGER REFERENCES tasks(id) ON DELETE CASCADE,
UNIQUE(task_draft_id, subtask_id)
);
CREATE INDEX idx_task_draft_subtasks_task_draft_id ON task_draft_subtasks(task_draft_id);
CREATE INDEX idx_task_draft_subtasks_subtask_id ON task_draft_subtasks(subtask_id);
COMMENT ON TABLE task_draft_subtasks IS 'Stores only checked subtask IDs for each draft. If subtask is not in this table, it means it is unchecked';
COMMENT ON COLUMN task_draft_subtasks.subtask_id IS 'Reference to subtask task. Only checked subtasks are stored here';

Binary file not shown.

View File

@@ -1,6 +1,6 @@
{ {
"name": "play-life-web", "name": "play-life-web",
"version": "4.1.2", "version": "4.2.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -317,3 +317,77 @@
text-decoration: none; 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;
}

View 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;
}

View File

@@ -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 { useAuth } from './auth/AuthContext'
import LoadingError from './LoadingError' import LoadingError from './LoadingError'
import Toast from './Toast' import Toast from './Toast'
@@ -331,20 +331,10 @@ const formatTelegramMessage = (task, rewards, subtasks, selectedSubtasks, progre
// Формируем сообщения подзадач // Формируем сообщения подзадач
const subtaskMessages = [] 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 => { 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 (!selectedSubtasks.has(subtask.task.id)) return
if (!subtask.task.reward_message || subtask.task.reward_message.trim() === '') 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 для наград подзадачи // Вычисляем score для наград подзадачи
const subtaskRewardStrings = {} const subtaskRewardStrings = {}
subtask.rewards.forEach(reward => { subtask.rewards.forEach(reward => {
@@ -393,6 +383,10 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
const [isCompleting, setIsCompleting] = useState(false) const [isCompleting, setIsCompleting] = useState(false)
const [toastMessage, setToastMessage] = useState(null) const [toastMessage, setToastMessage] = useState(null)
const [wishlistInfo, setWishlistInfo] = 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 () => { const fetchTaskDetail = useCallback(async () => {
try { try {
@@ -403,9 +397,6 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
throw new Error('Ошибка загрузки задачи') throw new Error('Ошибка загрузки задачи')
} }
const data = await response.json() 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) setTaskDetail(data)
// Используем информацию о wishlist из ответа API // Используем информацию о wishlist из ответа API
@@ -418,6 +409,26 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
} else { } else {
setWishlistInfo(null) 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) { } catch (err) {
setError(err.message) setError(err.message)
console.error('Error fetching task detail:', err) 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) => { const handleComplete = async (shouldDelete = false) => {
if (!taskDetail) return if (!taskDetail) return
@@ -564,8 +652,27 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
return formatTelegramMessage(task, rewards || [], subtasks || [], selectedSubtasks, progressionValue) return formatTelegramMessage(task, rewards || [], subtasks || [], selectedSubtasks, progressionValue)
}, [taskDetail, 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 ( 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" onClick={(e) => e.stopPropagation()}>
<div className="task-detail-modal-header"> <div className="task-detail-modal-header">
<h2 className="task-detail-title"> <h2 className="task-detail-title">
@@ -634,9 +741,6 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
<div className="task-subtasks"> <div className="task-subtasks">
{subtasks.map((subtask) => { {subtasks.map((subtask) => {
const subtaskName = subtask.task.name || 'Подзадача' 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 ( return (
<div key={subtask.task.id} className="subtask-item"> <div key={subtask.task.id} className="subtask-item">
<label className="subtask-checkbox-label"> <label className="subtask-checkbox-label">
@@ -691,19 +795,95 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
)} )}
{isCompleting ? 'Выполнение...' : 'Выполнить'} {isCompleting ? 'Выполнение...' : 'Выполнить'}
</button> </button>
{!isOneTime && canComplete && ( {/* Кнопка многоточие с 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 <button
onClick={() => handleComplete(true)} ref={dropdownButtonRef}
disabled={isCompleting || !canComplete} onClick={() => {
className="close-button-outline" const newShowState = !showDropdown
title="Выполнить и закрыть" 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"> <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"/> <circle cx="9" cy="4.5" r="1.5" fill="currentColor"/>
<path d="M3 11L7 15L15 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> <circle cx="9" cy="9" r="1.5" fill="currentColor"/>
<circle cx="9" cy="13.5" r="1.5" fill="currentColor"/>
</svg> </svg>
</button> </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> </div>
{!isOneTime && nextTaskDate && ( {!isOneTime && nextTaskDate && (
<div className="next-task-date-info"> <div className="next-task-date-info">

View File

@@ -313,9 +313,6 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
// Для задач-тестов не загружаем подзадачи // Для задач-тестов не загружаем подзадачи
setSubtasks([]) setSubtasks([])
} else { } 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 => ({ setSubtasks(data.subtasks.map(st => ({
id: st.task.id, id: st.task.id,
name: st.task.name || '', name: st.task.name || '',
@@ -1108,9 +1105,6 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
{subtask.rewards && subtask.rewards.length > 0 && ( {subtask.rewards && subtask.rewards.length > 0 && (
<div className="subtask-rewards"> <div className="subtask-rewards">
{subtask.rewards.map((reward, rIndex) => { {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 ( return (
<div key={rIndex} className="reward-item"> <div key={rIndex} className="reward-item">
<span className="reward-number">{rIndex}</span> <span className="reward-number">{rIndex}</span>