'use client'; export const dynamic = 'force-dynamic'; import { useEffect, useState, useMemo } from 'react'; import { useSearchParams } 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 { StatusDot } from '@/components/status-badge'; import { ProgressRing } from '@/components/progress-ring'; import { useUser } from '@/components/user-context'; import { calculateProgress, formatDateRange } from '@/lib/kr-utils'; import { REASON_CODES } from '@/lib/constants'; import type { Project, KR, User, Meeting } from '@/types/database'; import { toast } from 'sonner'; import { CalendarClock, FileText, AlertTriangle, CheckCircle, Loader2, Save, Presentation, } from 'lucide-react'; import { getErrorMessage } from '@/lib/error-utils'; interface ProjectWithRelations extends Project { owner: User | null; pm: User | null; } interface KRWithRelations extends KR { owner: User | null; } const meetingSchema = z.object({ summary: z.string().min(1, '总结不能为空'), decisions: z.string().optional(), risks: z.string().optional(), next_period_commitments: z.string().optional(), }); type MeetingFormValues = z.infer; export default function MeetingPage() { const searchParams = useSearchParams(); const initialProjectId = searchParams.get('project_id'); const { currentUser } = useUser(); const [projects, setProjects] = useState([]); const [krs, setKRs] = useState([]); const [meetings, setMeetings] = useState([]); const [selectedProjectId, setSelectedProjectId] = useState(initialProjectId || ''); const [periodStart, setPeriodStart] = useState(''); const [periodEnd, setPeriodEnd] = useState(''); const [isLoading, setIsLoading] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); const [showPresentation, setShowPresentation] = useState(false); const form = useForm({ resolver: zodResolver(meetingSchema), defaultValues: { summary: '', decisions: '', risks: '', next_period_commitments: '', }, }); useEffect(() => { async function fetchData() { try { const [projectsRes, krsRes, meetingsRes] = await Promise.all([ fetch('/api/projects'), fetch('/api/krs'), fetch('/api/meetings'), ]); if (projectsRes.ok) { const data = await projectsRes.json(); setProjects(data); } if (krsRes.ok) { const data = await krsRes.json(); setKRs(data); } if (meetingsRes.ok) { const data = await meetingsRes.json(); setMeetings(data); } } catch (error) { console.error('Failed to fetch data:', error); } finally { setIsLoading(false); } } fetchData(); const today = new Date(); const weekAgo = new Date(today); weekAgo.setDate(weekAgo.getDate() - 7); setPeriodStart(weekAgo.toISOString().split('T')[0]); setPeriodEnd(today.toISOString().split('T')[0]); }, []); const periodKRs = useMemo(() => { if (!selectedProjectId || !periodStart || !periodEnd) return []; return krs.filter(kr => { if (kr.project_id !== selectedProjectId) return false; return kr.period_start <= periodEnd && kr.period_end >= periodStart; }); }, [krs, selectedProjectId, periodStart, periodEnd]); const completedKRs = periodKRs.filter(kr => calculateProgress(kr) >= 1); const incompleteKRs = periodKRs.filter(kr => calculateProgress(kr) < 1); const riskKRs = periodKRs.filter(kr => kr.status === 'red' || kr.status === 'yellow'); const generateSummary = () => { const totalCount = periodKRs.length; const completedCount = completedKRs.length; const completionRate = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0; let summary = `本周期完成情况总结:\n`; summary += `- 共 ${totalCount} 个 KR,完成 ${completedCount} 个\n`; summary += `- 完成率:${completionRate}%\n\n`; if (completedKRs.length > 0) { summary += `已完成:\n`; completedKRs.forEach(kr => { summary += `- ${kr.title} (${kr.current_value}/${kr.target_value} ${kr.unit})\n`; }); summary += '\n'; } if (incompleteKRs.length > 0) { summary += `未完成:\n`; incompleteKRs.forEach(kr => { const progress = Math.round(calculateProgress(kr) * 100); summary += `- ${kr.title} (进度 ${progress}%`; if (kr.reason_code) { summary += ` - ${REASON_CODES[kr.reason_code]}`; } summary += `)\n`; }); } form.setValue('summary', summary); if (riskKRs.length > 0) { let risks = ''; riskKRs.forEach(kr => { risks += `- ${kr.title}`; if (kr.reason_code) { risks += `: ${REASON_CODES[kr.reason_code]}`; } if (kr.note) { risks += ` (${kr.note})`; } risks += '\n'; }); form.setValue('risks', risks); } }; const onSubmit = async (values: MeetingFormValues) => { if (!selectedProjectId || !periodStart || !periodEnd) { toast.error('请选择项目和周期'); return; } setIsSubmitting(true); try { const res = await fetch('/api/meetings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project_id: selectedProjectId, period_start: periodStart, period_end: periodEnd, ...values, created_by: currentUser?.id, }), }); if (!res.ok) { const data = await res.json(); throw new Error(data.error || '保存失败'); } toast.success('会议纪要已保存'); form.reset(); } catch (error) { toast.error(error instanceof Error ? error.message : '保存失败'); } finally { setIsSubmitting(false); } }; const selectedProject = projects.find(p => p.id === selectedProjectId); if (isLoading) { return (
); } if (showPresentation) { return (

{selectedProject?.name} 例会

{formatDateRange(periodStart, periodEnd)}

{periodKRs.length}

总 KR 数

{completedKRs.length}

已完成

{riskKRs.length}

需关注

KR 完成情况 {periodKRs.map(kr => (

{kr.title}

{kr.owner?.name}

{kr.current_value}/{kr.target_value} {kr.unit}

))}
{form.getValues('risks') && ( 风险与阻塞
                  {form.getValues('risks')}
                
)}
); } return (

例会模式

选择项目和周期,自动生成会议总结

会议设置
setPeriodStart(e.target.value)} />
setPeriodEnd(e.target.value)} />
{selectedProjectId && periodKRs.length > 0 && ( 周期内 KR ({periodKRs.length}) {periodKRs.map(kr => (

{kr.title}

{kr.owner?.name}

{kr.current_value}/{kr.target_value} {kr.unit}

{kr.reason_code && (

{REASON_CODES[kr.reason_code]}

)}
))}
)} 会议纪要
( 总结 *