← Назад
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> ); }