Add repetition_date support for tasks (v3.3.0)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 42s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 42s
- Add repetition_date field to tasks table (migration 018) - Support pattern-based repetition: day of week, day of month, specific date - Add 'Через'/'Каждое' mode selector in task form - Auto-calculate next_show_at from repetition_date on create/complete - Show calculated next date in postpone dialog for repetition_date tasks - Update version to 3.3.0
This commit is contained in:
@@ -90,14 +90,109 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Форматирование даты в 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 handlePostponeClick = (task, e) => {
|
||||
e.stopPropagation()
|
||||
setSelectedTaskForPostpone(task)
|
||||
// Устанавливаем дату по умолчанию - завтра
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
tomorrow.setHours(0, 0, 0, 0)
|
||||
setPostponeDate(tomorrow.toISOString().split('T')[0])
|
||||
|
||||
// Устанавливаем дату по умолчанию
|
||||
let defaultDate
|
||||
if (task.repetition_date) {
|
||||
// Для задач с repetition_date - вычисляем следующую подходящую дату
|
||||
const nextDate = calculateNextDateFromRepetitionDate(task.repetition_date)
|
||||
if (nextDate) {
|
||||
defaultDate = nextDate
|
||||
}
|
||||
}
|
||||
|
||||
if (!defaultDate) {
|
||||
// Без repetition_date или если не удалось вычислить - завтра
|
||||
defaultDate = new Date()
|
||||
defaultDate.setDate(defaultDate.getDate() + 1)
|
||||
}
|
||||
|
||||
defaultDate.setHours(0, 0, 0, 0)
|
||||
setPostponeDate(formatDateToLocal(defaultDate))
|
||||
}
|
||||
|
||||
const handlePostponeSubmit = async () => {
|
||||
@@ -267,6 +362,7 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
// Группируем задачи по проектам
|
||||
const groupedTasks = useMemo(() => {
|
||||
const today = new Date()
|
||||
@@ -287,7 +383,7 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
|
||||
let isInfinite = false
|
||||
|
||||
// Если next_show_at установлен, задача всегда в выполненных (если дата в будущем)
|
||||
// даже если она бесконечная
|
||||
// даже если она бесконечная (next_show_at приоритетнее всего)
|
||||
if (task.next_show_at) {
|
||||
const nextShowDate = new Date(task.next_show_at)
|
||||
nextShowDate.setHours(0, 0, 0, 0)
|
||||
@@ -324,7 +420,7 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Если repetition_period == null, используем старую логику
|
||||
// Если нет ни repetition_period, ни repetition_date, используем старую логику
|
||||
if (task.last_completed_at) {
|
||||
const completedDate = new Date(task.last_completed_at)
|
||||
completedDate.setHours(0, 0, 0, 0)
|
||||
|
||||
Reference in New Issue
Block a user