Files
play-life/play-life-web/src/components/ShoppingList.jsx
poignatov 1876595005
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 1m14s
6.18.4: Фикс загрузки товаров при создании доски
2026-03-15 20:00:12 +03:00

864 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useRef, useMemo } from 'react'
import { createPortal } from 'react-dom'
import { useAuth } from './auth/AuthContext'
import BoardSelector from './BoardSelector'
import ShoppingItemDetail from './ShoppingItemDetail'
import LoadingError from './LoadingError'
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'
const BOARDS_CACHE_KEY = 'shopping_boards_cache'
const ITEMS_CACHE_KEY = 'shopping_items_cache'
const SELECTED_BOARD_KEY = 'shopping_selected_board_id'
// Форматирование даты в 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.floor((target - now) / (1000 * 60 * 60 * 24))
if (diffDays === 0) return 'Сегодня'
if (diffDays === 1) return 'Завтра'
if (diffDays === -1) return 'Вчера'
if (diffDays > 0 && diffDays <= 7) {
const dayNames = ['Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота']
return dayNames[target.getDay()]
}
const monthNames = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря']
if (target.getFullYear() === now.getFullYear()) {
return `${target.getDate()} ${monthNames[target.getMonth()]}`
}
return `${target.getDate()} ${monthNames[target.getMonth()]} ${target.getFullYear()}`
}
// Вычисление следующей даты по 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 'day': case 'days':
if (value % 7 === 0 && value >= 7) {
nextDate.setDate(nextDate.getDate() + value)
} else {
nextDate.setDate(nextDate.getDate() + value)
}
break
case 'week': case 'weeks': case 'wks': case 'wk':
nextDate.setDate(nextDate.getDate() + value * 7)
break
case 'month': case 'months': case 'mons': case 'mon':
nextDate.setMonth(nextDate.getMonth() + value)
break
case 'year': case 'years': case 'yrs': case 'yr':
nextDate.setFullYear(nextDate.getFullYear() + value)
break
case 'hour': case 'hours': case 'hrs': case 'hr':
nextDate.setHours(nextDate.getHours() + value)
break
case 'minute': case 'minutes': case 'mins': case 'min':
nextDate.setMinutes(nextDate.getMinutes() + value)
break
default:
return null
}
return nextDate
}
function ShoppingList({ onNavigate, refreshTrigger = 0, isActive = false, initialBoardId = null, boardDeleted = false }) {
const { authFetch } = useAuth()
const [boards, setBoards] = useState([])
const getInitialBoardId = () => {
if (initialBoardId) return initialBoardId
try {
const saved = localStorage.getItem(SELECTED_BOARD_KEY)
if (saved) {
const boardId = parseInt(saved, 10)
if (!isNaN(boardId)) return boardId
}
} catch (err) {}
return null
}
const hasBoardsCache = () => {
try {
const cached = localStorage.getItem(BOARDS_CACHE_KEY)
if (cached) {
const data = JSON.parse(cached)
return !!(data.boards && data.boards.length >= 0)
}
} catch (err) {}
return false
}
const [selectedBoardId, setSelectedBoardIdState] = useState(getInitialBoardId)
const [items, setItems] = useState([])
const [loading, setLoading] = useState(true)
const [boardsLoading, setBoardsLoading] = useState(!hasBoardsCache())
const [error, setError] = useState('')
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 fetchingRef = useRef(false)
const initialFetchDoneRef = useRef(false)
const prevIsActiveRef = useRef(isActive)
// Refs для закрытия диалогов кнопкой "Назад"
const historyPushedForDetailRef = useRef(false)
const historyPushedForPostponeRef = useRef(false)
const selectedItemForDetailRef = useRef(selectedItemForDetail)
const selectedItemForPostponeRef = useRef(selectedItemForPostpone)
const setSelectedBoardId = (boardId) => {
setSelectedBoardIdState(boardId)
try {
if (boardId) {
localStorage.setItem(SELECTED_BOARD_KEY, String(boardId))
} else {
localStorage.removeItem(SELECTED_BOARD_KEY)
}
} catch (err) {}
}
// Загрузка досок
const fetchBoards = async (showLoading = true, preferBoardId = null) => {
if (showLoading) setBoardsLoading(true)
try {
const res = await authFetch('/api/shopping/boards')
if (res.ok) {
const data = await res.json()
const boardsList = Array.isArray(data) ? data : []
setBoards(boardsList)
try {
localStorage.setItem(BOARDS_CACHE_KEY, JSON.stringify({ boards: boardsList }))
} catch (err) {}
const effectiveBoardId = preferBoardId || selectedBoardId
if (boardDeleted || !boardsList.some(b => b.id === effectiveBoardId)) {
if (boardsList.length > 0) {
setSelectedBoardId(boardsList[0].id)
} else {
setSelectedBoardId(null)
}
} else if (preferBoardId && preferBoardId !== selectedBoardId) {
setSelectedBoardId(preferBoardId)
}
}
} catch (err) {
setError('Ошибка загрузки досок')
} finally {
setBoardsLoading(false)
}
}
// Загрузка товаров
const fetchItems = async (boardId) => {
if (!boardId || fetchingRef.current) return
fetchingRef.current = true
setLoading(true)
setError('')
try {
const res = await authFetch(`/api/shopping/boards/${boardId}/items`)
if (res.ok) {
const data = await res.json()
setItems(Array.isArray(data) ? data : [])
try {
localStorage.setItem(`${ITEMS_CACHE_KEY}_${boardId}`, JSON.stringify(data))
} catch (err) {}
} else {
setError('Ошибка загрузки товаров')
}
} catch (err) {
setError('Ошибка загрузки товаров')
} finally {
setLoading(false)
fetchingRef.current = false
}
}
// Загрузка из кэша
useEffect(() => {
try {
const cached = localStorage.getItem(BOARDS_CACHE_KEY)
if (cached) {
const data = JSON.parse(cached)
if (data.boards) setBoards(data.boards)
}
} catch (err) {}
if (selectedBoardId) {
try {
const cached = localStorage.getItem(`${ITEMS_CACHE_KEY}_${selectedBoardId}`)
if (cached) {
setItems(JSON.parse(cached) || [])
}
} catch (err) {}
}
}, [])
// Начальная загрузка
useEffect(() => {
const hasCache = hasBoardsCache()
fetchBoards(!hasCache, initialBoardId)
initialFetchDoneRef.current = true
}, [])
// Загрузка при смене доски
useEffect(() => {
if (selectedBoardId) {
fetchItems(selectedBoardId)
} else {
setItems([])
setLoading(false)
}
}, [selectedBoardId])
// Рефреш при возврате на таб
useEffect(() => {
if (isActive && !prevIsActiveRef.current && initialFetchDoneRef.current) {
fetchBoards(false)
if (selectedBoardId) fetchItems(selectedBoardId)
}
prevIsActiveRef.current = isActive
}, [isActive])
// Рефреш по триггеру
useEffect(() => {
if (refreshTrigger > 0) {
fetchBoards(false)
if (selectedBoardId) fetchItems(selectedBoardId)
}
}, [refreshTrigger])
// Синхронизация refs для диалогов
useEffect(() => {
selectedItemForDetailRef.current = selectedItemForDetail
selectedItemForPostponeRef.current = selectedItemForPostpone
}, [selectedItemForDetail, selectedItemForPostpone])
// Закрытие диалогов кнопкой "Назад" (browser history API)
useEffect(() => {
if (selectedItemForPostpone && !historyPushedForPostponeRef.current) {
window.history.pushState({ modalOpen: true, type: 'shopping-postpone' }, '', window.location.href)
historyPushedForPostponeRef.current = true
} else if (!selectedItemForPostpone) {
historyPushedForPostponeRef.current = false
}
if (selectedItemForDetail && !historyPushedForDetailRef.current) {
window.history.pushState({ modalOpen: true, type: 'shopping-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
if (currentDetail) {
window.history.pushState({ modalOpen: true, type: 'shopping-detail' }, '', window.location.href)
}
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)
})
// Сортируем future по next_show_at ASC
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 [expandedFuture, setExpandedFuture] = useState({})
const handleBoardChange = (boardId) => {
setSelectedBoardId(boardId)
}
const handleBoardEdit = () => {
if (selectedBoardId) {
const board = boards.find(b => b.id === selectedBoardId)
if (board?.is_owner) {
onNavigate('shopping-board-form', { boardId: selectedBoardId })
} else {
if (window.confirm('Покинуть доску?')) {
authFetch(`/api/shopping/boards/${selectedBoardId}/leave`, { method: 'POST' })
.then(res => {
if (res.ok) {
fetchBoards()
}
})
}
}
}
}
const handleAddBoard = () => {
onNavigate('shopping-board-form')
}
const handleRefresh = () => {
if (selectedBoardId) fetchItems(selectedBoardId)
}
const handleCloseDetail = (skipHistoryBack = false) => {
if (!skipHistoryBack && historyPushedForDetailRef.current) {
window.history.back()
} else {
historyPushedForDetailRef.current = false
setSelectedItemForDetail(null)
}
}
const openPostpone = (item) => {
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('')
}
}
// Модалка переноса
const handlePostponeClose = () => {
if (historyPushedForPostponeRef.current) {
window.history.back()
} else {
historyPushedForPostponeRef.current = false
setSelectedItemForPostpone(null)
setPostponeDate('')
}
}
const handleDateSelect = (date) => {
if (date) {
setPostponeDate(formatDateToLocal(date))
}
}
const handleDayClick = (date) => {
if (date) {
const dateStr = formatDateToLocal(date)
handlePostponeSubmitWithDate(dateStr)
}
}
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 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 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]
}))
}
return (
<div className="shopping-list">
<div className="shopping-header">
<BoardSelector
boards={boards}
selectedBoardId={selectedBoardId}
onBoardChange={handleBoardChange}
onBoardEdit={handleBoardEdit}
onAddBoard={handleAddBoard}
loading={boardsLoading}
showBoardAction={false}
/>
<button className="shopping-close-btn" onClick={() => window.history.back()}></button>
</div>
{boards.length === 0 && !boardsLoading && (
<div className="shopping-empty">
<p>Нет досок</p>
<p className="shopping-empty-hint">Создайте доску, чтобы начать добавлять товары</p>
</div>
)}
{selectedBoardId && error && (
<LoadingError onRetry={handleRefresh} />
)}
{selectedBoardId && !error && (
<>
{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>
)}
{!loading && 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 => {
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()
openPostpone(item)
}}
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>
)
})}
</div>
)}
{hasFuture && isFutureExpanded && (
<div className="task-group completed-tasks">
{group.future.map(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)
}}
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>
)})}
</div>
)}
</div>
)
})}
</>
)}
{/* Модалка выполнения */}
{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
onClick={handleTodayClick}
className="task-postpone-quick-button"
disabled={isPostponing}
>
Сегодня
</button>
)}
{!isTomorrow && (
<button
onClick={handleTomorrowClick}
className="task-postpone-quick-button"
disabled={isPostponing}
>
Завтра
</button>
)}
{!isCurrentDatePlanned && (
<button
onClick={() => handlePostponeSubmitWithDate(plannedDateStr)}
className="task-postpone-quick-button"
disabled={isPostponing}
>
По плану
</button>
)}
{selectedItemForPostpone?.next_show_at && (
<button
onClick={handleWithoutDateClick}
className="task-postpone-quick-button"
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 ShoppingList