Files
play-life/play-life-web/src/components/ShoppingItemDetail.jsx
poignatov ebd1398a81
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m5s
6.8.1: Фикс истории навигации при закрытии крестиком
2026-03-10 17:18:14 +03:00

241 lines
8.7 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, 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="shopping-item-description-card">
<div className="shopping-item-description">
{item.description ? (
item.description.split(/(https?:\/\/[^\s<>"'`,;!)\]]+)/gi).map((part, i) => {
if (/^https?:\/\//i.test(part)) {
let host
try {
host = new URL(part).host.replace(/^www\./, '')
} catch {
host = 'Открыть ссылку'
}
return (
<a key={i} href={part} target="_blank" rel="noopener noreferrer" className="shopping-item-description-link">
{host}
</a>
)
}
return <span key={i}>{part}</span>
})
) : (
<span style={{ color: '#9ca3af' }}>Описание отсутствует</span>
)}
</div>
<button
type="button"
className="shopping-item-history-button"
onClick={() => {
onClose?.(true)
onNavigate?.('shopping-item-history', { itemId: itemId })
}}
title="История"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
</button>
</div>
<div className="shopping-item-complete-row">
<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>
<button
onClick={handleComplete}
disabled={isCompleting}
className="shopping-item-complete-button"
>
{isCompleting ? (
<div className="shopping-item-complete-spinner"></div>
) : (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
)}
</button>
</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