← Назадimport { authFetch } from '../../lib/api';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogClose, DialogBody } from '../ui/dialog';
import { Button } from '../ui/button';
import { Loader2, Plus } from 'lucide-react';
import { useState, useEffect, useRef } from 'react';
import { toast } from 'sonner';
interface CreateTaskModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
projectId: string;
status: 'proposed' | 'approved' | 'done' | 'verified';
onTaskCreated?: (task: Task) => void;
}
interface Task {
id: string;
title: string;
description?: string;
priority: 'low' | 'medium' | 'high' | 'critical';
status: 'proposed' | 'approved' | 'done' | 'verified';
tags?: string[];
createdAt: string;
updatedAt: string;
}
const priorityOptions: Array<{ value: 'low' | 'medium' | 'high' | 'critical'; label: string }> = [
{ value: 'low', label: 'Низкий' },
{ value: 'medium', label: 'Средний' },
{ value: 'high', label: 'Высокий' },
{ value: 'critical', label: 'Критичный' },
];
const statusLabels: Record<string, string> = {
proposed: 'Предложено',
approved: 'Одобрено',
done: 'Сделано',
verified: 'Проверено',
};
export function CreateTaskModal({ open, onOpenChange, projectId, status, onTaskCreated }: CreateTaskModalProps) {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [priority, setPriority] = useState<'low' | 'medium' | 'high' | 'critical'>('medium');
const [tags, setTags] = useState('');
const [deadline, setDeadline] = useState('');
const [isLoading, setIsLoading] = useState(false);
const titleInputRef = useRef<HTMLInputElement>(null);
// Reset form when modal opens
useEffect(() => {
if (open) {
setTitle('');
setDescription('');
setPriority('medium');
setTags('');
setDeadline('');
// Focus on title input when modal opens
setTimeout(() => {
titleInputRef.current?.focus();
}, 100);
}
}, [open]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Validation
if (title.trim().length < 3) {
toast.error('Ошибка', {
description: 'Название должно содержать минимум 3 символа',
});
return;
}
setIsLoading(true);
try {
// Parse tags
const tagsArray = tags
.split(',')
.map(t => t.trim())
.filter(t => t.length > 0);
const response = await authFetch(`/api/projects/${projectId}/tasks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: title.trim(),
description: description.trim() || undefined,
priority,
status,
tags: tagsArray.length > 0 ? tagsArray : undefined,
deadline: deadline || undefined,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(errorData.error || `HTTP ${response.status}`);
}
const result = await response.json();
// Success
toast.success('Задача создана', {
description: `"${title}" добавлена в колонку "${statusLabels[status]}"`,
duration: 3000,
});
// Callback with created task
if (onTaskCreated && result.task) {
onTaskCreated(result.task);
}
// Close modal
onOpenChange(false);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Неизвестная ошибка';
toast.error('Ошибка создания', {
description: errorMsg,
duration: 4000,
});
console.error('[Create Task] Error:', error);
} finally {
setIsLoading(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLFormElement>) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
handleSubmit(e);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Plus className="w-6 h-6 text-primary" />
Новая задача
</DialogTitle>
<DialogClose onClick={() => onOpenChange(false)} />
</DialogHeader>
<DialogBody>
<form onSubmit={handleSubmit} onKeyDown={handleKeyDown} className="space-y-4">
{/* Status info */}
<div className="bg-white/5 rounded-lg p-3 border border-white/10">
<p className="text-sm text-gray-400">
Колонка: <span className="text-white font-semibold">{statusLabels[status]}</span>
</p>
</div>
{/* Title */}
<div>
<label htmlFor="task-title" className="block text-sm font-semibold text-gray-300 mb-2">
Название задачи <span className="text-danger">*</span>
</label>
<input
ref={titleInputRef}
id="task-title"
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Например: Fix API bug"
className="w-full px-4 py-2.5 bg-white/5 border border-white/10 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
required
minLength={3}
disabled={isLoading}
/>
</div>
{/* Description */}
<div>
<label htmlFor="task-description" className="block text-sm font-semibold text-gray-300 mb-2">
Описание
</label>
<textarea
id="task-description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Дополнительные детали задачи..."
rows={3}
className="w-full px-4 py-2.5 bg-white/5 border border-white/10 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all resize-none"
disabled={isLoading}
/>
</div>
{/* Priority */}
<div>
<label htmlFor="task-priority" className="block text-sm font-semibold text-gray-300 mb-2">
Приоритет
</label>
<select
id="task-priority"
value={priority}
onChange={(e) => setPriority(e.target.value as 'low' | 'medium' | 'high' | 'critical')}
className="w-full px-4 py-2.5 bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
disabled={isLoading}
>
{priorityOptions.map((opt) => (
<option key={opt.value} value={opt.value} className="bg-slate-800">
{opt.label}
</option>
))}
</select>
</div>
{/* Tags */}
<div>
<label htmlFor="task-tags" className="block text-sm font-semibold text-gray-300 mb-2">
Теги
</label>
<input
id="task-tags"
type="text"
value={tags}
onChange={(e) => setTags(e.target.value)}
placeholder="bug, api, urgent (через запятую)"
className="w-full px-4 py-2.5 bg-white/5 border border-white/10 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
disabled={isLoading}
/>
<p className="text-xs text-gray-500 mt-1">Разделяйте теги запятыми</p>
</div>
{/* Deadline */}
<div>
<label htmlFor="task-deadline" className="block text-sm font-semibold text-gray-300 mb-2">
Дедлайн
</label>
<input
id="task-deadline"
type="date"
value={deadline}
onChange={(e) => setDeadline(e.target.value)}
className="w-full px-4 py-2.5 bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
disabled={isLoading}
/>
</div>
{/* Buttons */}
<div className="flex items-center gap-3 pt-4">
<Button
type="submit"
variant="primary"
disabled={isLoading || title.trim().length < 3}
className="flex-1"
>
{isLoading && <Loader2 className="w-4 h-4 animate-spin" />}
{isLoading ? 'Создание...' : 'Создать задачу'}
</Button>
<Button
type="button"
variant="secondary"
onClick={() => onOpenChange(false)}
disabled={isLoading}
>
Отмена
</Button>
</div>
{/* Hint */}
<p className="text-xs text-gray-500 text-center">
Ctrl+Enter для быстрого создания
</p>
</form>
</DialogBody>
</DialogContent>
</Dialog>
);
}