v3.6.0: Улучшено модальное окно переноса задачи - нередактируемое поле с понятным форматированием даты
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 55s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 55s
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user