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

- 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:
poignatov
2026-01-06 16:41:54 +03:00
parent 508355dcb3
commit b41f6e7cdc
6 changed files with 473 additions and 74 deletions

View File

@@ -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)