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:
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
45
play-life-backend/migrations/000005_add_task_drafts.up.sql
Normal file
45
play-life-backend/migrations/000005_add_task_drafts.up.sql
Normal 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';
|
||||||
BIN
play-life-backend/play-eng-backend
Executable file
BIN
play-life-backend/play-eng-backend
Executable file
Binary file not shown.
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
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 { 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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user