337 lines
17 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import { supabase } from '../../supabase/client';
import { motion } from 'framer-motion';
import { FileText, Plus, Warehouse, Factory, ArrowLeft, Users, TrendingUp, Package, Clock, Building2, CheckCircle2, DollarSign, Droplets, LogOut, HelpCircle } from 'lucide-react';
import { useResponsive } from '../../hooks/useResponsive';
import { OnboardingGuide, shouldShowOnboarding } from '../../components/OnboardingGuide';
import { NotificationCenter } from '../../components/NotificationCenter';
import { HelpCenter, HelpButton } from '../../components/HelpCenter';
import { VersionUpdate, useVersionCheck, AboutModal } from '../../components/VersionUpdate';
import { FeedbackModal } from '../../components/FeedbackModal';
import { UserManual } from '../../components/UserManual';
import type { ProductionPlan } from '../../types';
const statusMap: Record<string, { label: string; color: string; bgColor: string; icon: any }> = {
pending: { label: '待确定', color: 'text-amber-600', bgColor: 'bg-amber-50', icon: Clock },
producing: { label: '生产中', color: 'text-blue-600', bgColor: 'bg-blue-50', icon: TrendingUp },
completed: { label: '已完成', color: 'text-emerald-600', bgColor: 'bg-emerald-50', icon: Package }
};
const getQuickActions = (isMaster: boolean) => {
const actions = [
{ id: 'plans', icon: FileText, label: '计划总览', path: '/purchaser/plans', gradient: 'from-blue-500 to-cyan-500', description: '查看所有计划' },
{ id: 'new', icon: Plus, label: '新建纺织计划', path: '/purchaser/plans/new', gradient: 'from-emerald-500 to-green-500', description: '创建新纺织计划' },
{ id: 'washing', icon: Droplets, label: '新建水洗', path: '/purchaser/washing-plans/new', gradient: 'from-cyan-500 to-blue-500', description: '创建水洗计划' },
{ id: 'warehouse', icon: Warehouse, label: '原坯布仓库', path: '/purchaser/warehouse', gradient: 'from-amber-500 to-orange-500', description: '管理原坯布库存' },
{ id: 'finished', icon: Package, label: '水洗仓库', path: '/purchaser/finished-warehouse', gradient: 'from-emerald-500 to-teal-500', description: '管理水洗厂内坯布库存' },
{ id: 'factories', icon: Factory, label: '工厂管理', path: '/purchaser/factories', gradient: 'from-purple-500 to-violet-500', description: '管理合作工厂' },
{ id: 'accounts-payable', icon: DollarSign, label: '应付账款', path: '/purchaser/accounts-payable', gradient: 'from-rose-500 to-pink-500', description: '查看应付账款' }
];
if (isMaster) {
actions.push({ id: 'members', icon: Users, label: '账号管理', path: '/members', gradient: 'from-gray-500 to-slate-500', description: '管理团队成员' });
}
return actions;
};
const containerVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { staggerChildren: 0.1 } }
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.4 } }
};
export function PurchaserDashboard() {
const navigate = useNavigate();
const { auth, logout } = useAuth();
const { isMobile } = useResponsive();
const [plans, setPlans] = useState<ProductionPlan[]>([]);
const [loading, setLoading] = useState(true);
// 帮助中心状态
const [showHelp, setShowHelp] = useState(false);
const [showOnboarding, setShowOnboarding] = useState(false);
const [showUserManual, setShowUserManual] = useState(false);
const [showFeedback, setShowFeedback] = useState(false);
const [showAbout, setShowAbout] = useState(false);
// 版本更新检查
const { showUpdate, setShowUpdate } = useVersionCheck();
// 检查是否显示新手引导
useEffect(() => {
if (shouldShowOnboarding('purchaser')) {
setShowOnboarding(true);
}
}, []);
useEffect(() => {
if (!auth.company) return;
fetchPlans();
}, [auth.company]);
const fetchPlans = async () => {
const { data: planData } = await supabase
.from('production_plans')
.select('*')
.eq('purchaser_id', auth.company!.id)
.order('created_at', { ascending: false });
// 获取流程步骤,检测 rejected 状态
const planIds = (planData || []).map(p => p.id);
const { data: stepsData } = await supabase
.from('plan_process_steps')
.select('plan_id, status')
.in('plan_id', planIds);
// 过滤掉有 rejected 步骤的计划
const rejectedPlanIds = new Set<string>();
(stepsData || []).forEach(step => {
if (step.status === 'rejected') {
rejectedPlanIds.add(step.plan_id);
}
});
const filteredPlans = (planData || []).filter(p => !rejectedPlanIds.has(p.id));
setPlans(filteredPlans);
setLoading(false);
};
const stats = {
all: plans.length,
producing: plans.filter(p => p.status === 'producing').length,
completed: plans.filter(p => p.status === 'completed').length
};
const recentPlans = plans.slice(0, 5);
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/30">
{/* 新用户引导动画 - 已集成到下方弹窗 */}
{/* Header - 移动端优化 */}
<header className="bg-white/80 backdrop-blur-xl border-b border-gray-200/50 sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-3 sm:px-4 md:px-8 py-3 sm:py-4 flex justify-between items-center">
<div className="flex items-center gap-2 sm:gap-3">
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg shadow-blue-500/20">
<Building2 className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
</div>
<div>
<h1 className="text-sm sm:text-lg font-bold text-gray-900 flex items-center gap-2 whitespace-nowrap">
{auth.company?.name || '采购商'}
<span className="text-[10px] sm:text-xs bg-gradient-to-r from-amber-500 to-orange-500 text-white px-1.5 sm:px-2 py-0.5 rounded-full font-medium whitespace-nowrap"></span>
</h1>
<p className="text-xs text-gray-500 hidden sm:block">{(auth.user as any)?.display_name || auth.user?.username || '用户'}</p>
</div>
</div>
<div className="flex items-center gap-2">
<HelpButton onClick={() => setShowHelp(true)} />
<NotificationCenter />
<button
onClick={async () => { await logout(); navigate('/login'); }}
className="p-2 sm:px-3 sm:py-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
title="退出登录"
>
<LogOut className="w-5 h-5 sm:hidden" />
<span className="hidden sm:inline text-xs sm:text-sm">退</span>
</button>
</div>
</div>
</header>
<div className="p-3 sm:p-4 md:p-8 max-w-7xl mx-auto">
{loading ? (
<div className="flex items-center justify-center py-20">
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
</div>
) : (
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="space-y-4 sm:space-y-6">
{/* 统计卡片 - 移动端优化 */}
<div className="grid grid-cols-3 gap-2 sm:gap-4">
<motion.div
variants={itemVariants}
whileHover={isMobile ? {} : { scale: 1.02, y: -2 }}
onClick={() => navigate('/purchaser/plans')}
className="bg-white rounded-xl sm:rounded-2xl p-3 sm:p-5 shadow-sm border border-gray-100 cursor-pointer hover:border-blue-300 hover:shadow-md transition-all"
>
<div className="flex items-center justify-between mb-2 sm:mb-3">
<p className="text-xs sm:text-sm text-gray-500"></p>
<div className="w-6 h-6 sm:w-8 sm:h-8 bg-blue-50 rounded-lg flex items-center justify-center">
<FileText className="w-3 h-3 sm:w-4 sm:h-4 text-blue-600" />
</div>
</div>
<p className="text-xl sm:text-3xl font-bold text-gray-900">{stats.all}</p>
<p className="text-xs text-gray-400 mt-0.5 sm:mt-1"></p>
</motion.div>
<motion.div
variants={itemVariants}
whileHover={isMobile ? {} : { scale: 1.02, y: -2 }}
onClick={() => navigate('/purchaser/plans?filter=producing')}
className="bg-white rounded-xl sm:rounded-2xl p-3 sm:p-5 shadow-sm border border-gray-100 cursor-pointer hover:border-emerald-300 hover:shadow-md transition-all"
>
<div className="flex items-center justify-between mb-2 sm:mb-3">
<p className="text-xs sm:text-sm text-gray-500"></p>
<div className="w-6 h-6 sm:w-8 sm:h-8 bg-emerald-50 rounded-lg flex items-center justify-center">
<TrendingUp className="w-3 h-3 sm:w-4 sm:h-4 text-emerald-600" />
</div>
</div>
<p className="text-xl sm:text-3xl font-bold text-gray-900">{stats.producing}</p>
<p className="text-xs text-gray-400 mt-0.5 sm:mt-1"></p>
</motion.div>
<motion.div
variants={itemVariants}
whileHover={isMobile ? {} : { scale: 1.02, y: -2 }}
onClick={() => navigate('/purchaser/plans?filter=completed')}
className="bg-white rounded-xl sm:rounded-2xl p-3 sm:p-5 shadow-sm border border-gray-100 cursor-pointer hover:border-amber-300 hover:shadow-md transition-all"
>
<div className="flex items-center justify-between mb-2 sm:mb-3">
<p className="text-xs sm:text-sm text-gray-500"></p>
<div className="w-6 h-6 sm:w-8 sm:h-8 bg-amber-50 rounded-lg flex items-center justify-center">
<Package className="w-3 h-3 sm:w-4 sm:h-4 text-amber-600" />
</div>
</div>
<p className="text-xl sm:text-3xl font-bold text-gray-900">{stats.completed}</p>
<p className="text-xs text-gray-400 mt-0.5 sm:mt-1"></p>
</motion.div>
</div>
{/* 快捷操作 - 移动端优化 */}
<motion.div variants={itemVariants} className="bg-white rounded-xl sm:rounded-2xl shadow-sm border border-gray-100 p-3 sm:p-4 md:p-6">
<h2 className="text-sm sm:text-base font-semibold text-gray-900 mb-3 sm:mb-4 whitespace-nowrap"></h2>
<div className="grid grid-cols-3 sm:grid-cols-4 gap-2 sm:gap-3">
{getQuickActions(auth.user?.is_master || false).map((action, index) => (
<motion.button
key={action.id}
whileHover={isMobile ? {} : { scale: 1.03, y: -2 }}
whileTap={{ scale: 0.97 }}
onClick={() => navigate(action.path)}
className="group flex flex-col items-center p-2 sm:p-3 rounded-lg sm:rounded-xl border border-gray-100 hover:border-transparent hover:shadow-md transition-all bg-gray-50/50 hover:bg-white"
>
<div className={`w-8 h-8 sm:w-10 sm:h-10 rounded-lg sm:rounded-xl flex items-center justify-center mb-1 sm:mb-2 bg-gradient-to-br ${action.gradient} shadow-sm group-hover:shadow-md transition-shadow`}>
<action.icon className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
</div>
<span className="text-[10px] sm:text-xs font-medium text-gray-700 group-hover:text-gray-900 text-center whitespace-nowrap">{action.label}</span>
</motion.button>
))}
</div>
</motion.div>
{/* 最近计划 - 移动端优化 */}
<motion.div variants={itemVariants} className="bg-white rounded-xl sm:rounded-2xl shadow-sm border border-gray-100 p-3 sm:p-4 md:p-6">
<div className="flex justify-between items-center mb-3 sm:mb-4">
<h2 className="text-sm sm:text-base font-semibold text-gray-900 whitespace-nowrap"></h2>
<button
onClick={() => navigate('/purchaser/plans')}
className="text-xs sm:text-sm text-blue-600 hover:text-blue-700 font-medium flex items-center gap-1 whitespace-nowrap"
>
<ArrowLeft className="w-3 h-3 sm:w-4 sm:h-4 rotate-180" />
</button>
</div>
{recentPlans.length === 0 ? (
<div className="text-center py-8 sm:py-12 text-gray-400">
<Package className="w-10 h-10 sm:w-12 sm:h-12 mx-auto mb-3 text-gray-300" />
<p className="text-sm"></p>
<button
onClick={() => navigate('/purchaser/plans/new')}
className="mt-3 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 transition-colors"
>
</button>
</div>
) : (
<div className="space-y-2 sm:space-y-3">
{recentPlans.map((plan, i) => {
const progress = plan.target_quantity > 0
? Math.round((plan.completed_quantity / plan.target_quantity) * 100)
: 0;
const status = statusMap[plan.status] || statusMap.pending;
const StatusIcon = status.icon;
return (
<motion.div
key={plan.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.1 }}
onClick={() => navigate(`/purchaser/plans?id=${plan.id}`)}
className="group flex items-center gap-2 sm:gap-4 p-3 sm:p-4 rounded-xl border border-gray-100 hover:border-blue-200 hover:bg-blue-50/30 cursor-pointer transition-all"
>
<div className={`w-8 h-8 sm:w-10 sm:h-10 rounded-lg flex items-center justify-center ${status.bgColor}`}>
<StatusIcon className={`w-4 h-4 sm:w-5 sm:h-5 ${status.color}`} />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 sm:gap-2 mb-0.5 sm:mb-1">
<h3 className="font-medium text-gray-900 text-xs sm:text-sm truncate">{plan.product_name}-{plan.color}</h3>
<span className={`px-1.5 py-0.5 text-xs rounded-full whitespace-nowrap ${status.bgColor} ${status.color}`}>
{status.label}
</span>
</div>
<p className="text-xs text-gray-500">{plan.fabric_code}-{plan.color_code}</p>
</div>
<div className="text-right hidden sm:block">
<p className="text-sm font-medium text-gray-900">{plan.target_quantity}</p>
<p className="text-xs text-gray-400"></p>
</div>
<div className="w-16 sm:w-24">
<div className="flex items-center justify-between text-xs text-gray-500 mb-1">
<span className="hidden sm:inline"></span>
<span>{progress}%</span>
</div>
<div className="h-1 sm:h-1.5 bg-gray-100 rounded-full overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ duration: 0.8, delay: i * 0.1 }}
className="h-full bg-gradient-to-r from-blue-500 to-indigo-500 rounded-full"
/>
</div>
</div>
</motion.div>
);
})}
</div>
)}
</motion.div>
</motion.div>
)}
</div>
{/* 帮助中心弹窗 */}
<HelpCenter isOpen={showHelp} onClose={() => setShowHelp(false)} />
{/* 新手引导 */}
<OnboardingGuide
role="purchaser"
isOpen={showOnboarding}
onClose={() => setShowOnboarding(false)}
/>
{/* 用户手册 */}
<UserManual isOpen={showUserManual} onClose={() => setShowUserManual(false)} />
{/* 反馈弹窗 */}
<FeedbackModal isOpen={showFeedback} onClose={() => setShowFeedback(false)} />
{/* 关于窗口 */}
<AboutModal isOpen={showAbout} onClose={() => setShowAbout(false)} />
{/* 版本更新弹窗 */}
<VersionUpdate
isOpen={showUpdate}
onClose={() => setShowUpdate(false)}
/>
</div>
);
}