5.10.0: Капсула прогрессии в списке задач
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m21s

This commit is contained in:
poignatov
2026-03-04 12:24:34 +03:00
parent 91d4a7337c
commit 81dc23b501
6 changed files with 114 additions and 24 deletions

View File

@@ -1 +1 @@
5.9.0 5.10.0

View File

@@ -343,6 +343,7 @@ type Task struct {
SubtasksCount int `json:"subtasks_count"` SubtasksCount int `json:"subtasks_count"`
HasProgression bool `json:"has_progression"` HasProgression bool `json:"has_progression"`
AutoComplete bool `json:"auto_complete"` AutoComplete bool `json:"auto_complete"`
DraftProgressionValue *float64 `json:"draft_progression_value,omitempty"`
} }
type Reward struct { type Reward struct {
@@ -7765,7 +7766,8 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
WHERE st.parent_task_id = t.id AND st.deleted = FALSE), WHERE st.parent_task_id = t.id AND st.deleted = FALSE),
ARRAY[]::text[] ARRAY[]::text[]
) as subtask_project_names, ) as subtask_project_names,
COALESCE(td.auto_complete, FALSE) as auto_complete COALESCE(td.auto_complete, FALSE) as auto_complete,
td.progression_value as draft_progression_value
FROM tasks t FROM tasks t
LEFT JOIN task_drafts td ON td.task_id = t.id AND td.user_id = $1 LEFT JOIN task_drafts td ON td.task_id = t.id AND td.user_id = $1
WHERE t.user_id = $1 AND t.parent_task_id IS NULL AND t.deleted = FALSE WHERE t.user_id = $1 AND t.parent_task_id IS NULL AND t.deleted = FALSE
@@ -7805,6 +7807,7 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
var projectNames pq.StringArray var projectNames pq.StringArray
var subtaskProjectNames pq.StringArray var subtaskProjectNames pq.StringArray
var autoComplete bool var autoComplete bool
var draftProgressionValue sql.NullFloat64
err := rows.Scan( err := rows.Scan(
&task.ID, &task.ID,
@@ -7823,6 +7826,7 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
&projectNames, &projectNames,
&subtaskProjectNames, &subtaskProjectNames,
&autoComplete, &autoComplete,
&draftProgressionValue,
) )
if err != nil { if err != nil {
log.Printf("Error scanning task: %v", err) log.Printf("Error scanning task: %v", err)
@@ -7863,6 +7867,9 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
task.GroupName = &groupNameVal task.GroupName = &groupNameVal
} }
task.AutoComplete = autoComplete task.AutoComplete = autoComplete
if draftProgressionValue.Valid {
task.DraftProgressionValue = &draftProgressionValue.Float64
}
// Объединяем проекты из основной задачи и подзадач // Объединяем проекты из основной задачи и подзадач
allProjects := make(map[string]bool) allProjects := make(map[string]bool)

View File

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

View File

@@ -477,7 +477,7 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
children_task_ids: Array.from(selectedSubtasks) children_task_ids: Array.from(selectedSubtasks)
} }
// Если есть прогрессия, отправляем значение (или progression_base, если не введено) // Если есть прогрессия, отправляем значение (или null, если не введено)
if (taskDetail.task.progression_base != null) { if (taskDetail.task.progression_base != null) {
if (progressionValue.trim()) { if (progressionValue.trim()) {
const parsedValue = parseFloat(progressionValue) const parsedValue = parseFloat(progressionValue)
@@ -486,8 +486,8 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
} }
payload.progression_value = parsedValue payload.progression_value = parsedValue
} else { } else {
// Если прогрессия не введена - используем progression_base // Если прогрессия не введена - отправляем null
payload.progression_value = taskDetail.task.progression_base payload.progression_value = null
} }
} else { } else {
// Если нет progression_base, но пользователь ввел значение - отправляем его // Если нет progression_base, но пользователь ввел значение - отправляем его

View File

@@ -256,11 +256,41 @@
margin-left: 0.25rem; margin-left: 0.25rem;
} }
.task-progression-capsule {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
background: #f3f4f6;
border-radius: 9999px;
padding: 0.15rem 0.5rem;
cursor: pointer;
transition: background 0.2s;
min-height: 1.5rem;
}
.task-progression-capsule:hover {
background: #e5e7eb;
}
.task-progression-capsule--saving {
opacity: 0.6;
cursor: not-allowed;
}
.task-progression-icon { .task-progression-icon {
color: #9ca3af; color: #9ca3af;
flex-shrink: 0; flex-shrink: 0;
} }
.task-progression-value {
font-size: 0.8rem;
font-weight: 500;
color: #374151;
min-width: 1rem;
text-align: center;
}
.task-infinite-icon { .task-infinite-icon {
color: #9ca3af; color: #9ca3af;
flex-shrink: 0; flex-shrink: 0;

View File

@@ -23,6 +23,7 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry
const [isPostponing, setIsPostponing] = useState(false) const [isPostponing, setIsPostponing] = useState(false)
const [toast, setToast] = useState(null) const [toast, setToast] = useState(null)
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [savingProgressionTaskId, setSavingProgressionTaskId] = useState(null)
// Режим группировки: 'project' (по проекту - по умолчанию) или 'group' (по группе) // Режим группировки: 'project' (по проекту - по умолчанию) или 'group' (по группе)
const [groupingMode, setGroupingMode] = useState(() => { const [groupingMode, setGroupingMode] = useState(() => {
// Восстанавливаем из localStorage, по умолчанию 'project' // Восстанавливаем из localStorage, по умолчанию 'project'
@@ -551,6 +552,45 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry
})) }))
} }
const handleProgressionChange = async (task, delta) => {
if (savingProgressionTaskId === task.id) return
const currentValue = task.draft_progression_value ?? 0
const newValue = currentValue + delta
setSavingProgressionTaskId(task.id)
try {
const response = await authFetch(`${API_URL}/${task.id}/draft`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
progression_value: newValue,
auto_complete: false,
children_task_ids: []
}),
})
if (!response.ok) {
throw new Error('Ошибка при сохранении прогрессии')
}
setTasks(prevTasks =>
prevTasks.map(t =>
t.id === task.id
? { ...t, draft_progression_value: newValue }
: t
)
)
} catch (err) {
console.error('Error saving progression:', err)
setToast({ message: err.message || 'Ошибка при сохранении прогрессии', type: 'error' })
} finally {
setSavingProgressionTaskId(null)
}
}
// Получаем все проекты из задачи (теперь они приходят в task.project_names) // Получаем все проекты из задачи (теперь они приходят в task.project_names)
const getTaskProjects = (task) => { const getTaskProjects = (task) => {
if (task.project_names && Array.isArray(task.project_names)) { if (task.project_names && Array.isArray(task.project_names)) {
@@ -888,21 +928,34 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry
</svg> </svg>
)} )}
{hasProgression && ( {hasProgression && (
<span
className={`task-progression-capsule ${savingProgressionTaskId === task.id ? 'task-progression-capsule--saving' : ''}`}
onClick={(e) => {
e.stopPropagation()
if (savingProgressionTaskId !== task.id) {
handleProgressionChange(task, task.progression_base)
}
}}
title="Задача с прогрессией"
>
<svg <svg
className="task-progression-icon" className="task-progression-icon"
width="16" width="14"
height="16" height="14"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
strokeWidth="2" strokeWidth="2"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
title="Задача с прогрессией"
> >
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline> <polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline>
<polyline points="17 6 23 6 23 12"></polyline> <polyline points="17 6 23 6 23 12"></polyline>
</svg> </svg>
{task.draft_progression_value != null && (
<span className="task-progression-value">{task.draft_progression_value}</span>
)}
</span>
)} )}
</span> </span>
</div> </div>