diff --git a/VERSION b/VERSION index cb2b00e..fd2a018 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.1 +3.1.0 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index 0434b3b..3020301 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -28,6 +28,7 @@ import ( "github.com/gorilla/mux" "github.com/joho/godotenv" _ "github.com/lib/pq" + "github.com/lib/pq" "github.com/robfig/cron/v3" "golang.org/x/crypto/bcrypt" ) @@ -207,6 +208,10 @@ type Task struct { RewardMessage *string `json:"reward_message,omitempty"` ProgressionBase *float64 `json:"progression_base,omitempty"` RepetitionPeriod *string `json:"repetition_period,omitempty"` + // Дополнительные поля для списка задач (без omitempty чтобы всегда передавались) + ProjectNames []string `json:"project_names"` + SubtasksCount int `json:"subtasks_count"` + HasProgression bool `json:"has_progression"` } type Reward struct { @@ -6248,13 +6253,40 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) { return } + // Запрос с получением всех необходимых данных для группировки и отображения query := ` - SELECT id, name, completed, last_completed_at, repetition_period::text - FROM tasks - WHERE user_id = $1 AND parent_task_id IS NULL AND deleted = FALSE + SELECT + t.id, + t.name, + t.completed, + t.last_completed_at, + t.repetition_period::text, + t.progression_base, + COALESCE(( + SELECT COUNT(*) + FROM tasks st + WHERE st.parent_task_id = t.id AND st.deleted = FALSE + ), 0) as subtasks_count, + COALESCE( + (SELECT array_agg(DISTINCT p.name) FILTER (WHERE p.name IS NOT NULL) + FROM reward_configs rc + JOIN projects p ON rc.project_id = p.id + WHERE rc.task_id = t.id), + ARRAY[]::text[] + ) as project_names, + COALESCE( + (SELECT array_agg(DISTINCT p.name) FILTER (WHERE p.name IS NOT NULL) + FROM tasks st + JOIN reward_configs rc ON rc.task_id = st.id + JOIN projects p ON rc.project_id = p.id + WHERE st.parent_task_id = t.id AND st.deleted = FALSE), + ARRAY[]::text[] + ) as subtask_project_names + FROM tasks t + WHERE t.user_id = $1 AND t.parent_task_id IS NULL AND t.deleted = FALSE ORDER BY - CASE WHEN last_completed_at IS NULL OR last_completed_at::date < CURRENT_DATE THEN 0 ELSE 1 END, - name + CASE WHEN t.last_completed_at IS NULL OR t.last_completed_at::date < CURRENT_DATE THEN 0 ELSE 1 END, + t.name ` rows, err := a.DB.Query(query, userID) @@ -6270,8 +6302,21 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) { var task Task var lastCompletedAt sql.NullString var repetitionPeriod sql.NullString + var progressionBase sql.NullFloat64 + var projectNames pq.StringArray + var subtaskProjectNames pq.StringArray - err := rows.Scan(&task.ID, &task.Name, &task.Completed, &lastCompletedAt, &repetitionPeriod) + err := rows.Scan( + &task.ID, + &task.Name, + &task.Completed, + &lastCompletedAt, + &repetitionPeriod, + &progressionBase, + &task.SubtasksCount, + &projectNames, + &subtaskProjectNames, + ) if err != nil { log.Printf("Error scanning task: %v", err) continue @@ -6283,6 +6328,30 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) { if repetitionPeriod.Valid { task.RepetitionPeriod = &repetitionPeriod.String } + if progressionBase.Valid { + task.HasProgression = true + task.ProgressionBase = &progressionBase.Float64 + } else { + task.HasProgression = false + } + + // Объединяем проекты из основной задачи и подзадач + allProjects := make(map[string]bool) + for _, pn := range projectNames { + if pn != "" { + allProjects[pn] = true + } + } + for _, pn := range subtaskProjectNames { + if pn != "" { + allProjects[pn] = true + } + } + + task.ProjectNames = make([]string, 0, len(allProjects)) + for pn := range allProjects { + task.ProjectNames = append(task.ProjectNames, pn) + } tasks = append(tasks, task) } diff --git a/play-life-web/src/components/TaskList.css b/play-life-web/src/components/TaskList.css index 20191fe..4310a8d 100644 --- a/play-life-web/src/components/TaskList.css +++ b/play-life-web/src/components/TaskList.css @@ -89,11 +89,31 @@ color: #8b5cf6; } +.task-name-container { + flex: 1; + display: flex; + align-items: center; + gap: 0.5rem; +} + .task-name { font-size: 1rem; font-weight: 500; color: #1f2937; - flex: 1; + display: flex; + align-items: center; + gap: 0.25rem; +} + +.task-subtasks-count { + color: #9ca3af; + font-size: 0.875rem; + font-weight: 400; +} + +.task-progression-icon { + color: #9ca3af; + flex-shrink: 0; } .task-actions { diff --git a/play-life-web/src/components/TaskList.jsx b/play-life-web/src/components/TaskList.jsx index b90f8b7..ba08b61 100644 --- a/play-life-web/src/components/TaskList.jsx +++ b/play-life-web/src/components/TaskList.jsx @@ -10,8 +10,6 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { const { authFetch } = useAuth() // Инициализируем tasks из data, если data есть, иначе пустой массив const [tasks, setTasks] = useState(() => data && Array.isArray(data) ? data : []) - const [taskDetails, setTaskDetails] = useState({}) - const [loadingDetails, setLoadingDetails] = useState(false) const [selectedTaskForDetail, setSelectedTaskForDetail] = useState(null) const [isCompleting, setIsCompleting] = useState(false) const [expandedCompleted, setExpandedCompleted] = useState({}) @@ -25,9 +23,6 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { } }) const [toast, setToast] = useState(null) - - // Для отслеживания изменений в списке задач (чтобы не перезагружать детали без необходимости) - const lastTaskIdsRef = useRef('') useEffect(() => { if (data) { @@ -38,64 +33,6 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { // Загрузка данных управляется из App.jsx через loadTabData // TaskList не инициирует загрузку самостоятельно - // Загружаем детали для всех задач - // Оптимизация: загружаем только если список задач изменился (по id и last_completed_at) - useEffect(() => { - if (!tasks || tasks.length === 0) { - setTaskDetails({}) - lastTaskIdsRef.current = '' - return - } - - // Создаем ключ из id и last_completed_at всех задач - const taskKey = tasks.map(t => `${t.id}:${t.last_completed_at || ''}`).sort().join(',') - - // Если ключ не изменился, не перезагружаем детали - if (taskKey === lastTaskIdsRef.current) { - return - } - - lastTaskIdsRef.current = taskKey - - const loadTaskDetails = async () => { - // Не показываем индикатор загрузки если детали уже есть (фоновое обновление) - const hasExistingDetails = Object.keys(taskDetails).length > 0 - if (!hasExistingDetails) { - setLoadingDetails(true) - } - - try { - const detailPromises = tasks.map(async (task) => { - try { - const response = await authFetch(`${API_URL}/${task.id}`) - if (response.ok) { - const detail = await response.json() - return { taskId: task.id, detail } - } - } catch (err) { - console.error(`Error loading task detail for ${task.id}:`, err) - } - return null - }) - - const details = await Promise.all(detailPromises) - const detailsMap = {} - details.forEach(item => { - if (item) { - detailsMap[item.taskId] = item.detail - } - }) - setTaskDetails(detailsMap) - } catch (err) { - console.error('Error loading task details:', err) - } finally { - setLoadingDetails(false) - } - } - - loadTaskDetails() - }, [tasks, authFetch]) - const handleTaskClick = (task) => { onNavigate?.('task-form', { taskId: task.id }) } @@ -103,9 +40,8 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { const handleCheckmarkClick = async (task, e) => { e.stopPropagation() - const detail = taskDetails[task.id] - const hasProgression = detail?.task?.progression_base != null - const hasSubtasks = detail?.subtasks && detail.subtasks.length > 0 + const hasProgression = task.has_progression || task.progression_base != null + const hasSubtasks = task.subtasks_count > 0 if (hasProgression || hasSubtasks) { // Открываем экран details @@ -174,36 +110,12 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { }) } - // Получаем все проекты из наград задачи и подзадач + // Получаем все проекты из задачи (теперь они приходят в task.project_names) const getTaskProjects = (task) => { - const projects = new Set() - const detail = taskDetails[task.id] - - if (detail) { - // Проекты из основной задачи - if (detail.rewards) { - detail.rewards.forEach(reward => { - if (reward.project_name) { - projects.add(reward.project_name) - } - }) - } - - // Проекты из подзадач - if (detail.subtasks) { - detail.subtasks.forEach(subtask => { - if (subtask.rewards) { - subtask.rewards.forEach(reward => { - if (reward.project_name) { - projects.add(reward.project_name) - } - }) - } - }) - } + if (task.project_names && Array.isArray(task.project_names)) { + return task.project_names } - - return Array.from(projects) + return [] } // Функция для проверки, является ли период нулевым @@ -340,12 +252,11 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { }) return groups - }, [tasks, taskDetails]) + }, [tasks]) const renderTaskItem = (task) => { - const detail = taskDetails[task.id] - const hasProgression = detail?.task?.progression_base != null - const hasSubtasks = detail?.subtasks && detail.subtasks.length > 0 + const hasProgression = task.has_progression || task.progression_base != null + const hasSubtasks = task.subtasks_count > 0 const showDetailOnCheckmark = hasProgression || hasSubtasks return ( @@ -365,7 +276,31 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { -
{task.name}
+
+
+ {task.name} + {hasSubtasks && ( + (+{task.subtasks_count}) + )} +
+ {hasProgression && ( + + + + + )} +
{task.completed}
@@ -408,10 +343,6 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { Добавить - {loadingDetails && tasks.length > 0 && ( -
Загрузка деталей задач...
- )} - {projectNames.length === 0 && !loading && tasks.length === 0 && (

Задач пока нет. Добавьте задачу через кнопку "Добавить".

diff --git a/run.sh b/run.sh index 64d9c2a..a8fcdcd 100755 --- a/run.sh +++ b/run.sh @@ -39,15 +39,19 @@ echo "" # Проверяем, запущены ли контейнеры if docker-compose ps | grep -q "Up"; then echo -e "${YELLOW}Перезапуск существующих контейнеров...${NC}" - echo " - Backend сервер" + echo " - Backend сервер (с пересборкой)" echo " - Frontend приложение (с пересборкой)" echo " - База данных" # Пересобираем и перезапускаем веб-сервер с новыми изменениями echo -e "${BLUE}Пересборка веб-приложения...${NC}" docker-compose build play-life-web docker-compose up -d play-life-web - # Перезапускаем остальные сервисы - docker-compose restart backend db + # Пересобираем и перезапускаем бэкенд с новыми изменениями + echo -e "${BLUE}Пересборка бэкенда...${NC}" + docker-compose build backend + docker-compose up -d --force-recreate backend + # Перезапускаем базу данных + docker-compose restart db echo -e "${GREEN}✅ Контейнеры перезапущены${NC}" else echo -e "${YELLOW}Запуск контейнеров...${NC}"