v3.6.0: Улучшено модальное окно переноса задачи - нередактируемое поле с понятным форматированием даты
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 55s

This commit is contained in:
poignatov
2026-01-10 19:17:03 +03:00
parent 3d3fa13f41
commit cc7c6a905e
7 changed files with 767 additions and 92 deletions

View File

@@ -17,6 +17,7 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
const [postponeDate, setPostponeDate] = useState('')
const [isPostponing, setIsPostponing] = useState(false)
const [toast, setToast] = useState(null)
const dateInputRef = useRef(null)
useEffect(() => {
if (data) {
@@ -34,43 +35,8 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
const handleCheckmarkClick = async (task, e) => {
e.stopPropagation()
const hasProgression = task.has_progression || task.progression_base != null
const hasSubtasks = task.subtasks_count > 0
if (hasProgression || hasSubtasks) {
// Открываем экран details
setSelectedTaskForDetail(task.id)
} else {
// Отправляем задачу
setIsCompleting(true)
try {
const response = await authFetch(`${API_URL}/${task.id}/complete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(errorData.message || 'Ошибка при выполнении задачи')
}
// Показываем toast о выполнении задачи
setToast({ message: 'Задача выполнена' })
// Обновляем список
if (onRefresh) {
onRefresh()
}
} catch (err) {
console.error('Error completing task:', err)
alert(err.message || 'Ошибка при выполнении задачи')
} finally {
setIsCompleting(false)
}
}
// Всегда открываем диалог подтверждения
setSelectedTaskForDetail(task.id)
}
const handleCloseDetail = () => {
@@ -210,6 +176,67 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
return `${year}-${month}-${day}`
}
// Форматирование даты для отображения с понятными названиями
const formatDateForDisplay = (dateStr) => {
if (!dateStr) return ''
// Парсим дату из формата YYYY-MM-DD
const dateParts = dateStr.split('-')
if (dateParts.length !== 3) return dateStr
const yearNum = parseInt(dateParts[0], 10)
const monthNum = parseInt(dateParts[1], 10) - 1 // месяцы в JS начинаются с 0
const dayNum = parseInt(dateParts[2], 10)
if (isNaN(yearNum) || isNaN(monthNum) || isNaN(dayNum)) return dateStr
const targetDate = new Date(yearNum, monthNum, dayNum)
targetDate.setHours(0, 0, 0, 0)
const now = new Date()
now.setHours(0, 0, 0, 0)
const diffDays = Math.floor((targetDate - now) / (1000 * 60 * 60 * 24))
// Сегодня
if (diffDays === 0) {
return 'Сегодня'
}
// Завтра
if (diffDays === 1) {
return 'Завтра'
}
// Вчера
if (diffDays === -1) {
return 'Вчера'
}
// Дни недели для ближайших дней из будущего (в пределах 7 дней)
if (diffDays > 0 && diffDays <= 7) {
const dayNames = ['Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота']
const dayOfWeek = targetDate.getDay()
return dayNames[dayOfWeek]
}
const monthNames = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря']
// Если это число из того же года - только день и месяц
if (targetDate.getFullYear() === now.getFullYear()) {
const displayDay = targetDate.getDate()
const displayMonth = monthNames[targetDate.getMonth()]
return `${displayDay} ${displayMonth}`
}
// Для других случаев - полная дата
const displayDay = targetDate.getDate()
const displayMonth = monthNames[targetDate.getMonth()]
const displayYear = targetDate.getFullYear()
return `${displayDay} ${displayMonth} ${displayYear}`
}
const handlePostponeClick = (task, e) => {
e.stopPropagation()
setSelectedTaskForPostpone(task)
@@ -436,6 +463,10 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
// Не проверяем next_show_at, так как для бесконечных задач он может быть установлен при выполнении
const isInfinite = (hasZeroPeriod && hasZeroDate) || (hasZeroPeriod && !task.repetition_date)
// Одноразовая задача: когда оба поля null/undefined
const isOneTime = (task.repetition_period == null || task.repetition_period === undefined) &&
(task.repetition_date == null || task.repetition_date === undefined)
// Отладка для задачи "Ролик"
if (task.name === 'Ролик') {
console.log('Task "Ролик":', {
@@ -508,6 +539,24 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
<path d="M12 12c0 2.5 1.5 4.5 3.5 4.5S19 14.5 19 12s-1.5-4.5-3.5-4.5S12 9.5 12 12z"/>
</svg>
)}
{isOneTime && (
<svg
className="task-onetime-icon"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
title="Одноразовая задача"
>
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="14"></line>
<circle cx="12" cy="18" r="1"></circle>
</svg>
)}
</span>
</div>
{/* Показываем дату только для выполненных задач */}
@@ -683,12 +732,30 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
<div className="task-postpone-modal-content">
<div className="task-postpone-input-group">
<input
ref={dateInputRef}
type="date"
value={postponeDate}
onChange={(e) => setPostponeDate(e.target.value)}
className="task-postpone-input"
min={new Date().toISOString().split('T')[0]}
/>
<div
className="task-postpone-display-date"
onClick={() => {
// Открываем календарь при клике
if (dateInputRef.current) {
if (typeof dateInputRef.current.showPicker === 'function') {
dateInputRef.current.showPicker()
} else {
// Fallback для браузеров без showPicker
dateInputRef.current.focus()
dateInputRef.current.click()
}
}
}}
>
{postponeDate ? formatDateForDisplay(postponeDate) : 'Выберите дату'}
</div>
{postponeDate && (
<button
onClick={handlePostponeSubmit}