6.22.0: Авторасчёт сроков товаров по истории
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m23s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m23s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import { DayPicker } from 'react-day-picker'
|
||||
import { ru } from 'react-day-picker/locale'
|
||||
import 'react-day-picker/style.css'
|
||||
import './TaskList.css'
|
||||
import './TaskDetail.css'
|
||||
import './ShoppingList.css'
|
||||
|
||||
const BOARDS_CACHE_KEY = 'shopping_boards_cache'
|
||||
@@ -129,6 +130,7 @@ function ShoppingList({ onNavigate, refreshTrigger = 0, isActive = false, initia
|
||||
const [selectedItemForDetail, setSelectedItemForDetail] = useState(null)
|
||||
const [selectedItemForPostpone, setSelectedItemForPostpone] = useState(null)
|
||||
const [postponeDate, setPostponeDate] = useState('')
|
||||
const [postponeRemaining, setPostponeRemaining] = useState('')
|
||||
const [isPostponing, setIsPostponing] = useState(false)
|
||||
const [toast, setToast] = useState(null)
|
||||
const initialFetchDoneRef = useRef(false)
|
||||
@@ -319,6 +321,7 @@ function ShoppingList({ onNavigate, refreshTrigger = 0, isActive = false, initia
|
||||
if (currentPostpone) {
|
||||
setSelectedItemForPostpone(null)
|
||||
setPostponeDate('')
|
||||
setPostponeRemaining('')
|
||||
historyPushedForPostponeRef.current = false
|
||||
if (currentDetail) {
|
||||
window.history.pushState({ modalOpen: true, type: 'shopping-detail' }, '', window.location.href)
|
||||
@@ -420,28 +423,9 @@ function ShoppingList({ onNavigate, refreshTrigger = 0, isActive = false, initia
|
||||
|
||||
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 remainingVal = item.estimated_remaining != null && item.estimated_remaining > 0 ? (Math.round(item.estimated_remaining * 10) / 10).toString() : ''
|
||||
setPostponeRemaining(remainingVal)
|
||||
// Дата рассчитывается в useEffect по остатку
|
||||
}
|
||||
|
||||
// Модалка переноса
|
||||
@@ -452,6 +436,7 @@ function ShoppingList({ onNavigate, refreshTrigger = 0, isActive = false, initia
|
||||
historyPushedForPostponeRef.current = false
|
||||
setSelectedItemForPostpone(null)
|
||||
setPostponeDate('')
|
||||
setPostponeRemaining('')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,10 +459,14 @@ function ShoppingList({ onNavigate, refreshTrigger = 0, isActive = false, initia
|
||||
setIsPostponing(true)
|
||||
try {
|
||||
const nextShowAt = new Date(dateStr + 'T00:00:00')
|
||||
const payload = { next_show_at: nextShowAt.toISOString() }
|
||||
if (postponeRemaining.trim()) {
|
||||
payload.volume_remaining = parseFloat(postponeRemaining)
|
||||
}
|
||||
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() })
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
if (res.ok) {
|
||||
setToast({ message: 'Дата перенесена', type: 'success' })
|
||||
@@ -507,10 +496,14 @@ function ShoppingList({ onNavigate, refreshTrigger = 0, isActive = false, initia
|
||||
if (!selectedItemForPostpone) return
|
||||
setIsPostponing(true)
|
||||
try {
|
||||
const payload = { next_show_at: null }
|
||||
if (postponeRemaining.trim()) {
|
||||
payload.volume_remaining = parseFloat(postponeRemaining)
|
||||
}
|
||||
const res = await authFetch(`/api/shopping/items/${selectedItemForPostpone.id}/postpone`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ next_show_at: null })
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
if (res.ok) {
|
||||
setToast({ message: 'Дата убрана', type: 'success' })
|
||||
@@ -525,6 +518,28 @@ function ShoppingList({ onNavigate, refreshTrigger = 0, isActive = false, initia
|
||||
}
|
||||
|
||||
|
||||
// Автообновление даты в календаре при вводе остатка
|
||||
useEffect(() => {
|
||||
if (!selectedItemForPostpone) return
|
||||
if (selectedItemForPostpone.daily_consumption > 0) {
|
||||
const remaining = postponeRemaining.trim() ? parseFloat(postponeRemaining) : (selectedItemForPostpone.estimated_remaining ?? 0)
|
||||
if (!isNaN(remaining) && remaining >= 0) {
|
||||
const daily = selectedItemForPostpone.daily_consumption
|
||||
const daysLeft = remaining / daily
|
||||
const target = new Date()
|
||||
target.setHours(0, 0, 0, 0)
|
||||
target.setDate(target.getDate() + Math.ceil(daysLeft))
|
||||
setPostponeDate(formatDateToLocal(target))
|
||||
return
|
||||
}
|
||||
}
|
||||
// Фолбэк: завтра
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setHours(0, 0, 0, 0)
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
setPostponeDate(formatDateToLocal(tomorrow))
|
||||
}, [postponeRemaining, selectedItemForPostpone])
|
||||
|
||||
const groupNames = useMemo(() => {
|
||||
const names = Object.keys(groupedItems)
|
||||
return names.sort((a, b) => {
|
||||
@@ -658,7 +673,14 @@ function ShoppingList({ onNavigate, refreshTrigger = 0, isActive = false, initia
|
||||
</div>
|
||||
<div className="task-name-container">
|
||||
<div className="task-name-wrapper">
|
||||
<div className="task-name">{item.name}</div>
|
||||
<div className="task-name">
|
||||
{item.name}
|
||||
{item.estimated_remaining > 0 && (
|
||||
<span style={{ color: '#9ca3af', fontSize: '0.8em', marginLeft: '6px' }}>
|
||||
~{Math.round(item.estimated_remaining * 10) / 10}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{dateDisplay && (
|
||||
<div className="task-next-show-date">{dateDisplay}</div>
|
||||
)}
|
||||
@@ -722,7 +744,14 @@ function ShoppingList({ onNavigate, refreshTrigger = 0, isActive = false, initia
|
||||
</div>
|
||||
<div className="task-name-container">
|
||||
<div className="task-name-wrapper">
|
||||
<div className="task-name">{item.name}</div>
|
||||
<div className="task-name">
|
||||
{item.name}
|
||||
{item.estimated_remaining > 0 && (
|
||||
<span style={{ color: '#9ca3af', fontSize: '0.8em', marginLeft: '6px' }}>
|
||||
~{Math.round(item.estimated_remaining * 10) / 10}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{dateDisplay && (
|
||||
<div className="task-next-show-date">{dateDisplay}</div>
|
||||
)}
|
||||
@@ -811,6 +840,41 @@ function ShoppingList({ onNavigate, refreshTrigger = 0, isActive = false, initia
|
||||
</button>
|
||||
</div>
|
||||
<div className="task-postpone-modal-content">
|
||||
<div style={{ marginBottom: '0.5rem' }}>
|
||||
<label className="progression-label" style={{ marginBottom: '0.25rem' }}>Остаток</label>
|
||||
<div className="progression-input-wrapper">
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
value={postponeRemaining}
|
||||
onChange={(e) => setPostponeRemaining(e.target.value)}
|
||||
placeholder={selectedItemForPostpone.estimated_remaining != null ? (Math.round(selectedItemForPostpone.estimated_remaining * 10) / 10).toString() : '0'}
|
||||
className="progression-input"
|
||||
style={{ paddingRight: postponeRemaining ? '2rem' : '0.75rem' }}
|
||||
/>
|
||||
{postponeRemaining && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPostponeRemaining('')}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '8px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#9ca3af',
|
||||
cursor: 'pointer',
|
||||
fontSize: '1.1rem',
|
||||
padding: '4px',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="task-postpone-calendar">
|
||||
<DayPicker
|
||||
mode="single"
|
||||
@@ -844,13 +908,21 @@ function ShoppingList({ onNavigate, refreshTrigger = 0, isActive = false, initia
|
||||
Завтра
|
||||
</button>
|
||||
)}
|
||||
{!isCurrentDatePlanned && (
|
||||
{selectedItemForPostpone.daily_consumption > 0 && (
|
||||
<button
|
||||
onClick={() => handlePostponeSubmitWithDate(plannedDateStr)}
|
||||
onClick={() => {
|
||||
const remaining = postponeRemaining.trim() ? parseFloat(postponeRemaining) : (selectedItemForPostpone.estimated_remaining ?? 0)
|
||||
const daily = selectedItemForPostpone.daily_consumption
|
||||
const daysLeft = remaining / daily
|
||||
const target = new Date()
|
||||
target.setHours(0, 0, 0, 0)
|
||||
target.setDate(target.getDate() + Math.ceil(daysLeft))
|
||||
handlePostponeSubmitWithDate(formatDateToLocal(target))
|
||||
}}
|
||||
className="task-postpone-quick-button"
|
||||
disabled={isPostponing}
|
||||
>
|
||||
По плану
|
||||
По остатку
|
||||
</button>
|
||||
)}
|
||||
{selectedItemForPostpone?.next_show_at && (
|
||||
|
||||
Reference in New Issue
Block a user