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 }) {
Задач пока нет. Добавьте задачу через кнопку "Добавить".
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}"