6.10.0: Синий блок настроек для задач
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m6s

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
poignatov
2026-03-10 23:49:24 +03:00
parent 4b28d90d68
commit dc50433eb1
4 changed files with 171 additions and 171 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "play-life-web",
"version": "6.9.2",
"version": "6.10.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -227,23 +227,24 @@
}
.add-subtask-button {
padding: 0.375rem;
background: #6366f1;
color: white;
border: none;
padding: 0.5rem;
background: white;
color: #3498db;
border: 1px dashed #bae6fd;
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
min-width: 2rem;
height: 2rem;
width: 100%;
height: 2.5rem;
margin-top: 0.5rem;
}
.add-subtask-button:hover {
background: #4f46e5;
background: #e0f2fe;
border-color: #3498db;
}
.subtask-form-item {
@@ -311,9 +312,9 @@
}
.remove-subtask-button {
padding: 0.5rem;
background: #ef4444;
color: white;
padding: 0.25rem;
background: none;
color: #9ca3af;
border: none;
border-radius: 0.375rem;
cursor: pointer;
@@ -322,12 +323,12 @@
align-items: center;
justify-content: center;
flex-shrink: 0;
min-width: 2.5rem;
height: 2.5rem;
min-width: 2rem;
height: 2rem;
}
.remove-subtask-button:hover {
background: #dc2626;
color: #ef4444;
}
.error-message {
@@ -459,13 +460,23 @@
padding: 1rem;
}
.test-config-section > label {
.test-config-section > label,
.test-config-section > .subtasks-header > label {
font-size: 1rem;
font-weight: 600;
color: #3498db;
margin-bottom: 1rem !important;
}
.test-config-section > .subtasks-header {
margin-bottom: 0.5rem;
}
.test-config-section .subtask-form-item {
background: white;
border-color: #bae6fd;
}
.test-config-fields {
display: grid;
grid-template-columns: 1fr 1fr;

View File

@@ -920,6 +920,148 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
/>
</div>
{/* Subtask-specific fields (regular task) */}
{!isTest && !isPurchase && !wishlistInfo && (
<div className="form-group test-config-section">
<label>Настройки задачи</label>
<div className="test-field-group" style={{ marginBottom: '1rem' }}>
<label htmlFor="progression_base">Прогрессия</label>
<input
id="progression_base"
type="number"
step="any"
value={progressionBase}
onChange={(e) => setProgressionBase(e.target.value)}
placeholder="Базовое значение"
className="form-input"
/>
<small style={{ color: '#666', fontSize: '0.9em' }}>
Оставьте пустым, если прогрессия не используется
</small>
</div>
<label style={{ fontSize: '0.875rem' }}>Подзадачи</label>
{subtasks.map((subtask, index) => (
<div key={index} className="subtask-form-item">
<div className="subtask-header-row">
<div className="subtask-position-controls">
<button
type="button"
onClick={() => handleMoveSubtaskUp(index)}
className="move-subtask-button"
disabled={index === 0}
title="Переместить вверх"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="18 15 12 9 6 15"></polyline>
</svg>
</button>
<button
type="button"
onClick={() => handleMoveSubtaskDown(index)}
className="move-subtask-button"
disabled={index === subtasks.length - 1}
title="Переместить вниз"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
</div>
<input
type="text"
value={subtask.name}
onChange={(e) => handleSubtaskChange(index, 'name', e.target.value)}
placeholder="Название подзадачи"
className="form-input subtask-name-input"
/>
<button
type="button"
onClick={() => handleRemoveSubtask(index)}
className="remove-subtask-button"
title="Удалить подзадачу"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<textarea
value={subtask.reward_message}
onChange={(e) => handleSubtaskRewardMessageChange(index, e.target.value)}
placeholder="Сообщение награды (опционально)"
className="form-textarea"
rows={2}
/>
{subtask.rewards && subtask.rewards.length > 0 && (
<div className="subtask-rewards">
{subtask.rewards.map((reward, rIndex) => {
return (
<div key={rIndex} className="reward-item">
<span className="reward-number">{rIndex}</span>
<input
type="text"
value={reward.project_name}
onChange={(e) => {
const newSubtasks = [...subtasks]
newSubtasks[index].rewards[rIndex].project_name = e.target.value
setSubtasks(newSubtasks)
}}
placeholder="Проект"
className="form-input reward-project-input"
list={`subtask-projects-${index}-${rIndex}`}
/>
<datalist id={`subtask-projects-${index}-${rIndex}`}>
{projects.map(p => (
<option key={p.project_id} value={p.project_name} />
))}
</datalist>
<input
type="number"
step="any"
value={reward.value}
onChange={(e) => {
const newSubtasks = [...subtasks]
newSubtasks[index].rewards[rIndex].value = e.target.value
setSubtasks(newSubtasks)
}}
placeholder="Score"
className="form-input reward-score-input"
/>
{progressionBase && (
<button
type="button"
tabIndex={0}
className={`progression-button progression-button-subtask ${reward.use_progression ? 'progression-button-filled' : 'progression-button-outlined'}`}
onMouseDown={(e) => {
e.preventDefault()
const newSubtasks = [...subtasks]
newSubtasks[index].rewards[rIndex].use_progression = !newSubtasks[index].rewards[rIndex].use_progression
setSubtasks(newSubtasks)
}}
title={reward.use_progression ? 'Отключить прогрессию' : 'Включить прогрессию'}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</button>
)}
</div>
)})}
</div>
)}
</div>
))}
<button type="button" onClick={handleAddSubtask} className="add-subtask-button" title="Добавить подзадачу">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div>
)}
{/* Информация о связанном желании */}
{wishlistInfo && (
<div className="form-group">
@@ -939,7 +1081,7 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
<option value="general">Общая</option>
</select>
<small style={{ color: '#666', fontSize: '0.9em', display: 'block', marginTop: '4px' }}>
{rewardPolicy === 'personal'
{rewardPolicy === 'personal'
? 'Задача выполняется только если вы сами завершили желание. Если другой пользователь завершит желание, задача будет удалена.'
: 'Задача выполняется если кто-либо (неважно кто) отметил желание завершённым.'}
</small>
@@ -948,29 +1090,6 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
</div>
)}
{!isTest && !wishlistInfo && (
<div className="form-group">
<label htmlFor="progression_base">Прогрессия</label>
<input
id="progression_base"
type="number"
step="any"
value={progressionBase}
onChange={(e) => {
if (!wishlistInfo) {
setProgressionBase(e.target.value)
}
}}
placeholder="Базовое значение"
className="form-input"
disabled={wishlistInfo !== null}
/>
<small style={{ color: wishlistInfo ? '#e74c3c' : '#666', fontSize: '0.9em' }}>
{wishlistInfo ? 'Задачи, привязанные к желанию, не могут иметь прогрессию' : 'Оставьте пустым, если прогрессия не используется'}
</small>
</div>
)}
{/* Test-specific fields */}
{isTest && (
<div className="form-group test-config-section">
@@ -1242,136 +1361,6 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
)}
</div>
{!isTest && !isPurchase && (
<div className="form-group">
<div className="subtasks-header">
<label>Подзадачи</label>
<button type="button" onClick={handleAddSubtask} className="add-subtask-button" title="Добавить подзадачу">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div>
{subtasks.map((subtask, index) => (
<div key={index} className="subtask-form-item">
<div className="subtask-header-row">
<div className="subtask-position-controls">
<button
type="button"
onClick={() => handleMoveSubtaskUp(index)}
className="move-subtask-button"
disabled={index === 0}
title="Переместить вверх"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="18 15 12 9 6 15"></polyline>
</svg>
</button>
<button
type="button"
onClick={() => handleMoveSubtaskDown(index)}
className="move-subtask-button"
disabled={index === subtasks.length - 1}
title="Переместить вниз"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
</div>
<input
type="text"
value={subtask.name}
onChange={(e) => handleSubtaskChange(index, 'name', e.target.value)}
placeholder="Название подзадачи"
className="form-input subtask-name-input"
/>
<button
type="button"
onClick={() => handleRemoveSubtask(index)}
className="remove-subtask-button"
title="Удалить подзадачу"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
<line x1="10" y1="11" x2="10" y2="17"></line>
<line x1="14" y1="11" x2="14" y2="17"></line>
</svg>
</button>
</div>
<textarea
value={subtask.reward_message}
onChange={(e) => handleSubtaskRewardMessageChange(index, e.target.value)}
placeholder="Сообщение награды (опционально)"
className="form-textarea"
rows={2}
/>
{subtask.rewards && subtask.rewards.length > 0 && (
<div className="subtask-rewards">
{subtask.rewards.map((reward, rIndex) => {
return (
<div key={rIndex} className="reward-item">
<span className="reward-number">{rIndex}</span>
<input
type="text"
value={reward.project_name}
onChange={(e) => {
const newSubtasks = [...subtasks]
newSubtasks[index].rewards[rIndex].project_name = e.target.value
setSubtasks(newSubtasks)
}}
placeholder="Проект"
className="form-input reward-project-input"
list={`subtask-projects-${index}-${rIndex}`}
/>
<datalist id={`subtask-projects-${index}-${rIndex}`}>
{projects.map(p => (
<option key={p.project_id} value={p.project_name} />
))}
</datalist>
<input
type="number"
step="any"
value={reward.value}
onChange={(e) => {
const newSubtasks = [...subtasks]
newSubtasks[index].rewards[rIndex].value = e.target.value
setSubtasks(newSubtasks)
}}
placeholder="Score"
className="form-input reward-score-input"
/>
{progressionBase && (
<button
type="button"
tabIndex={0}
className={`progression-button progression-button-subtask ${reward.use_progression ? 'progression-button-filled' : 'progression-button-outlined'}`}
onMouseDown={(e) => {
e.preventDefault()
const newSubtasks = [...subtasks]
newSubtasks[index].rewards[rIndex].use_progression = !newSubtasks[index].rewards[rIndex].use_progression
setSubtasks(newSubtasks)
}}
title={reward.use_progression ? 'Отключить прогрессию' : 'Включить прогрессию'}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</button>
)}
</div>
)})}
</div>
)}
</div>
))}
</div>
)}
{/* Показываем ошибку валидации только если это ошибка валидации, не ошибка действия */}
{error && (error.includes('обязательно') || error.includes('должны быть заполнены') || error.includes('нельзя одновременно')) && (
<div className="error-message">{error}</div>