4.6.0: Расчет срока разблокировки желаний
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m39s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m39s
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "play-life-web",
|
||||
"version": "4.5.0",
|
||||
"version": "4.6.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.condition-icon {
|
||||
@@ -124,7 +124,7 @@
|
||||
}
|
||||
|
||||
.condition-progress {
|
||||
margin-top: 0.25rem;
|
||||
margin-top: 0.125rem;
|
||||
margin-left: calc(16px + 0.5rem);
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
background-color: #e5e7eb;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.25rem;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
|
||||
@@ -286,7 +286,10 @@ function WishlistDetail({ wishlistId, onNavigate, onRefresh, boardId }) {
|
||||
<div className="progress-text">
|
||||
<span>{Math.round(progress.current)} / {Math.round(progress.required)}</span>
|
||||
{progress.remaining > 0 && (
|
||||
<span className="progress-remaining">Осталось: {Math.round(progress.remaining)}</span>
|
||||
<span className="progress-remaining">
|
||||
Осталось: {Math.round(progress.remaining)}
|
||||
{condition.weeks_text && ` (${condition.weeks_text})`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -277,20 +277,58 @@
|
||||
.condition-form {
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.condition-form-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.condition-form h3 {
|
||||
margin: 0 0 1.5rem 0;
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.condition-form-close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.375rem;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.condition-form-close-button:hover {
|
||||
background: #f3f4f6;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.condition-form form {
|
||||
padding: 1.5rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
@@ -310,6 +348,11 @@
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.condition-form-submit-button {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.submit-button:hover:not(:disabled) {
|
||||
background: #2980b9;
|
||||
transform: translateY(-1px);
|
||||
|
||||
@@ -98,6 +98,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
||||
start_date: cond.start_date || null,
|
||||
display_order: idx,
|
||||
user_id: cond.user_id || null,
|
||||
weeks_text: cond.weeks_text || null,
|
||||
})))
|
||||
} else {
|
||||
setUnlockConditions([])
|
||||
@@ -253,6 +254,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
||||
start_date: cond.start_date || null,
|
||||
display_order: idx,
|
||||
user_id: cond.user_id || null,
|
||||
weeks_text: cond.weeks_text || null,
|
||||
})))
|
||||
} else {
|
||||
setUnlockConditions([])
|
||||
@@ -798,16 +800,24 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
||||
const isOwnCondition = !cond.user_id || cond.user_id === user?.id
|
||||
return (
|
||||
<div key={idx} className="condition-item">
|
||||
<span
|
||||
className={`condition-item-text ${!isOwnCondition ? 'condition-item-other-user' : ''}`}
|
||||
onClick={() => isOwnCondition && handleEditCondition(idx)}
|
||||
style={{ cursor: isOwnCondition ? 'pointer' : 'default' }}
|
||||
title={!isOwnCondition ? 'Чужая цель - нельзя редактировать' : ''}
|
||||
>
|
||||
{cond.type === 'task_completion'
|
||||
? `Задача: ${tasks.find(t => t.id === cond.task_id)?.name || 'Не выбрана'}`
|
||||
: `Баллы: ${cond.required_points} в ${projects.find(p => p.project_id === cond.project_id)?.project_name || cond.project_name || 'Не выбран'}${cond.start_date ? ` с ${new Date(cond.start_date + 'T00:00:00').toLocaleDateString('ru-RU')}` : ' за всё время'}`}
|
||||
</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<span
|
||||
className={`condition-item-text ${!isOwnCondition ? 'condition-item-other-user' : ''}`}
|
||||
onClick={() => isOwnCondition && handleEditCondition(idx)}
|
||||
style={{ cursor: isOwnCondition ? 'pointer' : 'default', paddingBottom: '0.125rem' }}
|
||||
title={!isOwnCondition ? 'Чужая цель - нельзя редактировать' : ''}
|
||||
>
|
||||
{cond.type === 'task_completion'
|
||||
? tasks.find(t => t.id === cond.task_id)?.name || 'Не выбрана'
|
||||
: `${cond.required_points} в ${projects.find(p => p.project_id === cond.project_id)?.project_name || cond.project_name || 'Не выбран'}${cond.start_date ? ` с ${new Date(cond.start_date + 'T00:00:00').toLocaleDateString('ru-RU')}` : ' за всё время'}`}
|
||||
</span>
|
||||
{cond.type === 'project_points' && cond.weeks_text && (
|
||||
<div style={{ color: '#666', fontSize: '0.85em' }}>
|
||||
<span>Срок: </span>
|
||||
<span style={{ fontWeight: '600' }}>{cond.weeks_text}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isOwnCondition && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -849,6 +859,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
||||
editingCondition={editingConditionIndex !== null ? unlockConditions[editingConditionIndex] : null}
|
||||
onCreateTask={handleCreateTaskFromCondition}
|
||||
preselectedTaskId={newTaskId}
|
||||
authFetch={authFetch}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1125,12 +1136,13 @@ function TaskAutocomplete({ tasks, value, onChange, onCreateTask, preselectedTas
|
||||
}
|
||||
|
||||
// Компонент формы цели
|
||||
function ConditionForm({ tasks, projects, onSubmit, onCancel, editingCondition, onCreateTask, preselectedTaskId }) {
|
||||
function ConditionForm({ tasks, projects, onSubmit, onCancel, editingCondition, onCreateTask, preselectedTaskId, authFetch }) {
|
||||
const [type, setType] = useState(editingCondition?.type || 'project_points')
|
||||
const [taskId, setTaskId] = useState(editingCondition?.task_id || null)
|
||||
const [projectId, setProjectId] = useState(editingCondition?.project_id?.toString() || '')
|
||||
const [requiredPoints, setRequiredPoints] = useState(editingCondition?.required_points?.toString() || '')
|
||||
const [startDate, setStartDate] = useState(editingCondition?.start_date || '')
|
||||
const [calculatedWeeksText, setCalculatedWeeksText] = useState(null)
|
||||
|
||||
const isEditing = editingCondition !== null
|
||||
|
||||
@@ -1142,6 +1154,40 @@ function ConditionForm({ tasks, projects, onSubmit, onCancel, editingCondition,
|
||||
}
|
||||
}, [preselectedTaskId, editingCondition])
|
||||
|
||||
// Расчет недель при изменении проекта, баллов или даты
|
||||
useEffect(() => {
|
||||
const calculateWeeks = async () => {
|
||||
if (type === 'project_points' && projectId && requiredPoints && authFetch) {
|
||||
try {
|
||||
const response = await authFetch('/api/wishlist/calculate-weeks', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
project_id: parseInt(projectId),
|
||||
required_points: parseFloat(requiredPoints),
|
||||
start_date: startDate || '',
|
||||
condition_user_id: editingCondition?.user_id || null,
|
||||
}),
|
||||
})
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setCalculatedWeeksText(data.weeks_text || null)
|
||||
} else {
|
||||
setCalculatedWeeksText(null)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error calculating weeks:', err)
|
||||
setCalculatedWeeksText(null)
|
||||
}
|
||||
} else {
|
||||
setCalculatedWeeksText(null)
|
||||
}
|
||||
}
|
||||
calculateWeeks()
|
||||
}, [type, projectId, requiredPoints, startDate, editingCondition?.user_id, authFetch])
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation() // Предотвращаем всплытие события
|
||||
@@ -1173,7 +1219,12 @@ function ConditionForm({ tasks, projects, onSubmit, onCancel, editingCondition,
|
||||
return (
|
||||
<div className="condition-form-overlay" onClick={onCancel}>
|
||||
<div className="condition-form" onClick={(e) => e.stopPropagation()}>
|
||||
<h3>{isEditing ? 'Редактировать цель' : 'Добавить цель'}</h3>
|
||||
<div className="condition-form-header">
|
||||
<h3>{isEditing ? 'Редактировать цель' : 'Добавить цель'}</h3>
|
||||
<button onClick={onCancel} className="condition-form-close-button">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label>Тип условия</label>
|
||||
@@ -1241,13 +1292,16 @@ function ConditionForm({ tasks, projects, onSubmit, onCancel, editingCondition,
|
||||
)}
|
||||
|
||||
<div className="form-actions">
|
||||
<button type="button" onClick={onCancel} className="cancel-button">
|
||||
Отмена
|
||||
</button>
|
||||
<button type="submit" className="submit-button">
|
||||
<button type="submit" className="submit-button condition-form-submit-button">
|
||||
{isEditing ? 'Сохранить' : 'Добавить'}
|
||||
</button>
|
||||
</div>
|
||||
{type === 'project_points' && calculatedWeeksText && (
|
||||
<div className="calculated-weeks-info" style={{ marginTop: '4px', textAlign: 'left', color: '#666', fontSize: '0.85em' }}>
|
||||
<span>Срок: </span>
|
||||
<span style={{ fontWeight: '600' }}>{calculatedWeeksText}</span>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user