'use client'; import { useState, useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import * as z from 'zod'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Slider } from '@/components/ui/slider'; import { Label } from '@/components/ui/label'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Checkbox } from '@/components/ui/checkbox'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Loader2, Calendar, Trash2, Plus, Sparkles, X } from 'lucide-react'; import { useUser } from '@/components/user-context'; import { toast } from 'sonner'; import { formatDateRange } from '@/lib/kr-utils'; import type { Task, TaskStep } from '@/types/database'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog'; import { fetchApi } from '@/lib/api-client'; const taskSchema = z.object({ title: z.string().min(1, '计划标题不能为空'), description: z.string().optional(), status: z.enum(['todo', 'in_progress', 'expected', 'done']), priority: z.number().min(1).max(5), urgency: z.number().min(1).max(5), owner_user_id: z.string().optional(), executor_ids: z.array(z.string()), start_date: z.string().optional(), end_date: z.string().optional(), help_requested_to_user_id: z.string().optional(), }); type TaskFormValues = z.infer; interface TaskEditDialogProps { open: boolean; onOpenChange: (open: boolean) => void; krId?: string; projectId: string; // Used for context projectTitle: string; task?: Task & { steps?: TaskStep[] } | null; // If null, creating new plan onSuccess: () => void; } export function TaskEditDialog({ open, onOpenChange, krId, projectId, projectTitle, task, onSuccess, }: TaskEditDialogProps) { const HELP_NONE_VALUE = '__none__'; const { users, currentUser } = useUser(); const [isSubmitting, setIsSubmitting] = useState(false); const [activeTab, setActiveTab] = useState('details'); const [steps, setSteps] = useState([]); const [newStepContent, setNewStepContent] = useState(''); const [newStepStartDate, setNewStepStartDate] = useState(new Date().toISOString().split('T')[0]); const [newStepEndDate, setNewStepEndDate] = useState(new Date().toISOString().split('T')[0]); const [newStepHelpToUserId, setNewStepHelpToUserId] = useState(HELP_NONE_VALUE); const [isDeleteTaskOpen, setIsDeleteTaskOpen] = useState(false); const [isDeletingTask, setIsDeletingTask] = useState(false); const [deleteStepId, setDeleteStepId] = useState(null); const [isDeleteStepOpen, setIsDeleteStepOpen] = useState(false); const [isDeletingStep, setIsDeletingStep] = useState(false); // Initialize form const form = useForm({ resolver: zodResolver(taskSchema), defaultValues: { title: '', description: '', status: 'todo', priority: 3, urgency: 3, owner_user_id: '', executor_ids: [], start_date: new Date().toISOString().split('T')[0], end_date: new Date().toISOString().split('T')[0], help_requested_to_user_id: HELP_NONE_VALUE, }, }); // Reset form when task changes useEffect(() => { if (open) { if (task) { form.reset({ title: task.title, description: task.description || '', status: task.status as any, priority: task.priority || 3, urgency: task.urgency || 3, owner_user_id: task.owner_user_id || '', executor_ids: task.executor_ids || [], start_date: task.start_date || '', end_date: task.end_date || '', help_requested_to_user_id: task.help_requested_to_user_id || HELP_NONE_VALUE, }); setSteps(task.steps || []); } else { form.reset({ title: '', description: '', status: 'todo', priority: 3, urgency: 3, owner_user_id: '', executor_ids: [], start_date: new Date().toISOString().split('T')[0], end_date: new Date().toISOString().split('T')[0], help_requested_to_user_id: HELP_NONE_VALUE, }); setSteps([]); } setNewStepHelpToUserId(HELP_NONE_VALUE); setIsDeleteTaskOpen(false); setIsDeletingTask(false); setDeleteStepId(null); setIsDeleteStepOpen(false); setIsDeletingStep(false); } }, [open, task, form]); const onSubmit = async (values: TaskFormValues) => { setIsSubmitting(true); try { // 1. Create or update task const url = task ? `/api/tasks/${task.id}` : '/api/tasks'; const method = task ? 'PATCH' : 'POST'; const taskRes = await fetchApi(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kr_id: krId || null, project_id: projectId && projectId !== "" ? projectId : null, title: values.title, description: values.description || null, status: values.status, priority: values.priority, urgency: values.urgency, owner_user_id: values.owner_user_id && values.owner_user_id !== "" ? values.owner_user_id : null, executor_ids: values.executor_ids || [], start_date: values.start_date && values.start_date !== "" ? values.start_date : null, end_date: values.end_date && values.end_date !== "" ? values.end_date : null, help_requested_to_user_id: values.help_requested_to_user_id && values.help_requested_to_user_id !== HELP_NONE_VALUE ? values.help_requested_to_user_id : null, help_requested_by_user_id: values.help_requested_to_user_id && values.help_requested_to_user_id !== HELP_NONE_VALUE ? (currentUser?.id || null) : null, }), }); if (!taskRes.ok) { let errorMessage = '保存计划失败'; try { const errorData = await taskRes.json(); if (errorData?.error) errorMessage = errorData.error; } catch { const text = await taskRes.text().catch(() => ''); if (text) errorMessage = text; } throw new Error(errorMessage); } const savedTask = await taskRes.json(); // 2. Handle steps (only for new steps added in UI, existing ones are handled individually) // Note: In a real app, we might want to batch this. For now, we'll just save the task. // Steps created during edit are saved immediately in this implementation below. // Save any local (temp) steps const localSteps = steps.filter(s => s.id.startsWith('temp-')); if (localSteps.length > 0) { await Promise.all(localSteps.map(step => fetchApi('/api/task-steps', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ task_id: savedTask.id, content: step.content, start_date: step.start_date, end_date: step.end_date, is_completed: step.is_completed, }), }) )); } toast.success('计划保存成功'); onSuccess(); onOpenChange(false); } catch (error) { console.error(error); toast.error(error instanceof Error ? error.message : '保存失败'); } finally { setIsSubmitting(false); } }; const handleDeleteTask = async () => { if (!task) return; try { setIsDeletingTask(true); await fetchApi(`/api/tasks/${task.id}`, { method: 'DELETE' }); toast.success('计划已删除'); onSuccess(); onOpenChange(false); } catch (error) { toast.error('删除失败'); } finally { setIsDeletingTask(false); setIsDeleteTaskOpen(false); } }; const handleAddStep = async () => { if (!newStepContent.trim()) return; // If task exists, save step to DB immediately if (task) { try { const res = await fetchApi('/api/task-steps', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ task_id: task.id, content: newStepContent, start_date: newStepStartDate, end_date: newStepEndDate, help_requested_to_user_id: newStepHelpToUserId !== HELP_NONE_VALUE ? newStepHelpToUserId : null, help_requested_by_user_id: newStepHelpToUserId !== HELP_NONE_VALUE ? (currentUser?.id || null) : null, }), }); if (res.ok) { const newStep = await res.json(); setSteps([...steps, newStep]); setNewStepContent(''); setNewStepHelpToUserId(HELP_NONE_VALUE); } } catch (error) { toast.error('添加步骤失败'); } } else { // If task is new (not saved yet), add to local state with temp ID const tempStep: TaskStep = { id: `temp-${Date.now()}`, task_id: '', // Placeholder content: newStepContent, note: null, executor_id: null, start_date: newStepStartDate, end_date: newStepEndDate, is_completed: false, help_requested_to_user_id: newStepHelpToUserId !== HELP_NONE_VALUE ? newStepHelpToUserId : null, help_requested_by_user_id: newStepHelpToUserId !== HELP_NONE_VALUE ? (currentUser?.id || null) : null, help_requested_at: newStepHelpToUserId !== HELP_NONE_VALUE ? new Date().toISOString() : null, created_at: new Date().toISOString(), }; setSteps([...steps, tempStep]); setNewStepContent(''); setNewStepHelpToUserId(HELP_NONE_VALUE); } }; const toggleStepComplete = async (step: TaskStep) => { // If it's a local step, just update state if (step.id.startsWith('temp-')) { setSteps(steps.map(s => s.id === step.id ? { ...s, is_completed: !s.is_completed } : s)); return; } try { const res = await fetchApi('/api/task-steps', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: step.id, is_completed: !step.is_completed, }), }); if (res.ok) { setSteps(steps.map(s => s.id === step.id ? { ...s, is_completed: !s.is_completed } : s)); } } catch (error) { toast.error('更新状态失败'); } }; const deleteStepNow = async (stepId: string) => { // If it's a local step, just remove from state if (stepId.startsWith('temp-')) { setSteps(steps.filter(s => s.id !== stepId)); return; } try { const res = await fetchApi(`/api/task-steps?id=${stepId}`, { method: 'DELETE' }); if (res.ok) { setSteps(steps.filter(s => s.id !== stepId)); } } catch (error) { toast.error('删除步骤失败'); } }; const requestDeleteStep = (stepId: string) => { setDeleteStepId(stepId); setIsDeleteStepOpen(true); }; const confirmDeleteStep = async () => { if (!deleteStepId) return; try { setIsDeletingStep(true); await deleteStepNow(deleteStepId); } finally { setIsDeletingStep(false); setIsDeleteStepOpen(false); setDeleteStepId(null); } }; return (
{task ? '编辑计划' : '新建计划'}

项目 {projectTitle}

{/* Close button is built-in to DialogContent but we can add custom header actions here */}
{/* Left Column: Task Form */}
└ {projectTitle}
{form.formState.errors.title && (

{form.formState.errors.title.message}

)}