6.9.0: Задачи-закупки
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m20s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m20s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
640
play-life-web/src/components/PurchaseScreen.jsx
Normal file
640
play-life-web/src/components/PurchaseScreen.jsx
Normal file
@@ -0,0 +1,640 @@
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useAuth } from './auth/AuthContext'
|
||||
import ShoppingItemDetail from './ShoppingItemDetail'
|
||||
import Toast from './Toast'
|
||||
import { DayPicker } from 'react-day-picker'
|
||||
import { ru } from 'react-day-picker/locale'
|
||||
import 'react-day-picker/style.css'
|
||||
import './TaskList.css'
|
||||
import './ShoppingList.css'
|
||||
|
||||
// Форматирование даты в YYYY-MM-DD (локальное время)
|
||||
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 formatDateForDisplay = (dateStr) => {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
if (isNaN(date.getTime())) return ''
|
||||
|
||||
const now = new Date()
|
||||
now.setHours(0, 0, 0, 0)
|
||||
const target = new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
||||
const diffDays = Math.round((target - now) / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (diffDays === 0) return 'Сегодня'
|
||||
if (diffDays === 1) return 'Завтра'
|
||||
|
||||
const months = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
|
||||
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря']
|
||||
return `${date.getDate()} ${months[date.getMonth()]}`
|
||||
}
|
||||
|
||||
const calculateNextDateFromRepetitionPeriod = (periodStr) => {
|
||||
if (!periodStr) return null
|
||||
const match = periodStr.match(/(\d+)\s*(day|week|mon|year)/i)
|
||||
if (!match) return null
|
||||
const value = parseInt(match[1], 10)
|
||||
const unit = match[2].toLowerCase()
|
||||
const next = new Date()
|
||||
next.setHours(0, 0, 0, 0)
|
||||
if (unit.startsWith('day')) next.setDate(next.getDate() + value)
|
||||
else if (unit.startsWith('week')) next.setDate(next.getDate() + value * 7)
|
||||
else if (unit.startsWith('mon')) next.setMonth(next.getMonth() + value)
|
||||
else if (unit.startsWith('year')) next.setFullYear(next.getFullYear() + value)
|
||||
return next
|
||||
}
|
||||
|
||||
function PurchaseScreen({ onNavigate, purchaseConfigId, taskId, taskName }) {
|
||||
const { authFetch } = useAuth()
|
||||
const [items, setItems] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(false)
|
||||
const [selectedItemForDetail, setSelectedItemForDetail] = useState(null)
|
||||
const [selectedItemForPostpone, setSelectedItemForPostpone] = useState(null)
|
||||
const [postponeDate, setPostponeDate] = useState('')
|
||||
const [isPostponing, setIsPostponing] = useState(false)
|
||||
const [toast, setToast] = useState(null)
|
||||
const [expandedFuture, setExpandedFuture] = useState({})
|
||||
const [isCompleting, setIsCompleting] = useState(false)
|
||||
const historyPushedForDetailRef = useRef(false)
|
||||
const historyPushedForPostponeRef = useRef(false)
|
||||
const selectedItemForDetailRef = useRef(null)
|
||||
const selectedItemForPostponeRef = useRef(null)
|
||||
|
||||
const fetchItems = async () => {
|
||||
if (!purchaseConfigId) return
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(false)
|
||||
const response = await authFetch(`/api/purchase/items/${purchaseConfigId}`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setItems(Array.isArray(data) ? data : [])
|
||||
} else {
|
||||
setError(true)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading purchase items:', err)
|
||||
setError(true)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchItems()
|
||||
}, [purchaseConfigId])
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchItems()
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
onNavigate?.('tasks')
|
||||
}
|
||||
|
||||
const handleCompleteTask = async () => {
|
||||
if (!taskId || isCompleting) return
|
||||
setIsCompleting(true)
|
||||
try {
|
||||
const response = await authFetch(`/api/tasks/${taskId}/complete`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
if (response.ok) {
|
||||
setToast({ message: 'Задача выполнена', type: 'success' })
|
||||
setTimeout(() => onNavigate?.('tasks'), 500)
|
||||
} else {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
setToast({ message: errorData.error || 'Ошибка выполнения', type: 'error' })
|
||||
}
|
||||
} catch (err) {
|
||||
setToast({ message: 'Ошибка выполнения', type: 'error' })
|
||||
} finally {
|
||||
setIsCompleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Синхронизация refs для диалогов
|
||||
useEffect(() => {
|
||||
selectedItemForDetailRef.current = selectedItemForDetail
|
||||
selectedItemForPostponeRef.current = selectedItemForPostpone
|
||||
}, [selectedItemForDetail, selectedItemForPostpone])
|
||||
|
||||
// Пуш в историю при открытии модалок и обработка popstate
|
||||
useEffect(() => {
|
||||
if (selectedItemForPostpone && !historyPushedForPostponeRef.current) {
|
||||
window.history.pushState({ modalOpen: true, type: 'purchase-postpone' }, '', window.location.href)
|
||||
historyPushedForPostponeRef.current = true
|
||||
} else if (!selectedItemForPostpone) {
|
||||
historyPushedForPostponeRef.current = false
|
||||
}
|
||||
|
||||
if (selectedItemForDetail && !historyPushedForDetailRef.current) {
|
||||
window.history.pushState({ modalOpen: true, type: 'purchase-detail' }, '', window.location.href)
|
||||
historyPushedForDetailRef.current = true
|
||||
} else if (!selectedItemForDetail) {
|
||||
historyPushedForDetailRef.current = false
|
||||
}
|
||||
|
||||
if (!selectedItemForDetail && !selectedItemForPostpone) return
|
||||
|
||||
const handlePopState = () => {
|
||||
const currentDetail = selectedItemForDetailRef.current
|
||||
const currentPostpone = selectedItemForPostponeRef.current
|
||||
|
||||
if (currentPostpone) {
|
||||
setSelectedItemForPostpone(null)
|
||||
setPostponeDate('')
|
||||
historyPushedForPostponeRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
if (currentDetail) {
|
||||
setSelectedItemForDetail(null)
|
||||
historyPushedForDetailRef.current = false
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', handlePopState)
|
||||
return () => {
|
||||
window.removeEventListener('popstate', handlePopState)
|
||||
}
|
||||
}, [selectedItemForDetail, selectedItemForPostpone])
|
||||
|
||||
// Фильтрация и группировка
|
||||
const groupedItems = useMemo(() => {
|
||||
const now = new Date()
|
||||
now.setHours(0, 0, 0, 0)
|
||||
const todayEnd = new Date(now)
|
||||
todayEnd.setHours(23, 59, 59, 999)
|
||||
|
||||
const groups = {}
|
||||
|
||||
items.forEach(item => {
|
||||
const groupKey = item.group_name || 'Остальные'
|
||||
if (!groups[groupKey]) {
|
||||
groups[groupKey] = { active: [], future: [] }
|
||||
}
|
||||
|
||||
if (!item.next_show_at) {
|
||||
groups[groupKey].future.push(item)
|
||||
return
|
||||
}
|
||||
const showAt = new Date(item.next_show_at)
|
||||
if (showAt > todayEnd) {
|
||||
groups[groupKey].future.push(item)
|
||||
return
|
||||
}
|
||||
groups[groupKey].active.push(item)
|
||||
})
|
||||
|
||||
Object.values(groups).forEach(group => {
|
||||
group.future.sort((a, b) => {
|
||||
if (!a.next_show_at) return 1
|
||||
if (!b.next_show_at) return -1
|
||||
return new Date(a.next_show_at) - new Date(b.next_show_at)
|
||||
})
|
||||
})
|
||||
|
||||
return groups
|
||||
}, [items])
|
||||
|
||||
const groupNames = useMemo(() => {
|
||||
const names = Object.keys(groupedItems)
|
||||
return names.sort((a, b) => {
|
||||
const groupA = groupedItems[a]
|
||||
const groupB = groupedItems[b]
|
||||
const hasActiveA = groupA.active.length > 0
|
||||
const hasActiveB = groupB.active.length > 0
|
||||
|
||||
if (hasActiveA && !hasActiveB) return -1
|
||||
if (!hasActiveA && hasActiveB) return 1
|
||||
|
||||
if (a === 'Остальные') return 1
|
||||
if (b === 'Остальные') return -1
|
||||
return a.localeCompare(b, 'ru')
|
||||
})
|
||||
}, [groupedItems])
|
||||
|
||||
const toggleFuture = (groupName) => {
|
||||
setExpandedFuture(prev => ({
|
||||
...prev,
|
||||
[groupName]: !prev[groupName]
|
||||
}))
|
||||
}
|
||||
|
||||
const handleCloseDetail = () => {
|
||||
if (historyPushedForDetailRef.current) {
|
||||
window.history.back()
|
||||
} else {
|
||||
setSelectedItemForDetail(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePostponeClose = () => {
|
||||
if (historyPushedForPostponeRef.current) {
|
||||
window.history.back()
|
||||
} else {
|
||||
setSelectedItemForPostpone(null)
|
||||
setPostponeDate('')
|
||||
}
|
||||
}
|
||||
|
||||
const handlePostponeSubmitWithDate = async (dateStr) => {
|
||||
if (!selectedItemForPostpone || !dateStr) return
|
||||
setIsPostponing(true)
|
||||
try {
|
||||
const nextShowAt = new Date(dateStr + 'T00:00:00')
|
||||
const res = await authFetch(`/api/shopping/items/${selectedItemForPostpone.id}/postpone`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ next_show_at: nextShowAt.toISOString() })
|
||||
})
|
||||
if (res.ok) {
|
||||
setToast({ message: 'Дата обновлена', type: 'success' })
|
||||
handleRefresh()
|
||||
handlePostponeClose()
|
||||
} else {
|
||||
setToast({ message: 'Ошибка переноса', type: 'error' })
|
||||
}
|
||||
} catch (err) {
|
||||
setToast({ message: 'Ошибка переноса', type: 'error' })
|
||||
} finally {
|
||||
setIsPostponing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDateSelect = (date) => {
|
||||
if (date) {
|
||||
setPostponeDate(formatDateToLocal(date))
|
||||
}
|
||||
}
|
||||
|
||||
const handleDayClick = (date) => {
|
||||
if (date) {
|
||||
handlePostponeSubmitWithDate(formatDateToLocal(date))
|
||||
}
|
||||
}
|
||||
|
||||
const handleTodayClick = () => {
|
||||
handlePostponeSubmitWithDate(formatDateToLocal(new Date()))
|
||||
}
|
||||
|
||||
const handleTomorrowClick = () => {
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
handlePostponeSubmitWithDate(formatDateToLocal(tomorrow))
|
||||
}
|
||||
|
||||
const handleWithoutDateClick = async () => {
|
||||
if (!selectedItemForPostpone) return
|
||||
setIsPostponing(true)
|
||||
try {
|
||||
const res = await authFetch(`/api/shopping/items/${selectedItemForPostpone.id}/postpone`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ next_show_at: null })
|
||||
})
|
||||
if (res.ok) {
|
||||
setToast({ message: 'Дата убрана', type: 'success' })
|
||||
handleRefresh()
|
||||
handlePostponeClose()
|
||||
}
|
||||
} catch (err) {
|
||||
setToast({ message: 'Ошибка', type: 'error' })
|
||||
} finally {
|
||||
setIsPostponing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const renderItem = (item) => {
|
||||
let dateDisplay = null
|
||||
if (item.next_show_at) {
|
||||
const itemDate = new Date(item.next_show_at)
|
||||
const now = new Date()
|
||||
now.setHours(0, 0, 0, 0)
|
||||
const target = new Date(itemDate.getFullYear(), itemDate.getMonth(), itemDate.getDate())
|
||||
if (target > now) {
|
||||
dateDisplay = formatDateForDisplay(item.next_show_at)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="task-item"
|
||||
onClick={() => setSelectedItemForDetail(item.id)}
|
||||
>
|
||||
<div className="task-item-content">
|
||||
<div
|
||||
className="task-checkmark"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setSelectedItemForDetail(item.id)
|
||||
}}
|
||||
title="Выполнить"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="10" cy="10" r="9" stroke="currentColor" strokeWidth="2" fill="none" className="checkmark-circle" />
|
||||
<path d="M6 10 L9 13 L14 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="checkmark-check" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="task-name-container">
|
||||
<div className="task-name-wrapper">
|
||||
<div className="task-name">{item.name}</div>
|
||||
{dateDisplay && (
|
||||
<div className="task-next-show-date">{dateDisplay}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="task-actions">
|
||||
<button
|
||||
className="task-postpone-button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setSelectedItemForPostpone(item)
|
||||
// Предвыбираем дату "по плану" если она не совпадает с текущей next_show_at
|
||||
const now2 = new Date()
|
||||
now2.setHours(0, 0, 0, 0)
|
||||
let planned
|
||||
if (item.repetition_period) {
|
||||
planned = calculateNextDateFromRepetitionPeriod(item.repetition_period)
|
||||
}
|
||||
if (!planned) {
|
||||
planned = new Date(now2)
|
||||
planned.setDate(planned.getDate() + 1)
|
||||
}
|
||||
planned.setHours(0, 0, 0, 0)
|
||||
const plannedStr = formatDateToLocal(planned)
|
||||
let nextShowStr = null
|
||||
if (item.next_show_at) {
|
||||
nextShowStr = formatDateToLocal(new Date(item.next_show_at))
|
||||
}
|
||||
if (plannedStr !== nextShowStr) {
|
||||
setPostponeDate(plannedStr)
|
||||
} else {
|
||||
setPostponeDate('')
|
||||
}
|
||||
}}
|
||||
title="Перенести"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="10" cy="10" r="8" stroke="currentColor" strokeWidth="1.5" fill="none"/>
|
||||
<path d="M10 5V10L13 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" fill="none"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto" style={{ paddingBottom: taskId ? '5rem' : '2.5rem' }}>
|
||||
<button className="close-x-button" onClick={handleClose}>✕</button>
|
||||
<h2 className="text-2xl font-semibold text-gray-800 mb-6" style={{ marginTop: '1.25rem' }}>{taskName || 'Закупка'}</h2>
|
||||
|
||||
{loading && items.length === 0 && (
|
||||
<div className="shopping-loading">
|
||||
<div className="w-8 h-8 border-4 border-indigo-200 border-t-indigo-600 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="shopping-empty">
|
||||
<p>Ошибка загрузки</p>
|
||||
<button onClick={handleRefresh} style={{ marginTop: '8px', color: 'var(--accent-color)' }}>
|
||||
Повторить
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && items.length === 0 && (
|
||||
<div className="shopping-empty">
|
||||
<p>Нет товаров</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{groupNames.map(groupName => {
|
||||
const group = groupedItems[groupName]
|
||||
const hasActive = group.active.length > 0
|
||||
const hasFuture = group.future.length > 0
|
||||
const isFutureExpanded = expandedFuture[groupName]
|
||||
|
||||
return (
|
||||
<div key={groupName} className={`project-group ${!hasActive ? 'project-group-no-tasks' : ''}`}>
|
||||
<div
|
||||
className={`project-group-header ${hasFuture ? 'project-group-header-clickable' : ''}`}
|
||||
onClick={hasFuture ? () => toggleFuture(groupName) : undefined}
|
||||
>
|
||||
<h3 className={`project-group-title ${!hasActive ? 'project-group-title-empty' : ''}`}>{groupName}</h3>
|
||||
{hasFuture ? (
|
||||
<button
|
||||
className="completed-toggle-header"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggleFuture(groupName)
|
||||
}}
|
||||
title={isFutureExpanded ? 'Скрыть ожидающие' : 'Показать ожидающие'}
|
||||
>
|
||||
<span className="completed-toggle-icon">
|
||||
{isFutureExpanded ? '▼' : '▶'}
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<div className="completed-toggle-header" style={{ visibility: 'hidden', pointerEvents: 'none' }}>
|
||||
<span className="completed-toggle-icon">▶</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasActive && (
|
||||
<div className="task-group">
|
||||
{group.active.map(item => renderItem(item))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasFuture && isFutureExpanded && (
|
||||
<div className="task-group completed-tasks">
|
||||
{group.future.map(item => renderItem(item))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Кнопка завершения задачи — фиксированная внизу */}
|
||||
{!loading && !error && taskId && createPortal(
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
padding: '0.75rem 1rem',
|
||||
paddingBottom: 'max(0.75rem, env(safe-area-inset-bottom))',
|
||||
background: 'linear-gradient(to top, white 60%, rgba(255,255,255,0))',
|
||||
zIndex: 1500,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<button
|
||||
onClick={handleCompleteTask}
|
||||
disabled={isCompleting}
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: '42rem',
|
||||
padding: '0.875rem',
|
||||
background: 'linear-gradient(to right, #10b981, #059669)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '0.5rem',
|
||||
fontSize: '1rem',
|
||||
fontWeight: 600,
|
||||
cursor: isCompleting ? 'not-allowed' : 'pointer',
|
||||
opacity: isCompleting ? 0.6 : 1,
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
>
|
||||
{isCompleting ? 'Выполняется...' : 'Завершить'}
|
||||
</button>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
{/* Модалка выполнения */}
|
||||
{selectedItemForDetail && (
|
||||
<ShoppingItemDetail
|
||||
itemId={selectedItemForDetail}
|
||||
onClose={handleCloseDetail}
|
||||
onRefresh={handleRefresh}
|
||||
onItemCompleted={() => setToast({ message: 'Товар выполнен', type: 'success' })}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Модалка переноса */}
|
||||
{selectedItemForPostpone && (() => {
|
||||
const todayStr = formatDateToLocal(new Date())
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
const tomorrowStr = formatDateToLocal(tomorrow)
|
||||
|
||||
let nextShowAtStr = null
|
||||
if (selectedItemForPostpone.next_show_at) {
|
||||
const nextShowAtDate = new Date(selectedItemForPostpone.next_show_at)
|
||||
nextShowAtStr = formatDateToLocal(nextShowAtDate)
|
||||
}
|
||||
|
||||
const isTomorrow = nextShowAtStr === tomorrowStr
|
||||
const showTodayChip = !nextShowAtStr || nextShowAtStr > todayStr
|
||||
|
||||
const item = selectedItemForPostpone
|
||||
let plannedDate
|
||||
const now = new Date()
|
||||
now.setHours(0, 0, 0, 0)
|
||||
if (item.repetition_period) {
|
||||
const nextDate = calculateNextDateFromRepetitionPeriod(item.repetition_period)
|
||||
if (nextDate) plannedDate = nextDate
|
||||
}
|
||||
if (!plannedDate) {
|
||||
plannedDate = new Date(now)
|
||||
plannedDate.setDate(plannedDate.getDate() + 1)
|
||||
}
|
||||
plannedDate.setHours(0, 0, 0, 0)
|
||||
const plannedDateStr = formatDateToLocal(plannedDate)
|
||||
const plannedNorm = plannedDateStr.slice(0, 10)
|
||||
const nextShowNorm = nextShowAtStr ? String(nextShowAtStr).slice(0, 10) : ''
|
||||
const isCurrentDatePlanned = plannedNorm && nextShowNorm && plannedNorm === nextShowNorm
|
||||
|
||||
const modalContent = (
|
||||
<div className="task-postpone-modal-overlay" onClick={handlePostponeClose}>
|
||||
<div className="task-postpone-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="task-postpone-modal-header">
|
||||
<h3>{selectedItemForPostpone.name}</h3>
|
||||
<button onClick={handlePostponeClose} className="task-postpone-close-button">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="task-postpone-modal-content">
|
||||
<div className="task-postpone-calendar">
|
||||
<DayPicker
|
||||
mode="single"
|
||||
selected={postponeDate ? new Date(postponeDate + 'T00:00:00') : undefined}
|
||||
onSelect={handleDateSelect}
|
||||
onDayClick={handleDayClick}
|
||||
disabled={{ before: (() => {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
return today
|
||||
})() }}
|
||||
locale={ru}
|
||||
/>
|
||||
</div>
|
||||
<div className="task-postpone-quick-buttons">
|
||||
{showTodayChip && (
|
||||
<button
|
||||
className="task-postpone-quick-button"
|
||||
onClick={handleTodayClick}
|
||||
disabled={isPostponing}
|
||||
>
|
||||
Сегодня
|
||||
</button>
|
||||
)}
|
||||
{!isTomorrow && (
|
||||
<button
|
||||
className="task-postpone-quick-button"
|
||||
onClick={handleTomorrowClick}
|
||||
disabled={isPostponing}
|
||||
>
|
||||
Завтра
|
||||
</button>
|
||||
)}
|
||||
{!isCurrentDatePlanned && (
|
||||
<button
|
||||
className="task-postpone-quick-button"
|
||||
onClick={() => handlePostponeSubmitWithDate(plannedDateStr)}
|
||||
disabled={isPostponing}
|
||||
>
|
||||
По плану
|
||||
</button>
|
||||
)}
|
||||
{nextShowAtStr && (
|
||||
<button
|
||||
className="task-postpone-quick-button task-postpone-quick-button-danger"
|
||||
onClick={handleWithoutDateClick}
|
||||
disabled={isPostponing}
|
||||
>
|
||||
Без даты
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return typeof document !== 'undefined'
|
||||
? createPortal(modalContent, document.body)
|
||||
: modalContent
|
||||
})()}
|
||||
|
||||
{toast && (
|
||||
<Toast
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={() => setToast(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PurchaseScreen
|
||||
Reference in New Issue
Block a user