Обновление модального окна переноса задачи (v3.5.2)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 39s

This commit is contained in:
poignatov
2026-01-09 13:51:50 +03:00
parent b57b0bc901
commit 1097a84d06
4 changed files with 197 additions and 118 deletions

View File

@@ -1 +1 @@
3.5.1 3.5.2

View File

@@ -1,6 +1,6 @@
{ {
"name": "play-life-web", "name": "play-life-web",
"version": "3.5.0", "version": "3.5.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -220,23 +220,43 @@
padding: 1.5rem; padding: 1.5rem;
} }
.task-postpone-task-name { .task-postpone-quick-buttons {
margin: 0 0 1rem 0; display: flex;
font-size: 1rem; gap: 0.5rem;
color: #1f2937; margin-top: 0.5rem;
font-weight: 500;
} }
.task-postpone-label { .task-postpone-quick-button {
display: flex; padding: 0.5rem 1rem;
flex-direction: column; border: 1px solid #d1d5db;
gap: 0.5rem; border-radius: 0.375rem;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
color: #374151; color: #374151;
background: white;
cursor: pointer;
transition: all 0.2s;
}
.task-postpone-quick-button:hover:not(:disabled) {
background: #f3f4f6;
border-color: #6366f1;
color: #6366f1;
}
.task-postpone-quick-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.task-postpone-input-group {
display: flex;
gap: 0.5rem;
align-items: center;
} }
.task-postpone-input { .task-postpone-input {
flex: 1;
padding: 0.75rem; padding: 0.75rem;
border: 1px solid #d1d5db; border: 1px solid #d1d5db;
border-radius: 0.375rem; border-radius: 0.375rem;
@@ -250,43 +270,25 @@
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
} }
.task-postpone-modal-actions { .task-postpone-submit-checkmark {
padding: 1rem 1.5rem 1.5rem;
display: flex;
gap: 0.75rem;
justify-content: flex-end;
}
.task-postpone-cancel-button,
.task-postpone-submit-button {
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
background: linear-gradient(to right, #6366f1, #8b5cf6);
color: white;
border: none; border: none;
border-radius: 0.375rem; border-radius: 0.375rem;
font-size: 1rem; font-size: 1rem;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
min-width: 3rem;
} }
.task-postpone-cancel-button { .task-postpone-submit-checkmark:hover:not(:disabled) {
background: #f3f4f6; transform: translateY(-1px);
color: #374151; box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
} }
.task-postpone-cancel-button:hover:not(:disabled) { .task-postpone-submit-checkmark:disabled {
background: #e5e7eb;
}
.task-postpone-submit-button {
background: #6366f1;
color: white;
}
.task-postpone-submit-button:hover:not(:disabled) {
background: #4f46e5;
}
.task-postpone-submit-button:disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
} }

View File

@@ -163,6 +163,54 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
} }
} }
// Функция для вычисления следующей даты по 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':
nextDate.setMinutes(nextDate.getMinutes() + value)
break
case 'hour':
case 'hours':
nextDate.setHours(nextDate.getHours() + value)
break
case 'day':
case 'days':
nextDate.setDate(nextDate.getDate() + value)
break
case 'week':
case 'weeks':
nextDate.setDate(nextDate.getDate() + value * 7)
break
case 'month':
case 'months':
nextDate.setMonth(nextDate.getMonth() + value)
break
case 'year':
case 'years':
nextDate.setFullYear(nextDate.getFullYear() + value)
break
default:
return null
}
return nextDate
}
// Форматирование даты в YYYY-MM-DD (локальное время, без смещения в UTC) // Форматирование даты в YYYY-MM-DD (локальное время, без смещения в UTC)
const formatDateToLocal = (date) => { const formatDateToLocal = (date) => {
const year = date.getFullYear() const year = date.getFullYear()
@@ -177,17 +225,26 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
// Устанавливаем дату по умолчанию // Устанавливаем дату по умолчанию
let defaultDate let defaultDate
const now = new Date()
now.setHours(0, 0, 0, 0)
if (task.repetition_date) { if (task.repetition_date) {
// Для задач с repetition_date - вычисляем следующую подходящую дату // Для задач с repetition_date - вычисляем следующую подходящую дату
const nextDate = calculateNextDateFromRepetitionDate(task.repetition_date) const nextDate = calculateNextDateFromRepetitionDate(task.repetition_date)
if (nextDate) { if (nextDate) {
defaultDate = 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) { if (!defaultDate) {
// Без repetition_date или если не удалось вычислить - завтра // Без repetition_date/repetition_period или если не удалось вычислить - завтра
defaultDate = new Date() defaultDate = new Date(now)
defaultDate.setDate(defaultDate.getDate() + 1) defaultDate.setDate(defaultDate.getDate() + 1)
} }
@@ -197,11 +254,42 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
const handlePostponeSubmit = async () => { const handlePostponeSubmit = async () => {
if (!selectedTaskForPostpone || !postponeDate) return 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) setIsPostponing(true)
try { try {
// Преобразуем дату в ISO формат с временем // Преобразуем дату в ISO формат с временем
const dateObj = new Date(postponeDate) const dateObj = new Date(dateToUse)
dateObj.setHours(0, 0, 0, 0) dateObj.setHours(0, 0, 0, 0)
const isoDate = dateObj.toISOString() const isoDate = dateObj.toISOString()
@@ -234,45 +322,6 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
} }
} }
const handlePostponeReset = async () => {
if (!selectedTaskForPostpone) return
setIsPostponing(true)
try {
const response = await authFetch(`${API_URL}/${selectedTaskForPostpone.id}/postpone`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ next_show_at: null }),
})
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 resetting postpone:', err)
alert(err.message || 'Ошибка при сбросе переноса задачи')
} finally {
setIsPostponing(false)
}
}
const handlePostponeClose = () => {
setSelectedTaskForPostpone(null)
setPostponeDate('')
}
const toggleCompletedExpanded = (projectName) => { const toggleCompletedExpanded = (projectName) => {
setExpandedCompleted(prev => ({ setExpandedCompleted(prev => ({
...prev, ...prev,
@@ -588,19 +637,33 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
)} )}
{/* Модальное окно для переноса задачи */} {/* Модальное окно для переноса задачи */}
{selectedTaskForPostpone && ( {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-overlay" onClick={handlePostponeClose}>
<div className="task-postpone-modal" onClick={(e) => e.stopPropagation()}> <div className="task-postpone-modal" onClick={(e) => e.stopPropagation()}>
<div className="task-postpone-modal-header"> <div className="task-postpone-modal-header">
<h3>Перенести задачу</h3> <h3>{selectedTaskForPostpone.name}</h3>
<button onClick={handlePostponeClose} className="task-postpone-close-button"> <button onClick={handlePostponeClose} className="task-postpone-close-button">
</button> </button>
</div> </div>
<div className="task-postpone-modal-content"> <div className="task-postpone-modal-content">
<p className="task-postpone-task-name">{selectedTaskForPostpone.name}</p> <div className="task-postpone-input-group">
<label className="task-postpone-label">
Дата показа:
<input <input
type="date" type="date"
value={postponeDate} value={postponeDate}
@@ -608,28 +671,42 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
className="task-postpone-input" className="task-postpone-input"
min={new Date().toISOString().split('T')[0]} min={new Date().toISOString().split('T')[0]}
/> />
</label> {postponeDate && (
</div>
<div className="task-postpone-modal-actions">
<button
onClick={handlePostponeReset}
className="task-postpone-cancel-button"
disabled={isPostponing}
>
{isPostponing ? 'Сброс...' : 'Сбросить'}
</button>
<button <button
onClick={handlePostponeSubmit} onClick={handlePostponeSubmit}
className="task-postpone-submit-button"
disabled={isPostponing || !postponeDate} disabled={isPostponing || !postponeDate}
className="task-postpone-submit-checkmark"
> >
{isPostponing ? 'Перенос...' : 'Перенести'}
</button> </button>
</div>
</div>
</div>
)} )}
</div> </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>
) )
} }