5.8.0: Окно переноса и логика next_show_at
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m22s

This commit is contained in:
poignatov
2026-03-04 11:39:03 +03:00
parent 8ea71ef95f
commit 0f1f5e3943
5 changed files with 80 additions and 13 deletions

View File

@@ -1 +1 @@
5.7.0 5.8.0

View File

@@ -7773,9 +7773,9 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
-- Для невыполненных: сортируем по completed DESC (больше завершений выше), затем по id ASC (раньше добавленные выше) -- Для невыполненных: сортируем по completed DESC (больше завершений выше), затем по id ASC (раньше добавленные выше)
CASE WHEN t.last_completed_at IS NULL OR t.last_completed_at::date < CURRENT_DATE THEN -t.completed ELSE 0 END, CASE WHEN t.last_completed_at IS NULL OR t.last_completed_at::date < CURRENT_DATE THEN -t.completed ELSE 0 END,
CASE WHEN t.last_completed_at IS NULL OR t.last_completed_at::date < CURRENT_DATE THEN t.id ELSE 0 END, CASE WHEN t.last_completed_at IS NULL OR t.last_completed_at::date < CURRENT_DATE THEN t.id ELSE 0 END,
-- Для выполненных: сортируем по next_show_at ASC (ранние в начале), NULL значения в начале через COALESCE -- Для выполненных: сортируем по next_show_at ASC (ранние в начале), NULL значения в конце через COALESCE
CASE WHEN t.last_completed_at IS NOT NULL AND t.last_completed_at::date >= CURRENT_DATE CASE WHEN t.last_completed_at IS NOT NULL AND t.last_completed_at::date >= CURRENT_DATE
THEN COALESCE(t.next_show_at, '1970-01-01'::timestamp with time zone) THEN COALESCE(t.next_show_at, '9999-12-31'::timestamp with time zone)
ELSE '1970-01-01'::timestamp with time zone ELSE '1970-01-01'::timestamp with time zone
END END
` `
@@ -8566,12 +8566,21 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) {
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, wishlistIDValue, rewardPolicyValue, req.GroupName} insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, wishlistIDValue, rewardPolicyValue, req.GroupName}
} }
} else { } else {
// Получаем часовой пояс для задач без повторения
timezoneStr := getEnv("TIMEZONE", "UTC")
loc, err := time.LoadLocation(timezoneStr)
if err != nil {
log.Printf("Warning: Invalid timezone '%s': %v. Using UTC instead.", timezoneStr, err)
loc = time.UTC
}
now := time.Now().In(loc)
insertSQL = ` insertSQL = `
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, completed, deleted, wishlist_id, reward_policy, group_name) INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, next_show_at, completed, deleted, wishlist_id, reward_policy, group_name)
VALUES ($1, $2, $3, $4, NULL, NULL, 0, FALSE, $5, $6, $7) VALUES ($1, $2, $3, $4, NULL, NULL, $5, 0, FALSE, $6, $7, $8)
RETURNING id RETURNING id
` `
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, wishlistIDValue, rewardPolicyValue, req.GroupName} insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, now, wishlistIDValue, rewardPolicyValue, req.GroupName}
} }
err = tx.QueryRow(insertSQL, insertArgs...).Scan(&taskID) err = tx.QueryRow(insertSQL, insertArgs...).Scan(&taskID)

View File

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

View File

@@ -319,8 +319,8 @@
.task-postpone-modal { .task-postpone-modal {
background: white; background: white;
border-radius: 0.5rem; border-radius: 0.5rem;
max-width: 400px; width: fit-content;
width: 90%; max-width: 90%;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
} }
@@ -491,6 +491,16 @@
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
margin-top: 0.5rem; margin-top: 0.5rem;
overflow-x: auto;
flex-wrap: nowrap;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
-ms-overflow-style: none;
width: 100%;
}
.task-postpone-quick-buttons::-webkit-scrollbar {
display: none;
} }
.task-postpone-quick-button { .task-postpone-quick-button {
@@ -503,6 +513,8 @@
background: white; background: white;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
flex-shrink: 0;
white-space: nowrap;
} }
.task-postpone-quick-button:hover:not(:disabled) { .task-postpone-quick-button:hover:not(:disabled) {

View File

@@ -508,6 +508,42 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry
} }
} }
const handleWithoutDateClick = async () => {
if (!selectedTaskForPostpone) return
setIsPostponing(true)
try {
const response = await authFetch(`${API_URL}/${selectedTaskForPostpone.id}/postpone`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ next_show_at: null }),
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(errorData.message || 'Ошибка при переносе задачи')
}
if (onRefresh) {
onRefresh()
}
if (historyPushedForPostponeRef.current) {
window.history.back()
} else {
setSelectedTaskForPostpone(null)
setPostponeDate('')
}
} catch (err) {
console.error('Error postponing task:', err)
setToast({ message: err.message || 'Ошибка при переносе задачи', type: 'error' })
} finally {
setIsPostponing(false)
}
}
const toggleCompletedExpanded = (projectName) => { const toggleCompletedExpanded = (projectName) => {
setExpandedCompleted(prev => ({ setExpandedCompleted(prev => ({
...prev, ...prev,
@@ -622,7 +658,8 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry
nextShowDate.setHours(0, 0, 0, 0) nextShowDate.setHours(0, 0, 0, 0)
isCompleted = nextShowDate.getTime() > today.getTime() isCompleted = nextShowDate.getTime() > today.getTime()
} else { } else {
isCompleted = false // Задачи без даты (next_show_at = null) идут в выполненные
isCompleted = true
} }
groupKeys.forEach(groupKey => { groupKeys.forEach(groupKey => {
@@ -670,10 +707,10 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry
if (!isInfiniteA && isInfiniteB) return 1 if (!isInfiniteA && isInfiniteB) return 1
if (isInfiniteA && isInfiniteB) return 0 if (isInfiniteA && isInfiniteB) return 0
// Для остальных: NULL значения идут первыми // Для остальных: NULL значения идут последними
if (!a.next_show_at && !b.next_show_at) return 0 if (!a.next_show_at && !b.next_show_at) return 0
if (!a.next_show_at) return -1 if (!a.next_show_at) return 1
if (!b.next_show_at) return 1 if (!b.next_show_at) return -1
// Сравниваем даты // Сравниваем даты
const dateA = new Date(a.next_show_at).getTime() const dateA = new Date(a.next_show_at).getTime()
@@ -1182,6 +1219,15 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry
По плану По плану
</button> </button>
)} )}
{selectedTaskForPostpone?.next_show_at && (
<button
onClick={handleWithoutDateClick}
className="task-postpone-quick-button"
disabled={isPostponing}
>
Без даты
</button>
)}
</div> </div>
</div> </div>
</div> </div>