import { useEffect, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { createTodo } from "../api/todos"; import { fetchProjects } from "../api/projects"; import { fetchTeamWeeklyReports, fetchMyWeeklyReport, submitMyWeeklyReport, updateMyWeeklyReport, type WeeklyReportProjectLink } from "../api/weeklyReports"; import { useAuth } from "../auth/AuthContext"; import { hasAnyPermission } from "../auth/permissions"; import { PageHeader } from "../components/common/PageHeader"; import { FilterBar, ListWorkspace, PrimaryTable } from "../components/patterns/ListWorkspace"; import { MetricCard } from "../components/patterns/MetricCard"; import { SectionPanel } from "../components/patterns/SectionPanel"; import { Button } from "../components/ui/button"; import { Textarea } from "../components/ui/textarea"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; import { toast } from "sonner"; import { isApiError } from "../api/types"; import { mapApiErrorMessage } from "../api/errorMessages"; import { TEST_IDS } from "../lib/testIds"; function errorMessage(error: unknown, fallback: string) { if (isApiError(error)) { return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`; } return error instanceof Error ? error.message : fallback; } type DraftProjectLink = { projectId: string; progressText: string; riskText: string; nextStepText: string; }; function emptyProjectLink(): DraftProjectLink { return { projectId: "", progressText: "", riskText: "", nextStepText: "" }; } function mapProjectLink(item: WeeklyReportProjectLink): DraftProjectLink { return { projectId: item.projectId, progressText: item.progressText ?? "", riskText: item.riskText ?? "", nextStepText: item.nextStepText ?? "", }; } export function WeeklyReportsPage() { const queryClient = useQueryClient(); const { permissions } = useAuth(); const canViewTeam = hasAnyPermission(permissions, ["report.dashboard.view", "system.users"]); const [weekStart, setWeekStart] = useState(""); const reportQuery = useQuery({ queryKey: ["weekly-report", "me", weekStart], queryFn: () => fetchMyWeeklyReport(weekStart || undefined) }); const teamQuery = useQuery({ enabled: canViewTeam, queryKey: ["weekly-report", "team", weekStart], queryFn: () => fetchTeamWeeklyReports(weekStart || undefined) }); const projectsQuery = useQuery({ queryKey: ["projects", "weekly-links"], queryFn: () => fetchProjects({ mine: true }) }); const report = reportQuery.data?.item; const [draft, setDraft] = useState({ highlightsText: "", blockersText: "", nextWeekText: "", needsHelp: false }); const [projectLinks, setProjectLinks] = useState([emptyProjectLink()]); useEffect(() => { if (!report) return; setDraft({ highlightsText: report.highlightsText, blockersText: report.blockersText, nextWeekText: report.nextWeekText, needsHelp: report.needsHelp, }); setProjectLinks(report.projectLinks.length ? report.projectLinks.map(mapProjectLink) : [emptyProjectLink()]); }, [report]); const normalizedProjectLinks = projectLinks .filter((item) => item.projectId) .map((item) => ({ projectId: item.projectId, progressText: item.progressText.trim() || undefined, riskText: item.riskText.trim() || undefined, nextStepText: item.nextStepText.trim() || undefined, })); const linkedProjectCount = useMemo( () => projectLinks.filter((item) => item.projectId).length, [projectLinks], ); const saveMutation = useMutation({ mutationFn: () => updateMyWeeklyReport({ weekStart: weekStart || undefined, ...draft, projectLinks: normalizedProjectLinks }), onSuccess: async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["weekly-report", "me"] }), queryClient.invalidateQueries({ queryKey: ["weekly-report", "team"] }), queryClient.invalidateQueries({ queryKey: ["portal-home"] }), queryClient.invalidateQueries({ queryKey: ["projects"] }), ]); toast.success("周报已保存"); }, onError: (error) => toast.error(errorMessage(error, "保存失败")), }); const submitMutation = useMutation({ mutationFn: () => submitMyWeeklyReport({ weekStart: weekStart || undefined }), onSuccess: async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["weekly-report", "me"] }), queryClient.invalidateQueries({ queryKey: ["weekly-report", "team"] }), queryClient.invalidateQueries({ queryKey: ["portal-home"] }), ]); toast.success("周报已提交"); }, onError: (error) => toast.error(errorMessage(error, "提交失败")), }); const createTodoMutation = useMutation({ mutationFn: () => createTodo({ title: `${report?.weekStart ?? "本周"} 周计划`, description: draft.nextWeekText || "来自周报的下周计划", source: "weekly_report", priority: draft.needsHelp ? "high" : "normal", projectId: normalizedProjectLinks[0]?.projectId ?? null, }), onSuccess: async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["todos"] }), queryClient.invalidateQueries({ queryKey: ["portal-home"] }), ]); toast.success("已从周报生成待办"); }, onError: (error) => toast.error(errorMessage(error, "生成待办失败")), }); return (
按周归档
默认按周一归档;提交后会同步更新 Portal 与项目摘要。
setWeekStart(event.target.value)} />
0 ? "warning" : "neutral"} />
{reportQuery.isLoading ?
周报加载中...
: null} {reportQuery.isError ?
{errorMessage(reportQuery.error, "加载周报失败")}
: null}