diff --git a/VERSION b/VERSION index d5c0c99..87ce492 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.1 +3.5.2 diff --git a/play-life-web/package.json b/play-life-web/package.json index 923025a..19c4d0a 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "3.5.0", + "version": "3.5.2", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/components/TaskList.css b/play-life-web/src/components/TaskList.css index 867fbff..41559c5 100644 --- a/play-life-web/src/components/TaskList.css +++ b/play-life-web/src/components/TaskList.css @@ -220,23 +220,43 @@ padding: 1.5rem; } -.task-postpone-task-name { - margin: 0 0 1rem 0; - font-size: 1rem; - color: #1f2937; - font-weight: 500; +.task-postpone-quick-buttons { + display: flex; + gap: 0.5rem; + margin-top: 0.5rem; } -.task-postpone-label { - display: flex; - flex-direction: column; - gap: 0.5rem; +.task-postpone-quick-button { + padding: 0.5rem 1rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; font-size: 0.875rem; font-weight: 500; color: #374151; + background: white; + cursor: pointer; + transition: all 0.2s; +} + +.task-postpone-quick-button:hover:not(:disabled) { + background: #f3f4f6; + border-color: #6366f1; + color: #6366f1; +} + +.task-postpone-quick-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.task-postpone-input-group { + display: flex; + gap: 0.5rem; + align-items: center; } .task-postpone-input { + flex: 1; padding: 0.75rem; border: 1px solid #d1d5db; border-radius: 0.375rem; @@ -250,43 +270,25 @@ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); } -.task-postpone-modal-actions { - padding: 1rem 1.5rem 1.5rem; - display: flex; - gap: 0.75rem; - justify-content: flex-end; -} - -.task-postpone-cancel-button, -.task-postpone-submit-button { +.task-postpone-submit-checkmark { 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; + min-width: 3rem; } -.task-postpone-cancel-button { - background: #f3f4f6; - color: #374151; +.task-postpone-submit-checkmark:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); } -.task-postpone-cancel-button:hover:not(:disabled) { - background: #e5e7eb; -} - -.task-postpone-submit-button { - background: #6366f1; - color: white; -} - -.task-postpone-submit-button:hover:not(:disabled) { - background: #4f46e5; -} - -.task-postpone-submit-button:disabled { +.task-postpone-submit-checkmark:disabled { opacity: 0.5; cursor: not-allowed; } diff --git a/play-life-web/src/components/TaskList.jsx b/play-life-web/src/components/TaskList.jsx index 8d3cf8d..bb0b1a3 100644 --- a/play-life-web/src/components/TaskList.jsx +++ b/play-life-web/src/components/TaskList.jsx @@ -163,6 +163,54 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { } } + // Функция для вычисления следующей даты по repetition_period + const calculateNextDateFromRepetitionPeriod = (repetitionPeriodStr) => { + if (!repetitionPeriodStr) return null + + const parts = repetitionPeriodStr.trim().split(/\s+/) + if (parts.length < 2) return null + + const value = parseInt(parts[0], 10) + if (isNaN(value) || value === 0) return null + + const unit = parts[1].toLowerCase() + const now = new Date() + now.setHours(0, 0, 0, 0) + + const nextDate = new Date(now) + + switch (unit) { + case 'minute': + case 'minutes': + nextDate.setMinutes(nextDate.getMinutes() + value) + break + case 'hour': + case 'hours': + nextDate.setHours(nextDate.getHours() + value) + break + case 'day': + case 'days': + nextDate.setDate(nextDate.getDate() + value) + break + case 'week': + case 'weeks': + nextDate.setDate(nextDate.getDate() + value * 7) + break + case 'month': + case 'months': + nextDate.setMonth(nextDate.getMonth() + value) + break + case 'year': + case 'years': + nextDate.setFullYear(nextDate.getFullYear() + value) + break + default: + return null + } + + return nextDate + } + // Форматирование даты в YYYY-MM-DD (локальное время, без смещения в UTC) const formatDateToLocal = (date) => { const year = date.getFullYear() @@ -177,17 +225,26 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { // Устанавливаем дату по умолчанию let defaultDate + const now = new Date() + now.setHours(0, 0, 0, 0) + if (task.repetition_date) { // Для задач с repetition_date - вычисляем следующую подходящую дату const nextDate = calculateNextDateFromRepetitionDate(task.repetition_date) if (nextDate) { defaultDate = nextDate } + } else if (task.repetition_period && !isZeroPeriod(task.repetition_period)) { + // Для задач с repetition_period (не нулевым) - вычисляем следующую дату + const nextDate = calculateNextDateFromRepetitionPeriod(task.repetition_period) + if (nextDate) { + defaultDate = nextDate + } } if (!defaultDate) { - // Без repetition_date или если не удалось вычислить - завтра - defaultDate = new Date() + // Без repetition_date/repetition_period или если не удалось вычислить - завтра + defaultDate = new Date(now) defaultDate.setDate(defaultDate.getDate() + 1) } @@ -197,11 +254,42 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { const handlePostponeSubmit = async () => { if (!selectedTaskForPostpone || !postponeDate) return + await handlePostponeSubmitWithDate(postponeDate) + } + + const handlePostponeClose = () => { + setSelectedTaskForPostpone(null) + setPostponeDate('') + } + + const handleTodayClick = () => { + const today = new Date() + today.setHours(0, 0, 0, 0) + setPostponeDate(formatDateToLocal(today)) + // Применяем дату сразу + if (selectedTaskForPostpone) { + handlePostponeSubmitWithDate(formatDateToLocal(today)) + } + } + + const handleTomorrowClick = () => { + const tomorrow = new Date() + tomorrow.setDate(tomorrow.getDate() + 1) + tomorrow.setHours(0, 0, 0, 0) + setPostponeDate(formatDateToLocal(tomorrow)) + // Применяем дату сразу + if (selectedTaskForPostpone) { + handlePostponeSubmitWithDate(formatDateToLocal(tomorrow)) + } + } + + const handlePostponeSubmitWithDate = async (dateToUse) => { + if (!selectedTaskForPostpone || !dateToUse) return setIsPostponing(true) try { // Преобразуем дату в ISO формат с временем - const dateObj = new Date(postponeDate) + const dateObj = new Date(dateToUse) dateObj.setHours(0, 0, 0, 0) const isoDate = dateObj.toISOString() @@ -234,45 +322,6 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { } } - const handlePostponeReset = 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() - } - - // Закрываем модальное окно - setSelectedTaskForPostpone(null) - setPostponeDate('') - } catch (err) { - console.error('Error resetting postpone:', err) - alert(err.message || 'Ошибка при сбросе переноса задачи') - } finally { - setIsPostponing(false) - } - } - - const handlePostponeClose = () => { - setSelectedTaskForPostpone(null) - setPostponeDate('') - } - const toggleCompletedExpanded = (projectName) => { setExpandedCompleted(prev => ({ ...prev, @@ -588,47 +637,75 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { )} {/* Модальное окно для переноса задачи */} - {selectedTaskForPostpone && ( -
-
e.stopPropagation()}> -
-

Перенести задачу

- -
-
-

{selectedTaskForPostpone.name}

- -
-
- - + {selectedTaskForPostpone && (() => { + const todayStr = formatDateToLocal(new Date()) + const tomorrow = new Date() + tomorrow.setDate(tomorrow.getDate() + 1) + const tomorrowStr = formatDateToLocal(tomorrow) + + // Проверяем next_show_at задачи, а не значение в поле ввода + let nextShowAtStr = null + if (selectedTaskForPostpone.next_show_at) { + const nextShowAtDate = new Date(selectedTaskForPostpone.next_show_at) + nextShowAtStr = formatDateToLocal(nextShowAtDate) + } + + const isToday = nextShowAtStr === todayStr + const isTomorrow = nextShowAtStr === tomorrowStr + + return ( +
+
e.stopPropagation()}> +
+

{selectedTaskForPostpone.name}

+ +
+
+
+ setPostponeDate(e.target.value)} + className="task-postpone-input" + min={new Date().toISOString().split('T')[0]} + /> + {postponeDate && ( + + )} +
+
+ {!isToday && ( + + )} + {!isTomorrow && ( + + )} +
+
-
- )} + ) + })()}
) }