'use client'; import { useEffect, useState, useCallback } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import * as z from 'zod'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; 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 { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Switch } from '@/components/ui/switch'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; import { StatusBadge, StatusDot } from '@/components/status-badge'; import { ProgressRing } from '@/components/progress-ring'; import { useUser } from '@/components/user-context'; import { Label } from '@/components/ui/label'; import { calculateProgress, calculateExpectedProgress, getDaysRemaining, formatDateRange, isKRActive, } from '@/lib/kr-utils'; import { REASON_CODES, PERIOD_TYPES, DIRECTION_LABELS } from '@/lib/constants'; import { toast } from 'sonner'; import { TaskEditDialog } from '@/components/task-edit-dialog'; import type { KR, User, Checkin, Objective, Task, TaskStep } from '@/types/database'; import { ArrowLeft, Plus, Clock, TrendingUp, TrendingDown, Target, AlertTriangle, Calendar, User as UserIcon, HandHelping, Loader2, Pencil, Trash2, CheckSquare, } from 'lucide-react'; import { getErrorMessage } from '@/lib/error-utils'; interface KRWithRelations extends KR { owner: User | null; project: { id: string; name: string; status: string } | null; objective: Objective | null; } interface CheckinWithRelations extends Checkin { user: User | null; } import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; const checkinSchema = z.object({ current_value: z.number(), confidence: z.number().min(0).max(100), reason_code: z.string().optional(), note: z.string().optional(), }); type CheckinFormValues = z.infer; export default function KRDetailPage({ krId }: { krId: string }) { const router = useRouter(); const { currentUser } = useUser(); const [kr, setKR] = useState(null); const [checkins, setCheckins] = useState([]); const [tasks, setTasks] = useState<(Task & { steps: TaskStep[] })[]>([]); const [isLoading, setIsLoading] = useState(true); const [loadError, setLoadError] = useState(null); const [isCheckinOpen, setIsCheckinOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [isEditing, setIsEditing] = useState(false); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); const [isTaskDialogOpen, setIsTaskDialogOpen] = useState(false); const [editingTask, setEditingTask] = useState<(Task & { steps: TaskStep[] }) | null>(null); const fetchData = useCallback(async () => { try { const [krRes, checkinsRes, tasksRes] = await Promise.all([ fetch(`/api/krs/${krId}`), fetch(`/api/checkins?kr_id=${krId}`), fetch(`/api/tasks?kr_id=${krId}`), ]); if (krRes.status === 403) { setLoadError('你没有权限查看此 KR'); setKR(null); return; } if (krRes.status === 404) { setLoadError('KR 不存在'); setKR(null); return; } if (krRes.ok) { const krData = await krRes.json(); setKR(krData); setLoadError(null); } if (checkinsRes.ok) { const checkinsData = await checkinsRes.json(); setCheckins(checkinsData); } if (tasksRes.ok) { const tasksData = await tasksRes.json(); setTasks(tasksData); } } catch (error) { console.error('Failed to fetch KR data:', error); setLoadError('加载失败'); } finally { setIsLoading(false); } }, [krId]); useEffect(() => { fetchData(); }, [fetchData]); const form = useForm({ resolver: zodResolver(checkinSchema), defaultValues: { current_value: kr?.current_value || 0, confidence: kr?.confidence || 80, reason_code: '', note: '', }, }); useEffect(() => { if (kr) { form.reset({ current_value: kr.current_value, confidence: kr.confidence, reason_code: kr.reason_code || '', note: '', }); } }, [kr, form]); const onCheckinSubmit = async (values: CheckinFormValues) => { if (!kr) return; setIsSubmitting(true); try { const res = await fetch('/api/checkins', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kr_id: kr.id, project_id: kr.project_id, user_id: currentUser?.id, current_value: values.current_value, confidence: values.confidence, reason_code: values.reason_code || null, note: values.note || null, }), }); if (!res.ok) { const data = await res.json(); throw new Error(data.error || '打卡失败'); } toast.success('打卡成功'); setIsCheckinOpen(false); fetchData(); } catch (error) { toast.error(getErrorMessage(error)); } finally { setIsSubmitting(false); } }; const toggleNeedHelp = async () => { if (!kr) return; try { const res = await fetch(`/api/krs/${kr.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ need_owner_help: !kr.need_owner_help, skipReasonCodeCheck: true, }), }); if (res.ok) { toast.success(kr.need_owner_help ? '已取消求助' : '已发送求助请求'); fetchData(); } } catch (error) { toast.error('操作失败'); } }; const handleDeleteKR = async () => { if (!kr) return; setIsDeleting(true); try { const res = await fetch(`/api/krs/${kr.id}`, { method: 'DELETE' }); if (!res.ok) throw new Error('删除失败'); toast.success('KR 已删除'); router.push(`/projects/${kr.project_id}`); } catch (error) { toast.error(getErrorMessage(error)); } finally { setIsDeleting(false); } }; const handleEditKR = async (e: React.FormEvent) => { e.preventDefault(); if (!kr) return; setIsEditing(true); const formData = new FormData(e.currentTarget); try { const res = await fetch(`/api/krs/${kr.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: formData.get('title'), metric_name: formData.get('metric_name'), unit: formData.get('unit'), target_value: parseFloat(formData.get('target_value') as string), period_start: formData.get('period_start'), period_end: formData.get('period_end'), }), }); if (!res.ok) throw new Error('更新失败'); toast.success('KR 已更新'); setIsEditDialogOpen(false); fetchData(); } catch (error) { toast.error(getErrorMessage(error)); } finally { setIsEditing(false); } }; if (isLoading) { return (
); } if (!kr) { return (

{loadError || 'KR 不存在'}

); } const progress = calculateProgress(kr); const expectedProgress = calculateExpectedProgress(kr); const daysLeft = getDaysRemaining(kr.period_end); const isActive = isKRActive(kr); const isOwner = currentUser?.id === kr.owner_user_id; const TrendIcon = kr.direction === 'increase' ? TrendingUp : TrendingDown; const handleTaskStatusChange = async (taskId: string, newStatus: string) => { try { const res = await fetch(`/api/tasks/${taskId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: newStatus }), }); if (res.ok) { setTasks(tasks.map(t => t.id === taskId ? { ...t, status: newStatus as any } : t)); toast.success('计划状态已更新'); } } catch (error) { toast.error('更新失败'); } }; const isBooleanMetric = kr?.unit === '状态' && kr?.metric_name === '完成状态'; return (
返回我的 KR

{kr.title}

{kr.need_owner_help && ( 需要帮助 )}
{kr.project?.name} | {kr.owner?.name || '未分配'} | {formatDateRange(kr.period_start, kr.period_end)}
{isActive && (
{currentUser?.role === 'owner' && ( <> 编辑 KR
确定要删除此 KR 吗? 此操作无法撤销。这将永久删除 KR 及其所有打卡记录。 取消 {isDeleting && } 删除 )} KR 进度打卡
( 当前状态 {isBooleanMetric ? ( field.onChange(parseInt(val))} defaultValue={field.value.toString()} className="flex flex-col space-y-1" >
) : ( <> field.onChange(parseFloat(e.target.value) || 0)} />

目标值: {kr.target_value} {kr.unit}

)}
)} /> ( 信心度 ({field.value}%) field.onChange(parseInt(e.target.value))} className="w-full" />
0% 50% 100%
)} /> ( 问题原因 (如有) )} /> ( 备注