import { useState } from 'react' import { useSearchParams } from 'react-router-dom' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useAuthStore } from '@/app/store/auth' import { toast, errorMessage } from '@/app/store/toast' import { confirmDialog } from '@/app/store/confirm' import { PageLoading, ErrorState, EmptyState } from '@/components/ui/feedback' import { PageHeader } from '@/components/common/PageHeader' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Card } from '@/components/ui/card' import { Avatar } from '@/components/ui/avatar' import { Checkbox } from '@/components/ui/checkbox' import { Tabs } from '@/components/ui/tabs' import { Table, THead, TBody, Tr, Th, Td } from '@/components/ui/table' import { CountryFlag } from '@/components/common/CountryFlag' import { IntentScoreBadge, OpportunityStageBadge, TaskPriorityBadge } from '@/components/common/StatusBadges' import { TaskFormDialog } from '@/components/crm/TaskFormDialog' import { getOpportunities, updateOpportunity, getTasks, updateTask } from '@/services/api/crm' import { getTeam } from '@/services/api/org' import { OPPORTUNITY_STAGES, taskTypeLabel, TASK_TYPES, TASK_PRIORITIES } from '@/config/constants' import { formatDateTime, formatMoney } from '@/utils/format' import { cn } from '@/utils/cn' import type { FollowTask, Opportunity } from '@/types' export function OpportunitiesPage() { const queryClient = useQueryClient() const user = useAuthStore((s) => s.user) const [searchParams, setSearchParams] = useSearchParams() const requestedView = searchParams.get('tab') const [view, setView] = useState( requestedView === 'tasks' || requestedView === 'calendar' ? requestedView : 'kanban', ) const [taskFilter, setTaskFilter] = useState('all') const [taskDialog, setTaskDialog] = useState(false) const { data: oppData, isLoading: oppLoading } = useQuery({ queryKey: ['opportunities'], queryFn: () => getOpportunities(), }) const { data: taskData, isLoading: taskLoading } = useQuery({ queryKey: ['tasks', taskFilter], queryFn: () => getTasks({ filter: taskFilter, ownerId: user?.id }), }) const { data: team = [] } = useQuery({ queryKey: ['team'], queryFn: getTeam }) const memberNames = new Map(team.map((member) => [member.id, member.name])) const oppMut = useMutation({ mutationFn: ({ id, stage }: { id: string; stage: string }) => updateOpportunity(id, { stage: stage as any }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['opportunities'] }) toast.success('商机阶段已更新') }, onError: (e) => toast.error(errorMessage(e)), }) const taskMut = useMutation({ mutationFn: (p: { id: string; data: Partial }) => updateTask(p.id, p.data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['tasks'] }) toast.success('任务已更新') }, onError: (e) => toast.error(errorMessage(e)), }) const handleDragStart = (e: React.DragEvent, opp: Opportunity) => { e.dataTransfer.setData('oppId', opp.id) } const handleDrop = (e: React.DragEvent, stage: string) => { const id = e.dataTransfer.getData('oppId') if (id) oppMut.mutate({ id, stage: stage as any }) } const opps = oppData?.list ?? [] const tasks = taskData?.list ?? [] const groupedOpps = OPPORTUNITY_STAGES.map((s) => ({ ...s, items: opps.filter((o) => o.stage === s.value), })) // 简单月历 const today = new Date() const monthDays = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate() const firstDay = new Date(today.getFullYear(), today.getMonth(), 1).getDay() return (
} /> { setView(next) setSearchParams(next === 'kanban' ? {} : { tab: next }, { replace: true }) }} className="mb-5" items={[ { value: 'kanban', label: '商机看板', count: opps.length }, { value: 'tasks', label: '跟进任务', count: tasks.filter((t) => t.status !== 'done').length }, { value: 'calendar', label: '日历视图' }, ]} /> {view === 'kanban' && (
{groupedOpps.map((col) => (
e.preventDefault()} onDrop={(e) => handleDrop(e, col.value)} >
{col.label} {col.items.length}
{col.items.map((opp) => ( handleDragStart(e, opp)} className="cursor-grab p-3.5 active:cursor-grabbing hover:border-primary-200 hover:shadow-[0_8px_20px_-16px_rgba(37,99,235,.55)]" >

{opp.name}

{opp.customerName}

{formatMoney(opp.amount)} {opp.probability}%

{opp.nextAction}

))} {col.items.length === 0 &&

拖拽至此列

}
))}
)} {view === 'tasks' && (
{[ { v: 'all', l: '全部' }, { v: 'mine', l: '我的待办' }, { v: 'today', l: '今日' }, { v: 'overdue', l: '已逾期' }, { v: 'done', l: '已完成' }, ].map((f) => ( ))}
{tasks.length === 0 && } {tasks.map((t) => ( ))}
状态任务类型客户优先级截止时间负责人操作
taskMut.mutate({ id: t.id, data: { status: t.status === 'done' ? 'pending' : 'done' } })} /> {t.title} {taskTypeLabel(t.type)} {t.customerName} {formatDateTime(t.dueAt)} {memberNames.get(t.ownerId) ?? '未分配'} {t.status !== 'done' && ( )}
)} {view === 'calendar' && (
{today.getFullYear()} 年 {today.getMonth() + 1} 月
{['日', '一', '二', '三', '四', '五', '六'].map((d) =>
{d}
)} {Array.from({ length: firstDay }).map((_, i) =>
)} {Array.from({ length: monthDays }).map((_, i) => { const d = i + 1 const key = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}` const dayTasks = tasks.filter((t) => t.dueAt?.startsWith?.(key)) const isToday = d === today.getDate() && today.getMonth() === new Date().getMonth() return (
{d} {dayTasks.slice(0, 2).map((dt) => (
{dt.title}
))} {dayTasks.length > 2 &&
+{dayTasks.length - 2} 项
}
) })}
)}
) }