6.4.0: Экран товаров (Shopping List)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m22s

This commit is contained in:
poignatov
2026-03-08 16:11:08 +03:00
parent cd51b097c8
commit 60fca2d93c
14 changed files with 3324 additions and 23 deletions

View File

@@ -0,0 +1,202 @@
import React, { useState, useEffect, useCallback } from 'react'
import { createPortal } from 'react-dom'
import { useAuth } from './auth/AuthContext'
import LoadingError from './LoadingError'
import Toast from './Toast'
import './TaskDetail.css'
function ShoppingItemDetail({ itemId, onClose, onRefresh, onItemCompleted, onNavigate }) {
const { authFetch } = useAuth()
const [item, setItem] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [volumeValue, setVolumeValue] = useState('')
const [isCompleting, setIsCompleting] = useState(false)
const [toastMessage, setToastMessage] = useState(null)
const fetchItem = useCallback(async () => {
try {
setLoading(true)
setError(null)
const response = await authFetch(`/api/shopping/items/${itemId}`)
if (!response.ok) {
throw new Error('Ошибка загрузки товара')
}
const data = await response.json()
setItem(data)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}, [itemId, authFetch])
useEffect(() => {
if (itemId) {
fetchItem()
} else {
setItem(null)
setLoading(true)
setError(null)
setVolumeValue('')
}
}, [itemId, fetchItem])
const handleComplete = async () => {
if (!item) return
setIsCompleting(true)
try {
const payload = {}
if (volumeValue.trim()) {
payload.volume = parseFloat(volumeValue)
if (isNaN(payload.volume)) {
throw new Error('Неверное значение объёма')
}
} else {
payload.volume = item.volume_base
}
const response = await authFetch(`/api/shopping/items/${itemId}/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(errorData.error || 'Ошибка при выполнении')
}
onItemCompleted?.()
onRefresh?.()
onClose?.()
} catch (err) {
setToastMessage({ text: err.message || 'Ошибка', type: 'error' })
} finally {
setIsCompleting(false)
}
}
if (!itemId) return null
const modalContent = (
<div className="task-detail-modal-overlay" onClick={onClose}>
<div className="task-detail-modal" onClick={(e) => e.stopPropagation()}>
<div className="task-detail-modal-header">
<h2
className="task-detail-title"
onClick={item ? () => {
onClose?.(true)
onNavigate?.('shopping-item-form', { itemId: itemId, boardId: item.board_id })
} : undefined}
style={{ cursor: item ? 'pointer' : 'default' }}
>
{loading ? 'Загрузка...' : error ? 'Ошибка' : item ? (
<>
{item.name}
<svg
className="task-detail-edit-icon"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
</svg>
</>
) : 'Товар'}
</h2>
<button onClick={onClose} className="task-detail-close-button">
</button>
</div>
<div className="task-detail-modal-content">
{loading && (
<div className="loading">Загрузка...</div>
)}
{error && !loading && (
<LoadingError onRetry={fetchItem} />
)}
{!loading && !error && item && (
<>
<div className="progression-section">
<label className="progression-label">Объём</label>
<div className="progression-input-wrapper">
<input
type="number"
step="any"
value={volumeValue}
onChange={(e) => setVolumeValue(e.target.value)}
placeholder={item.volume_base?.toString() || '1'}
className="progression-input"
/>
<div className="progression-controls-capsule">
<button
type="button"
className="progression-control-btn progression-control-minus"
onClick={() => {
const current = parseFloat(volumeValue) || 0
const step = item.volume_base || 1
setVolumeValue((current - step).toString())
}}
>
</button>
<button
type="button"
className="progression-control-btn progression-control-plus"
onClick={() => {
const current = parseFloat(volumeValue) || 0
const step = item.volume_base || 1
setVolumeValue((current + step).toString())
}}
>
+
</button>
</div>
</div>
</div>
<div className="task-detail-divider"></div>
<div className="task-actions-section">
<div className="task-actions-buttons">
<div className="task-action-left">
<button
onClick={handleComplete}
disabled={isCompleting}
className="action-button action-button-check"
>
{isCompleting ? 'Выполнение...' : 'Выполнить'}
</button>
</div>
</div>
</div>
</>
)}
</div>
{toastMessage && (
<Toast
message={toastMessage.text}
type={toastMessage.type}
onClose={() => setToastMessage(null)}
/>
)}
</div>
</div>
)
return typeof document !== 'undefined'
? createPortal(modalContent, document.body)
: modalContent
}
export default ShoppingItemDetail