import React from "react"; import{motion}from "motion/react"; import{Menu,Search,Wallet,CheckCircle2,Clock,ShieldCheck,PlusCircle,Edit,Download,MoreHorizontal,BadgeCheck,ChevronRight,AlertCircle,Plus,ArrowUpRight,ArrowDownRight}from "lucide-react"; import{cn}from "./lib/utils"; import{Builder,Todo}from "./types"; import{BarChart,Bar,XAxis,YAxis,CartesianGrid,Tooltip,ResponsiveContainer,Cell,LabelList,PieChart,Pie}from "recharts"; export default function Dashboard({onNavigate,onSearchNavigate,builders = [],processedIds = []}: {onNavigate: (view: string) => void,onSearchNavigate: (query: string,tab?: string) => void,builders?: Builder[],processedIds?: string[]}){// Generate todos from builders (same logic as TodoCenter) const allTodos: (Todo & {builderName: string})[] = [];builders.forEach(builder => {if (builder.mainCertExpiry && builder.mainCertExpiry !== "-") {allTodos.push({id: `main-${builder.id}`,title: `${builder.name} - 主证到期`,builderName: builder.name,description: `${builder.certificateType} 有效期至 ${builder.mainCertExpiry}`,time: builder.mainCertExpiry,type: processedIds.includes(`main-${builder.id}`) ? "已完成" : "异常提醒",date: builder.mainCertExpiry});} if (builder.bCertExpiry && builder.bCertExpiry !== "-") {allTodos.push({id: `b-${builder.id}`,title: `${builder.name} - B证到期`,builderName: builder.name,description: `B证有效期至 ${builder.bCertExpiry}`,time: builder.bCertExpiry,type: processedIds.includes(`b-${builder.id}`) ? "已完成" : "异常提醒",date: builder.bCertExpiry});}});// Filter for Unprocessed items const unprocessedTodos = allTodos.filter(todo => !processedIds.includes(todo.id));// Sort by date (closest first) const sortedUnprocessed = unprocessedTodos.sort((a,b) => {return new Date(a.date).getTime() - new Date(b.date).getTime();});// Take top 5 for recent activity const recentActivities = sortedUnprocessed.slice(0,5);// Calculate Monthly Stats const now = new Date("2026-03-21");// Using current context date const currentMonth = now.getMonth();const currentYear = now.getFullYear();const currentMonthProfit = builders .filter(b => {const d = new Date(b.performanceDate); return d.getMonth() === currentMonth && d.getFullYear() === currentYear;}) .reduce((acc,b) => acc + b.performanceProfit,0);// Generate last 6 months trend const monthlyTrend = Array.from({length: 6}).map((_,i) => {const d = new Date(currentYear,currentMonth - (5 - i),1); const m = d.getMonth(); const y = d.getFullYear(); const monthLabel = `${m + 1}月`; const profit = builders .filter(b => {const bd = new Date(b.performanceDate); return bd.getMonth() === m && bd.getFullYear() === y;}) .reduce((acc,b) => acc + b.performanceProfit,0); return {month: monthLabel,profit,monthIdx: m,yearIdx: y};});// Calculate growth rates const trendWithGrowth = monthlyTrend.map((item,i) => {if (i === 0) return {...item,growth: 0}; const prevProfit = monthlyTrend[i - 1].profit; const growth = prevProfit === 0 ? (item.profit > 0 ? 100 : 0) : ((item.profit - prevProfit) / prevProfit) * 100; return {...item,growth};});const lastMonthGrowth = trendWithGrowth[5].growth;// Calculate profit by major for current month const currentMonthBuilders = builders.filter(b => {const d = new Date(b.performanceDate); return d.getMonth() === currentMonth && d.getFullYear() === currentYear;});const profitByCertType = currentMonthBuilders.reduce((acc,b) => {const certType = b.certificateType || "其他"; acc[certType] = (acc[certType] || 0) + b.performanceProfit; return acc;},{} as Record<string,number>);const pieData = Object.entries(profitByCertType) .map(([name,value]) => ({name,value})) .sort((a,b) => b.value - a.value);const PIE_COLORS = ["#FFFFFF","rgba(255,255,255,0.7)","rgba(255,255,255,0.4)","rgba(255,255,255,0.2)"];const receivedProfit = builders.filter(b => b.isPaid).reduce((acc,b) => acc + b.performanceProfit,0);const pendingProfit = builders.filter(b => !b.isPaid).reduce((acc,b) => acc + b.performanceProfit,0);const totalProfit = receivedProfit + pendingProfit;const mainCertExpiryCount = unprocessedTodos.filter(t => t.id.startsWith("main-")).length;return (<div className="pb-32"> {} <header className="sticky top-0 w-full z-50 bg-surface/80 backdrop-blur-md"> <div className="flex items-center justify-between px-6 h-16 w-full max-w-md mx-auto"> <div className="flex items-center gap-4"> <Menu className="text-primary cursor-pointer active:scale-95 transition-transform" size={24} /> <h1 className="font-headline font-extrabold text-xl tracking-tighter text-primary">建程通</h1> </div> <div className="flex items-center gap-2"> <div className="hover:bg-primary/10 p-2 rounded-full transition-colors cursor-pointer active:scale-95"> <Search className="text-on-surface-variant" size={24} /> </div> </div> </div> </header> <main className="max-w-md mx-auto px-6 pt-4 space-y-6"> {} <section className="space-y-1 px-1"> <h2 className="text-on-surface-variant font-headline text-[11px] font-bold uppercase tracking-[0.2em] opacity-70">PROJECT PULSE</h2> <p className="font-headline text-2xl font-extrabold text-on-surface tracking-tight">下午好，项目主管</p> </section> {} <section className="grid grid-cols-2 gap-4"> {} <motion.div initial={{opacity: 0,y: 20}} animate={{opacity: 1,y: 0}} className="col-span-2 primary-gradient rounded-3xl p-6 text-white shadow-xl shadow-primary/20 relative overflow-hidden group" > <div className="absolute -right-6 -top-6 opacity-10 group-hover:scale-110 transition-transform duration-700"> <Wallet size={160} /> </div> <div className="relative z-10 flex justify-between items-start"> <div className="flex-1"> <p className="text-white/80 font-headline text-[11px] font-bold uppercase tracking-widest">本月业绩总利润</p> <div className="flex items-baseline gap-2 mt-2"> <span className="font-headline text-3xl font-black tracking-tight">￥{currentMonthProfit.toLocaleString()}</span> <span className={cn("text-white text-[10px] font-bold px-2 py-0.5 rounded-full backdrop-blur-sm flex items-center gap-0.5",lastMonthGrowth >= 0 ? "bg-emerald-500/30" : "bg-red-500/30")}> {lastMonthGrowth >= 0 ? <ArrowUpRight size={10} /> : <ArrowDownRight size={10} />} {Math.abs(lastMonthGrowth).toFixed(1)}% MoM </span> </div> <div className="mt-6 space-y-2"> {pieData.slice(0,3).map((item,idx) => (<div key={item.name} className="flex items-center gap-2"> <div className="w-1.5 h-1.5 rounded-full" style={{backgroundColor: PIE_COLORS[idx]}}></div> <span className="text-[10px] font-bold text-white/70 truncate max-w-[100px]">{item.name}</span> <span className="text-[10px] font-black text-white"> {((item.value / currentMonthProfit) * 100).toFixed(0)}% </span> </div>))} </div> </div> <div className="w-32 h-32 -mr-2 -mt-2"> <ResponsiveContainer width="100%" height="100%"> <PieChart> <Pie data={pieData} cx="50%" cy="50%" innerRadius={35} outerRadius={50} paddingAngle={5} dataKey="value" stroke="none" > {pieData.map((entry,index) => (<Cell key={`cell-${index}`} fill={PIE_COLORS[index % PIE_COLORS.length]} />))} </Pie> </PieChart> </ResponsiveContainer> </div> </div> <div className="relative z-10 mt-4"> <div className="flex justify-between items-end mb-2"> <span className="text-white/60 font-bold text-[10px] tracking-wider uppercase">Current Progress</span> <span className="text-white/80 font-bold text-xs">84%</span> </div> <div className="w-full h-2 bg-white/20 rounded-full overflow-hidden backdrop-blur-sm"> <motion.div initial={{width: 0}} animate={{width: "84%"}} transition={{duration: 1,ease: "easeOut"}} className="bg-white h-full rounded-full" /> </div> </div> </motion.div> {} <motion.div initial={{opacity: 0,x: -20}} animate={{opacity: 1,x: 0}} transition={{delay: .1}} onClick={() => onSearchNavigate("","已收业绩")} className="bg-surface-container-lowest rounded-2xl p-5 flex flex-col justify-between shadow-sm border border-black/[0.02] cursor-pointer active:scale-95 transition-transform" > <div> <div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center mb-3"> <CheckCircle2 className="text-primary" size={20} fill="currentColor" fillOpacity={.2} /> </div> <p className="text-on-surface-variant text-[10px] font-bold uppercase tracking-wider">已收业绩利润</p> </div> <p className="font-headline text-xl font-black text-on-surface mt-2">￥{receivedProfit.toLocaleString()}</p> </motion.div> <motion.div initial={{opacity: 0,x: 20}} animate={{opacity: 1,x: 0}} transition={{delay: .2}} onClick={() => onSearchNavigate("","待收业绩")} className="bg-surface-container-lowest rounded-2xl p-5 flex flex-col justify-between shadow-sm border border-black/[0.02] cursor-pointer active:scale-95 transition-transform" > <div> <div className="w-8 h-8 rounded-lg bg-tertiary/10 flex items-center justify-center mb-3"> <Clock className="text-tertiary" size={20} /> </div> <p className="text-on-surface-variant text-[10px] font-bold uppercase tracking-wider">待收业绩利润</p> </div> <p className="font-headline text-xl font-black text-on-surface mt-2">￥{pendingProfit.toLocaleString()}</p> </motion.div> {} <motion.div initial={{opacity: 0,scale: .95}} animate={{opacity: 1,scale: 1}} transition={{delay: .3}} className="col-span-2 bg-error-container/50 border border-error/10 rounded-2xl p-4 flex items-center justify-between" > <div className="flex items-center gap-4"> <div className="bg-error text-white w-12 h-12 rounded-xl flex items-center justify-center shadow-lg shadow-error/20"> <ShieldCheck size={24} fill="currentColor" fillOpacity={.2} /> </div> <div> <p className="text-on-error-container font-extrabold text-sm">主证到期提醒</p> <p className="text-on-error-container/70 text-[11px] mt-0.5">需立即更新资质文档</p> </div> </div> <div className="text-error font-headline text-3xl font-black pr-2">{mainCertExpiryCount}</div> </motion.div> </section> {} <section className="grid grid-cols-3 gap-3"> <button onClick={() => onNavigate("new-builder")} className="bg-surface-container-lowest py-4 rounded-2xl flex flex-col items-center gap-2 active:scale-95 transition-all shadow-sm border border-black/[0.02]" > <div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center"> <PlusCircle className="text-primary" size={20} fill="currentColor" fillOpacity={.2} /> </div> <span className="text-[11px] font-bold text-on-surface">快速添加</span> </button> <button className="bg-surface-container-lowest py-4 rounded-2xl flex flex-col items-center gap-2 active:scale-95 transition-all shadow-sm border border-black/[0.02]"> <div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center"> <Edit className="text-primary" size={20} /> </div> <span className="text-[11px] font-bold text-on-surface">业绩录入</span> </button> <button className="bg-surface-container-lowest py-4 rounded-2xl flex flex-col items-center gap-2 active:scale-95 transition-all shadow-sm border border-black/[0.02]"> <div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center"> <Download className="text-primary" size={20} /> </div> <span className="text-[11px] font-bold text-on-surface">导出备份</span> </button> </section> {} <section className="bg-surface-container-lowest rounded-2xl p-6 shadow-sm border border-black/[0.02] space-y-6"> <div className="flex justify-between items-center"> <h3 className="font-headline font-extrabold text-on-surface tracking-tight">近6个月业绩趋势</h3> <MoreHorizontal className="text-on-surface-variant cursor-pointer" size={20} /> </div> <div className="h-64 w-full"> <ResponsiveContainer width="100%" height="100%"> <BarChart data={trendWithGrowth} margin={{top: 20,right: 0,left: -20,bottom: 0}} > <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f0f0f0" /> <XAxis dataKey="month" axisLine={false} tickLine={false} tick={{fontSize: 10,fontWeight: 700,fill: "#64748b"}} dy={10} /> <YAxis hide /> <Tooltip cursor={{fill: "#f8fafc"}} content={({active,payload}) => {if (active && payload && payload.length) {const data = payload[0].payload; return (<div className="bg-white p-3 rounded-xl shadow-xl border border-slate-100"> <p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">{data.month} 利润</p> <p className="text-sm font-black text-slate-900">￥{data.profit.toLocaleString()}</p> <div className={cn("flex items-center gap-1 text-[10px] font-bold mt-1",data.growth >= 0 ? "text-emerald-600" : "text-red-600")}> {data.growth >= 0 ? <ArrowUpRight size={12} /> : <ArrowDownRight size={12} />} {Math.abs(data.growth).toFixed(1)}% </div> </div>);} return null;}} /> <Bar dataKey="profit" radius={[6,6,0,0]} barSize={32} > {trendWithGrowth.map((entry,index) => (<Cell key={`cell-${index}`} fill={index === 5 ? "#2346D5" : "#e2e8f0"} className="transition-all duration-300" />))} <LabelList dataKey="profit" position="top" content={(props: any) => {const {x,y,width,value,index} = props; const growth = trendWithGrowth[index].growth; return (<g> <text x={x + width / 2} y={y - 10} fill="#64748b" textAnchor="middle" fontSize={9} fontWeight={700} > {value > 0 ? `￥${(value / 10000).toFixed(1)}w` : ""} </text> {index > 0 && value > 0 && (<text x={x + width / 2} y={y - 22} fill={growth >= 0 ? "#059669" : "#dc2626"} textAnchor="middle" fontSize={8} fontWeight={800} > {growth >= 0 ? "↑" : "↓"}{Math.abs(growth).toFixed(0)}% </text>)} </g>);}} /> </Bar> </BarChart> </ResponsiveContainer> </div> </section> {} <section className="space-y-4"> <div className="flex justify-between items-end px-1"> <h3 className="font-headline font-extrabold text-on-surface tracking-tight">最近活动</h3> <span onClick={() => onNavigate("todos")} className="text-primary text-[11px] font-bold uppercase tracking-widest cursor-pointer" > 查看全部 </span> </div> <div className="space-y-3"> {recentActivities.map((activity) => {const isExpiry = activity.id.startsWith("main-") || activity.id.startsWith("b-"); const Icon = isExpiry ? BadgeCheck : Clock; const color = isExpiry ? "text-error" : "text-primary"; const bgColor = isExpiry ? "bg-error/10" : "bg-primary/10"; return (<div key={activity.id} onClick={() => onSearchNavigate(activity.builderName)} className="bg-surface-container-lowest p-4 rounded-2xl flex items-center gap-4 shadow-sm border border-black/[0.01] active:bg-surface-container-low transition-colors cursor-pointer group" > <div className={cn("w-12 h-12 rounded-xl flex items-center justify-center",bgColor,color)}> <Icon size={24} /> </div> <div className="flex-1"> <p className="text-sm font-bold text-on-surface group-hover:text-primary transition-colors">{activity.title}</p> <p className="text-[11px] text-on-surface-variant font-medium mt-0.5">{activity.time} • {isExpiry ? "系统提醒" : "待办任务"}</p> </div> <ChevronRight className="text-outline group-hover:text-primary transition-colors" size={20} /> </div>);})} {recentActivities.length === 0 && (<div className="py-10 text-center bg-surface-container-low/30 rounded-2xl border-2 border-dashed border-outline-variant/10"> <p className="text-xs text-on-surface-variant font-medium">暂无最近活动</p> </div>)} </div> </section> </main> {} <button onClick={() => onNavigate("new-builder")} className="fixed right-6 bottom-32 w-16 h-16 primary-gradient text-white rounded-2xl flex items-center justify-center shadow-2xl shadow-primary/40 active:scale-90 transition-all z-40" > <Plus size={32} /> </button> </div>)}
