Files
play-life/play-life-web/src/components/TaskList.jsx
poignatov f3a7d1c503
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 54s
fix: исправлен расчет даты переноса задач с периодами повторения
- Добавлена поддержка сокращенных форм единиц времени (mons, min, hrs, wks, yrs и т.д.)
- Исправлена обработка недель, которые PostgreSQL возвращает как дни (7 days вместо 1 week)
- Добавлено приведение repetition_period к тексту при чтении из БД
- Обновлена версия до 3.8.6
2026-01-11 15:09:32 +03:00

817 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useMemo, useRef } from 'react'
import { useAuth } from './auth/AuthContext'
import TaskDetail from './TaskDetail'
import Toast from './Toast'
import './TaskList.css'
const API_URL = '/api/tasks'
function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
const { authFetch } = useAuth()
// Инициализируем tasks из data, если data есть, иначе пустой массив
const [tasks, setTasks] = useState(() => data && Array.isArray(data) ? data : [])
const [selectedTaskForDetail, setSelectedTaskForDetail] = useState(null)
const [isCompleting, setIsCompleting] = useState(false)
const [expandedCompleted, setExpandedCompleted] = useState({})
const [selectedTaskForPostpone, setSelectedTaskForPostpone] = useState(null)
const [postponeDate, setPostponeDate] = useState('')
const [isPostponing, setIsPostponing] = useState(false)
const [toast, setToast] = useState(null)
const dateInputRef = useRef(null)
useEffect(() => {
if (data) {
setTasks(data)
}
}, [data])
// Загрузка данных управляется из App.jsx через loadTabData
// TaskList не инициирует загрузку самостоятельно
const handleTaskClick = (task) => {
onNavigate?.('task-form', { taskId: task.id })
}
const handleCheckmarkClick = async (task, e) => {
e.stopPropagation()
// Всегда открываем диалог подтверждения
setSelectedTaskForDetail(task.id)
}
const handleCloseDetail = () => {
setSelectedTaskForDetail(null)
}
const handleAddClick = () => {
onNavigate?.('task-form', { taskId: undefined })
}
// Функция для вычисления следующей даты по repetition_date
const calculateNextDateFromRepetitionDate = (repetitionDateStr) => {
if (!repetitionDateStr) return null
const parts = repetitionDateStr.trim().split(/\s+/)
if (parts.length < 2) return null
const value = parts[0]
const unit = parts[1].toLowerCase()
const now = new Date()
now.setHours(0, 0, 0, 0)
switch (unit) {
case 'week': {
// N-й день недели (1=понедельник, 7=воскресенье)
const dayOfWeek = parseInt(value, 10)
if (isNaN(dayOfWeek) || dayOfWeek < 1 || dayOfWeek > 7) return null
// JavaScript: 0=воскресенье, 1=понедельник... 6=суббота
// Наш формат: 1=понедельник... 7=воскресенье
// Конвертируем: наш 1 (Пн) -> JS 1, наш 7 (Вс) -> JS 0
const targetJsDay = dayOfWeek === 7 ? 0 : dayOfWeek
const currentJsDay = now.getDay()
// Вычисляем дни до следующего вхождения (включая сегодня, если ещё не прошло)
let daysUntil = (targetJsDay - currentJsDay + 7) % 7
// Если сегодня тот же день, берём следующую неделю
if (daysUntil === 0) daysUntil = 7
const nextDate = new Date(now)
nextDate.setDate(now.getDate() + daysUntil)
return nextDate
}
case 'month': {
// N-й день месяца
const dayOfMonth = parseInt(value, 10)
if (isNaN(dayOfMonth) || dayOfMonth < 1 || dayOfMonth > 31) return null
// Ищем ближайшую дату с этим днём
let searchDate = new Date(now)
for (let i = 0; i < 12; i++) {
const year = searchDate.getFullYear()
const month = searchDate.getMonth()
const lastDayOfMonth = new Date(year, month + 1, 0).getDate()
const actualDay = Math.min(dayOfMonth, lastDayOfMonth)
const candidateDate = new Date(year, month, actualDay)
if (candidateDate > now) {
return candidateDate
}
// Переходим к следующему месяцу
searchDate = new Date(year, month + 1, 1)
}
return null
}
case 'year': {
// MM-DD формат
const dateParts = value.split('-')
if (dateParts.length !== 2) return null
const monthNum = parseInt(dateParts[0], 10)
const day = parseInt(dateParts[1], 10)
if (isNaN(monthNum) || isNaN(day) || monthNum < 1 || monthNum > 12 || day < 1 || day > 31) return null
let year = now.getFullYear()
let candidateDate = new Date(year, monthNum - 1, day)
if (candidateDate <= now) {
candidateDate = new Date(year + 1, monthNum - 1, day)
}
return candidateDate
}
default:
return null
}
}
// Функция для вычисления следующей даты по 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':
case 'mins':
case 'min':
nextDate.setMinutes(nextDate.getMinutes() + value)
break
case 'hour':
case 'hours':
case 'hrs':
case 'hr':
nextDate.setHours(nextDate.getHours() + value)
break
case 'day':
case 'days':
// PostgreSQL может возвращать недели как дни (например, "7 days" вместо "1 week")
// Если количество дней кратно 7, обрабатываем как недели
if (value % 7 === 0 && value >= 7) {
const weeks = value / 7
nextDate.setDate(nextDate.getDate() + weeks * 7)
} else {
nextDate.setDate(nextDate.getDate() + value)
}
break
case 'week':
case 'weeks':
case 'wks':
case 'wk':
nextDate.setDate(nextDate.getDate() + value * 7)
break
case 'month':
case 'months':
case 'mons':
case 'mon':
nextDate.setMonth(nextDate.getMonth() + value)
break
case 'year':
case 'years':
case 'yrs':
case 'yr':
nextDate.setFullYear(nextDate.getFullYear() + value)
break
default:
return null
}
return nextDate
}
// Форматирование даты в YYYY-MM-DD (локальное время, без смещения в UTC)
const formatDateToLocal = (date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
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)
// Устанавливаем дату по умолчанию
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/repetition_period или если не удалось вычислить - завтра
defaultDate = new Date(now)
defaultDate.setDate(defaultDate.getDate() + 1)
}
defaultDate.setHours(0, 0, 0, 0)
setPostponeDate(formatDateToLocal(defaultDate))
}
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(dateToUse)
dateObj.setHours(0, 0, 0, 0)
const isoDate = dateObj.toISOString()
const response = await authFetch(`${API_URL}/${selectedTaskForPostpone.id}/postpone`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ next_show_at: isoDate }),
})
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 postponing task:', err)
alert(err.message || 'Ошибка при переносе задачи')
} finally {
setIsPostponing(false)
}
}
const toggleCompletedExpanded = (projectName) => {
setExpandedCompleted(prev => ({
...prev,
[projectName]: !prev[projectName]
}))
}
// Получаем все проекты из задачи (теперь они приходят в task.project_names)
const getTaskProjects = (task) => {
if (task.project_names && Array.isArray(task.project_names)) {
return task.project_names
}
return []
}
// Функция для проверки, является ли период нулевым
const isZeroPeriod = (intervalStr) => {
if (!intervalStr) return false
const trimmed = intervalStr.trim()
// Проверяем формат времени "00:00:00" или "0:00:00"
if (/^\d{1,2}:\d{2}:\d{2}/.test(trimmed)) {
const timeParts = trimmed.split(':')
if (timeParts.length >= 3) {
const hours = parseInt(timeParts[0], 10)
const minutes = parseInt(timeParts[1], 10)
const seconds = parseInt(timeParts[2], 10)
return !isNaN(hours) && !isNaN(minutes) && !isNaN(seconds) &&
hours === 0 && minutes === 0 && seconds === 0
}
}
// PostgreSQL может возвращать "0 day", "0 days", "0", и т.д.
const parts = trimmed.split(/\s+/)
if (parts.length < 1) return false
const value = parseInt(parts[0], 10)
return !isNaN(value) && value === 0
}
// Функция для проверки, является ли repetition_date нулевым
const isZeroDate = (dateStr) => {
if (!dateStr) return false
const trimmed = dateStr.trim()
const parts = trimmed.split(/\s+/)
if (parts.length < 2) return false
const value = parts[0]
// Проверяем, является ли значение "0" (для формата "0 week", "0 month", "0 year")
const numValue = parseInt(value, 10)
return !isNaN(numValue) && numValue === 0
}
// Группируем задачи по проектам
const groupedTasks = useMemo(() => {
const today = new Date()
today.setHours(0, 0, 0, 0)
const groups = {}
tasks.forEach(task => {
const projects = getTaskProjects(task)
// Если у задачи нет проектов, добавляем в группу "Без проекта"
if (projects.length === 0) {
projects.push('Без проекта')
}
// Определяем, в какую группу попадает задача
let isCompleted = false
let isInfinite = false
// Используем только next_show_at для группировки
if (task.next_show_at) {
const nextShowDate = new Date(task.next_show_at)
nextShowDate.setHours(0, 0, 0, 0)
isCompleted = nextShowDate.getTime() > today.getTime()
isInfinite = false
} else {
// Бесконечная задача: repetition_period == 0 И (repetition_date == 0 ИЛИ отсутствует)
// Для обратной совместимости: если repetition_period = 0, считаем бесконечной
const hasZeroPeriod = task.repetition_period && isZeroPeriod(task.repetition_period)
const hasZeroDate = task.repetition_date && isZeroDate(task.repetition_date)
// Идеально: оба поля = 0, но для старых задач может быть только repetition_period = 0
isInfinite = (hasZeroPeriod && hasZeroDate) || (hasZeroPeriod && !task.repetition_date)
isCompleted = false
}
projects.forEach(projectName => {
if (!groups[projectName]) {
groups[projectName] = {
notCompleted: [],
completed: []
}
}
if (isCompleted) {
groups[projectName].completed.push(task)
} else {
// Бесконечные задачи теперь идут в обычный список
groups[projectName].notCompleted.push(task)
}
})
})
return groups
}, [tasks])
const renderTaskItem = (task, isCompleted = false) => {
const hasProgression = task.has_progression || task.progression_base != null
const hasSubtasks = task.subtasks_count > 0
const showDetailOnCheckmark = hasProgression || hasSubtasks
// Проверяем бесконечную задачу: repetition_period = 0 И (repetition_date = 0 ИЛИ отсутствует)
// Для обратной совместимости: если repetition_period = 0, считаем бесконечной
const hasZeroPeriod = task.repetition_period && isZeroPeriod(task.repetition_period)
const hasZeroDate = task.repetition_date && isZeroDate(task.repetition_date)
// Бесконечная задача: repetition_period = 0 И (repetition_date = 0 ИЛИ отсутствует)
// Не проверяем 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 "Ролик":', {
name: task.name,
repetition_period: task.repetition_period,
repetition_date: task.repetition_date,
next_show_at: task.next_show_at,
hasZeroPeriod,
hasZeroDate,
isInfinite
})
}
return (
<div
key={task.id}
className="task-item"
onClick={() => handleTaskClick(task)}
>
<div className="task-item-content">
<div
className={`task-checkmark ${showDetailOnCheckmark ? 'task-checkmark-detail' : ''}`}
onClick={(e) => handleCheckmarkClick(task, e)}
title={showDetailOnCheckmark ? 'Открыть детали' : 'Выполнить задачу'}
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="9" stroke="currentColor" strokeWidth="2" fill="none" className="checkmark-circle" />
<path d="M6 10 L9 13 L14 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="checkmark-check" />
</svg>
</div>
<div className="task-name-container">
<div className="task-name-wrapper">
<div className="task-name">
{task.name}
{hasSubtasks && (
<span className="task-subtasks-count">(+{task.subtasks_count})</span>
)}
<span className="task-badge-bar">
{hasProgression && (
<svg
className="task-progression-icon"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
title="Задача с прогрессией"
>
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline>
<polyline points="17 6 23 6 23 12"></polyline>
</svg>
)}
{isInfinite && (
<svg
className="task-infinite-icon"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
title="Бесконечная задача"
>
<path d="M12 12c0-2.5-1.5-4.5-3.5-4.5S5 9.5 5 12s1.5 4.5 3.5 4.5S12 14.5 12 12z"/>
<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>
{/* Показываем дату только для выполненных задач */}
{isCompleted && task.next_show_at && (() => {
const showDate = new Date(task.next_show_at)
// Нормализуем дату: устанавливаем время в 00:00:00 в локальном времени
const showDateNormalized = new Date(showDate.getFullYear(), showDate.getMonth(), showDate.getDate())
const today = new Date()
const todayNormalized = new Date(today.getFullYear(), today.getMonth(), today.getDate())
const tomorrowNormalized = new Date(todayNormalized)
tomorrowNormalized.setDate(tomorrowNormalized.getDate() + 1)
// Не показываем текст если дата равна сегодня
if (showDateNormalized.getTime() === todayNormalized.getTime()) {
return null
}
let dateText
if (showDateNormalized.getTime() === tomorrowNormalized.getTime()) {
dateText = 'Завтра'
} else {
dateText = showDate.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' })
}
return (
<div className="task-next-show-date">
{dateText}
</div>
)
})()}
</div>
</div>
<div className="task-actions">
<button
className="task-postpone-button"
onClick={(e) => handlePostponeClick(task, e)}
title="Перенести задачу"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="8" stroke="currentColor" strokeWidth="1.5" fill="none"/>
<path d="M10 5V10L13 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" fill="none"/>
</svg>
</button>
</div>
</div>
</div>
)
}
// Показываем загрузку только если данных нет и это не фоновая загрузка
// Проверяем наличие данных более надежно: либо в data, либо в tasks
// Важно: проверяем оба источника данных, так как они могут обновляться асинхронно
const hasDataInProps = data && Array.isArray(data) && data.length > 0
const hasDataInState = tasks && Array.isArray(tasks) && tasks.length > 0
const hasData = hasDataInProps || hasDataInState
// Показываем загрузку только если:
// 1. Идет загрузка (loading = true)
// 2. Это не фоновая загрузка (backgroundLoading = false)
// 3. Данных нет (hasData = false)
// Это предотвращает показ загрузки при переключении табов, когда данные уже есть
if (loading && !backgroundLoading && !hasData) {
return (
<div className="task-list">
<div className="loading">Загрузка...</div>
</div>
)
}
const projectNames = Object.keys(groupedTasks).sort()
return (
<div className="task-list">
{toast && (
<Toast
message={toast.message}
onClose={() => setToast(null)}
/>
)}
<button onClick={handleAddClick} className="add-task-button">
Добавить
</button>
{projectNames.length === 0 && !loading && tasks.length === 0 && (
<div className="empty-state">
<p>Задач пока нет. Добавьте задачу через кнопку "Добавить".</p>
</div>
)}
{projectNames.map(projectName => {
const group = groupedTasks[projectName]
const hasCompleted = group.completed.length > 0
const isCompletedExpanded = expandedCompleted[projectName]
return (
<div key={projectName} className="project-group">
<div className="project-group-header">
<h3 className="project-group-title">{projectName}</h3>
</div>
{/* Обычные задачи (включая бесконечные) */}
{group.notCompleted.length > 0 && (
<div className="task-group">
{group.notCompleted.map(task => renderTaskItem(task, false))}
</div>
)}
{/* Выполненные задачи */}
{hasCompleted && (
<div className="completed-section">
<button
className="completed-toggle"
onClick={() => toggleCompletedExpanded(projectName)}
>
<span className="completed-toggle-icon">
{isCompletedExpanded ? '▼' : '▶'}
</span>
<span>Выполненные ({group.completed.length})</span>
</button>
{isCompletedExpanded && (
<div className="task-group completed-tasks">
{group.completed.map(task => renderTaskItem(task, true))}
</div>
)}
</div>
)}
{group.notCompleted.length === 0 && !hasCompleted && (
<div className="empty-group">Нет задач в этой группе</div>
)}
</div>
)
})}
{/* Модальное окно для деталей задачи */}
{selectedTaskForDetail && (
<TaskDetail
taskId={selectedTaskForDetail}
onClose={handleCloseDetail}
onRefresh={onRefresh}
onTaskCompleted={() => setToast({ message: 'Задача выполнена' })}
/>
)}
{/* Модальное окно для переноса задачи */}
{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 (
<div className="task-postpone-modal-overlay" onClick={handlePostponeClose}>
<div className="task-postpone-modal" onClick={(e) => e.stopPropagation()}>
<div className="task-postpone-modal-header">
<h3>{selectedTaskForPostpone.name}</h3>
<button onClick={handlePostponeClose} className="task-postpone-close-button">
</button>
</div>
<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}
disabled={isPostponing || !postponeDate}
className="task-postpone-submit-checkmark"
>
</button>
)}
</div>
<div className="task-postpone-quick-buttons">
{!isToday && (
<button
onClick={handleTodayClick}
className="task-postpone-quick-button"
disabled={isPostponing}
>
Сегодня
</button>
)}
{!isTomorrow && (
<button
onClick={handleTomorrowClick}
className="task-postpone-quick-button"
disabled={isPostponing}
>
Завтра
</button>
)}
</div>
</div>
</div>
</div>
)
})()}
</div>
)
}
export default TaskList