diff --git a/VERSION b/VERSION index 87ce492..444877d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.2 +3.5.3 diff --git a/play-life-web/package.json b/play-life-web/package.json index 19c4d0a..a4f00e9 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "3.5.2", + "version": "3.5.3", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/components/TaskForm.jsx b/play-life-web/src/components/TaskForm.jsx index 34e31bf..26a1d6c 100644 --- a/play-life-web/src/components/TaskForm.jsx +++ b/play-life-web/src/components/TaskForm.jsx @@ -78,9 +78,21 @@ function TaskForm({ onNavigate, taskId }) { setRewardMessage(data.task.reward_message || '') setProgressionBase(data.task.progression_base ? String(data.task.progression_base) : '') - // Парсим repetition_date если он есть (приоритет над repetition_period) - if (data.task.repetition_date) { - const dateStr = data.task.repetition_date.trim() + // Проверяем, является ли задача бесконечной (оба поля = 0) + const periodStr = data.task.repetition_period ? data.task.repetition_period.trim() : '' + const dateStr = data.task.repetition_date ? data.task.repetition_date.trim() : '' + const isPeriodZero = periodStr && (periodStr === '0 day' || periodStr.startsWith('0 ')) + const isDateZero = dateStr && (dateStr === '0 week' || dateStr.startsWith('0 ')) + const isInfinite = isPeriodZero && isDateZero + + if (isInfinite) { + // Бесконечная задача: показываем 0 в форме + setRepetitionPeriodValue('0') + setRepetitionPeriodType('day') + setRepetitionMode('after') + console.log('Loading infinite task: both repetition_period and repetition_date are 0') + } else if (data.task.repetition_date) { + // Парсим repetition_date если он есть (приоритет над repetition_period) console.log('Parsing repetition_date:', dateStr) // Отладка // Формат: "N unit" где unit = week, month, year @@ -415,15 +427,26 @@ function TaskForm({ onNavigate, taskId }) { if (repetitionPeriodValue && repetitionPeriodValue.trim() !== '') { const valueStr = repetitionPeriodValue.trim() + const value = parseInt(valueStr, 10) - if (repetitionMode === 'each') { + // Проверяем, является ли значение нулевым (бесконечная задача) + const isZero = !isNaN(value) && value === 0 + + if (isZero) { + // Бесконечная задача: устанавливаем оба поля в 0 + // Для repetition_period используем "0 day" + repetitionPeriod = '0 day' + // Для repetition_date используем "0 week" (можно использовать любой тип, но week - наиболее универсальный) + repetitionDate = '0 week' + console.log('Creating infinite task: repetition_period=0 day, repetition_date=0 week') + } else if (repetitionMode === 'each') { // Режим "Каждое" - сохраняем как repetition_date // Формат: "N unit" где unit = week, month, year repetitionDate = `${valueStr} ${repetitionPeriodType}` + repetitionPeriod = null // Убеждаемся, что repetition_period = null console.log('Sending repetition_date:', repetitionDate) } else { // Режим "Через" - сохраняем как repetition_period (INTERVAL) - const value = parseInt(valueStr, 10) if (!isNaN(value) && value >= 0) { const typeMap = { 'minute': 'minute', @@ -435,12 +458,24 @@ function TaskForm({ onNavigate, taskId }) { } const unit = typeMap[repetitionPeriodType] || 'day' repetitionPeriod = `${value} ${unit}` + repetitionDate = null // Убеждаемся, что repetition_date = null console.log('Sending repetition_period:', repetitionPeriod, 'from value:', repetitionPeriodValue, 'type:', repetitionPeriodType) } } } else { console.log('No repetition to send (value:', repetitionPeriodValue, 'type:', repetitionPeriodType, 'mode:', repetitionMode, ')') } + + // Валидация: если repetition_period != null, то repetition_date == null и наоборот, кроме случая когда они оба == 0 + if (repetitionPeriod && repetitionDate) { + const isPeriodZero = repetitionPeriod.trim() === '0 day' || repetitionPeriod.trim().startsWith('0 ') + const isDateZero = repetitionDate.trim() === '0 week' || repetitionDate.trim().startsWith('0 ') + if (!isPeriodZero || !isDateZero) { + setError('Нельзя одновременно использовать repetition_period и repetition_date, кроме случая бесконечной задачи (оба = 0)') + setLoading(false) + return + } + } const payload = { name: name.trim(), diff --git a/play-life-web/src/components/TaskList.css b/play-life-web/src/components/TaskList.css index 41559c5..ba89e51 100644 --- a/play-life-web/src/components/TaskList.css +++ b/play-life-web/src/components/TaskList.css @@ -94,6 +94,8 @@ display: flex; align-items: center; gap: 0.5rem; + min-width: 0; + overflow: hidden; } .task-name-wrapper { @@ -101,6 +103,8 @@ display: flex; flex-direction: column; gap: 0.125rem; + min-width: 0; + overflow: hidden; } .task-name { @@ -110,6 +114,11 @@ display: flex; align-items: center; gap: 0.25rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + flex: 1; } .task-next-show-date { @@ -124,11 +133,23 @@ font-weight: 400; } +.task-badge-bar { + display: inline-flex; + align-items: center; + gap: 0.25rem; + margin-left: 0.25rem; +} + .task-progression-icon { color: #9ca3af; flex-shrink: 0; } +.task-infinite-icon { + color: #9ca3af; + flex-shrink: 0; +} + .task-actions { display: flex; align-items: center; @@ -181,8 +202,7 @@ } .task-postpone-modal-header { - padding: 1.5rem; - border-bottom: 1px solid #e5e7eb; + padding: 1rem 1.5rem; display: flex; justify-content: space-between; align-items: center; @@ -217,7 +237,7 @@ } .task-postpone-modal-content { - padding: 1.5rem; + padding: 0 1.5rem 1.5rem 1.5rem; } .task-postpone-quick-buttons { diff --git a/play-life-web/src/components/TaskList.jsx b/play-life-web/src/components/TaskList.jsx index bb0b1a3..9f462e2 100644 --- a/play-life-web/src/components/TaskList.jsx +++ b/play-life-web/src/components/TaskList.jsx @@ -16,15 +16,6 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { const [selectedTaskForPostpone, setSelectedTaskForPostpone] = useState(null) const [postponeDate, setPostponeDate] = useState('') const [isPostponing, setIsPostponing] = useState(false) - // Загружаем состояние раскрытия "Бесконечные" из localStorage (по умолчанию true) - const [expandedInfinite, setExpandedInfinite] = useState(() => { - try { - const saved = localStorage.getItem('taskList_expandedInfinite') - return saved ? JSON.parse(saved) : {} - } catch { - return {} - } - }) const [toast, setToast] = useState(null) useEffect(() => { @@ -329,22 +320,6 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { })) } - const toggleInfiniteExpanded = (projectName) => { - setExpandedInfinite(prev => { - const newState = { - ...prev, - [projectName]: !prev[projectName] - } - // Сохраняем в localStorage - try { - localStorage.setItem('taskList_expandedInfinite', JSON.stringify(newState)) - } catch (err) { - console.error('Error saving expandedInfinite to localStorage:', err) - } - return newState - }) - } - // Получаем все проекты из задачи (теперь они приходят в task.project_names) const getTaskProjects = (task) => { if (task.project_names && Array.isArray(task.project_names)) { @@ -357,13 +332,42 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { const isZeroPeriod = (intervalStr) => { if (!intervalStr) return false - const parts = intervalStr.trim().split(/\s+/) + 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() @@ -389,30 +393,28 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { nextShowDate.setHours(0, 0, 0, 0) isCompleted = nextShowDate.getTime() > today.getTime() isInfinite = false - } else if (task.repetition_period && isZeroPeriod(task.repetition_period)) { - // Если у задачи период повторения = 0 и нет next_show_at, она в бесконечных - isInfinite = true - isCompleted = false } else { - // Если нет next_show_at и период не 0, задача в обычных + // Бесконечная задача: 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 - isInfinite = false } projects.forEach(projectName => { if (!groups[projectName]) { groups[projectName] = { notCompleted: [], - completed: [], - infinite: [] + completed: [] } } - if (isInfinite) { - groups[projectName].infinite.push(task) - } else if (isCompleted) { + if (isCompleted) { groups[projectName].completed.push(task) } else { + // Бесконечные задачи теперь идут в обычный список groups[projectName].notCompleted.push(task) } }) @@ -425,6 +427,27 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { 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) + + // Отладка для задачи "Ролик" + 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 (
(+{task.subtasks_count}) )} - {hasProgression && ( - - - - - )} + + {hasProgression && ( + + + + + )} + {isInfinite && ( + + + + + )} +
{task.next_show_at && (() => { const showDate = new Date(task.next_show_at) @@ -559,12 +601,7 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { {projectNames.map(projectName => { const group = groupedTasks[projectName] const hasCompleted = group.completed.length > 0 - const hasInfinite = group.infinite.length > 0 const isCompletedExpanded = expandedCompleted[projectName] - // По умолчанию бесконечные раскрыты (true), если не сохранено иное - const isInfiniteExpanded = expandedInfinite[projectName] !== undefined - ? expandedInfinite[projectName] - : true return (
@@ -572,33 +609,13 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {

{projectName}

- {/* Обычные задачи */} + {/* Обычные задачи (включая бесконечные) */} {group.notCompleted.length > 0 && (
{group.notCompleted.map(renderTaskItem)}
)} - {/* Бесконечные задачи */} - {hasInfinite && ( -
- - {isInfiniteExpanded && ( -
- {group.infinite.map(renderTaskItem)} -
- )} -
- )} - {/* Выполненные задачи */} {hasCompleted && (
@@ -619,7 +636,7 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
)} - {group.notCompleted.length === 0 && !hasCompleted && !hasInfinite && ( + {group.notCompleted.length === 0 && !hasCompleted && (
Нет задач в этой группе
)}