import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../../contexts/AuthContext'; import { supabase } from '../../supabase/client'; import { motion, AnimatePresence } from 'framer-motion'; import { ChevronDown, ChevronRight, ArrowLeft, Link2, Clipboard, Check, Loader2, X } from 'lucide-react'; import { ProcessFlow } from '../../components/ProcessFlow'; import { notifyStepCompleted, notifyPlanUpdated } from '../../utils/notifications'; import { getSupabaseUrl } from '../../supabase/client'; import type { ProductionPlan, PlanProcessStep, Company, Profile } from '../../types'; const statusMap: Record = { pending: { label: '待确定', color: 'bg-yellow-100 text-yellow-700' }, producing: { label: '生产中', color: 'bg-blue-100 text-blue-700' }, completed: { label: '已完成', color: 'bg-green-100 text-green-700' } }; interface PlanGroup { purchaserId: string; purchaserName: string; plans: (ProductionPlan & { processSteps: PlanProcessStep[]; inventoryRecords: { time: string; quantity: number; rolls: number | null }[] })[]; } export function TextilePlanOverview() { const navigate = useNavigate(); const { auth } = useAuth(); const [groups, setGroups] = useState([]); const [expandedGroups, setExpandedGroups] = useState>(new Set()); const [expandedPlan, setExpandedPlan] = useState(null); const [loading, setLoading] = useState(true); const [confirming, setConfirming] = useState(null); const [rejecting, setRejecting] = useState(null); const [showRejectModal, setShowRejectModal] = useState(false); const [rejectStepId, setRejectStepId] = useState(null); const [rejectReason, setRejectReason] = useState(''); const [importLink, setImportLink] = useState(''); const [operatorNames, setOperatorNames] = useState>({}); useEffect(() => { if (!auth.company) return; fetchPlans(); // 订阅入库记录变化,实现实时更新 const subscription = supabase .channel('plan_overview_inventory_changes') .on( 'postgres_changes', { event: '*', schema: 'public', table: 'inventory_records' }, () => { // 有入库记录变化时刷新数据 fetchPlans(); } ) .subscribe(); return () => { subscription.unsubscribe(); }; }, [auth.company]); const fetchPlans = async () => { // 获取关联到当前纺织厂的计划 const { data: planFactories } = await supabase .from('plan_factories') .select('plan_id') .eq('factory_id', auth.company!.id) .eq('factory_type', 'textile'); if (!planFactories || planFactories.length === 0) { setGroups([]); setLoading(false); return; } const planIds = planFactories.map(pf => pf.plan_id); // 获取计划详情 const { data: planData } = await supabase .from('production_plans') .select('*') .in('id', planIds) .order('created_at', { ascending: false }); if (!planData || planData.length === 0) { setGroups([]); setLoading(false); return; } // 获取所有流程步骤 const { data: stepsData } = await supabase .from('plan_process_steps') .select('*') .in('plan_id', planIds); // 获取操作人信息 const operatorIds = [...new Set((stepsData || []).filter(s => s.operator_id).map(s => s.operator_id).filter(Boolean))] as string[]; if (operatorIds.length > 0) { const { data: profilesData } = await supabase .from('profiles') .select('id, username') .in('id', operatorIds); const names: Record = {}; (profilesData || []).forEach(p => { names[p.id] = p.username; }); setOperatorNames(names); } // 获取采购商公司信息 const purchaserIds = [...new Set(planData.map(p => p.purchaser_id))]; const { data: companiesData } = await supabase .from('companies') .select('*') .in('id', purchaserIds); const companyMap: Record = {}; (companiesData || []).forEach(c => { companyMap[c.id] = c.name; }); // 获取所有计划的入库记录 const { data: inventoryData } = await supabase .from('inventory_records') .select('plan_id, quantity, rolls, created_at') .in('plan_id', planIds) .order('created_at', { ascending: false }); // 按采购商分组 const grouped: Record = {}; planData.forEach(plan => { const pid = plan.purchaser_id; if (!grouped[pid]) { grouped[pid] = { purchaserId: pid, purchaserName: companyMap[pid] || '未知布行', plans: [] }; } const processSteps = (stepsData || []) .filter(s => s.plan_id === plan.id) .sort((a, b) => { const order = ['confirm', 'yarn_purchase', 'dyeing', 'machine_start', 'fabric_warehouse']; return order.indexOf(a.step_type) - order.indexOf(b.step_type); }); const inventoryRecords = (inventoryData || []) .filter(r => r.plan_id === plan.id) .map(r => ({ time: r.created_at, quantity: r.quantity, rolls: r.rolls })); grouped[pid].plans.push({ ...plan, processSteps, inventoryRecords }); }); const groupList = Object.values(grouped); setGroups(groupList); // 默认展开第一个组 if (groupList.length > 0) { setExpandedGroups(new Set([groupList[0].purchaserId])); } setLoading(false); }; const toggleGroup = (id: string) => { setExpandedGroups(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; // 从 hash 路由链接中提取查询参数 const extractParamsFromLink = (link: string) => { const hashMatch = link.match(/#\/import\?(.+)$/); if (hashMatch) { const queryString = hashMatch[1]; const params = new URLSearchParams(queryString); return { token: params.get('token'), plan: params.get('plan'), type: params.get('type') }; } try { const url = new URL(link, window.location.origin); return { token: url.searchParams.get('token'), plan: url.searchParams.get('plan'), type: url.searchParams.get('type') }; } catch { return { token: null, plan: null, type: null }; } }; const handleImportFromLink = () => { if (!importLink.trim()) return; const { token, plan, type } = extractParamsFromLink(importLink); if (token) { navigate(`/import?token=${token}`); } else if (plan && type) { navigate(`/import?plan=${plan}&type=${type}`); } else { alert('无效的链接格式,请确保链接包含 token 或 plan 参数'); } }; const handlePasteFromClipboard = async () => { try { const text = await navigator.clipboard.readText(); if (text.includes('/import?') || text.includes('#/import?')) { setImportLink(text); const { token, plan, type } = extractParamsFromLink(text); if (token) { navigate(`/import?token=${token}`); } else if (plan && type) { navigate(`/import?plan=${plan}&type=${type}`); } else { alert('链接格式不正确,请检查链接是否完整'); } } else { alert('剪贴板中没有检测到有效的导入链接'); } } catch (err) { console.error('读取剪贴板失败:', err); alert('无法读取剪贴板,请手动粘贴链接'); } }; // 打开拒绝弹窗 const handleOpenRejectModal = (stepId: string) => { setRejectStepId(stepId); setRejectReason(''); setShowRejectModal(true); }; // 关闭拒绝弹窗 const handleCloseRejectModal = () => { setShowRejectModal(false); setRejectStepId(null); setRejectReason(''); }; // 处理拒绝计划 const handleRejectStep = async () => { if (!rejectStepId || !rejectReason.trim()) { alert('请输入拒绝原因'); return; } setRejecting(rejectStepId); const now = new Date().toISOString(); try { // 获取步骤信息 const { data: step } = await supabase .from('plan_process_steps') .select('plan_id') .eq('id', rejectStepId) .maybeSingle(); if (!step?.plan_id) { setRejecting(null); handleCloseRejectModal(); return; } // 更新步骤状态为拒绝(使用 notes 字段记录拒绝原因) const { error } = await supabase .from('plan_process_steps') .update({ status: 'pending' as const, notes: `拒绝原因: ${rejectReason.trim()}`, timestamp: now, operator_id: auth.user?.id }) .eq('id', rejectStepId); if (error) { console.error('拒绝失败:', error); alert('拒绝失败,请重试'); setRejecting(null); return; } // 发送拒绝通知给采购商 await notifyPlanUpdated(step.plan_id, 'rejected', auth.company!.id, auth.user!.id); // 刷新数据 await fetchPlans(); handleCloseRejectModal(); } catch (err) { console.error('拒绝失败:', err); alert('拒绝失败,请重试'); } setRejecting(null); }; const handleConfirmStep = async (stepId: string, quantity?: number, rolls?: number) => { setConfirming(stepId); const now = new Date().toISOString(); // 获取步骤信息 const { data: step } = await supabase .from('plan_process_steps') .select('step_type, plan_id') .eq('id', stepId) .maybeSingle(); const planId = step?.plan_id; if (!planId) { setConfirming(null); return; } // 如果是坯布入库,使用 Edge Function 进行事务处理 if (step?.step_type === 'fabric_warehouse' && quantity && quantity > 0) { // 获取纺织厂的仓库和工厂信息 const { data: warehouse } = await supabase .from('warehouses') .select('id') .eq('company_id', auth.company!.id) .eq('type', 'fabric') .maybeSingle(); // 获取工厂信息(地址或名称) const { data: factoryInfo } = await supabase .from('companies') .select('name, address') .eq('id', auth.company!.id) .maybeSingle(); // 仓库位置:优先使用地址,没有则使用工厂名称 const warehouseLocation = factoryInfo?.address || factoryInfo?.name || ''; // 获取计划信息(包括采购商ID和采购价) const { data: plan } = await supabase .from('production_plans') .select('completed_quantity, target_quantity, purchaser_id, production_price') .eq('id', planId) .maybeSingle(); if (plan) { const newCompletedQuantity = plan.completed_quantity + quantity; const progressPercent = (newCompletedQuantity / plan.target_quantity) * 100; const pricePerMeter = plan.production_price || 0; const amount = quantity * pricePerMeter; // 使用 Edge Function 进行入库事务处理 const session = (await supabase.auth.getSession()).data.session; const headers: Record = { 'Content-Type': 'application/json' }; if (session) { headers['Authorization'] = `Bearer ${session.access_token}`; } const response = await fetch(`${getSupabaseUrl()}/functions/v1/fabric-warehouse-inbound`, { method: 'POST', headers, body: JSON.stringify({ planId: planId, quantity: quantity, rolls: rolls || 0, warehouseId: warehouse?.id || null, warehouseLocation: warehouseLocation, operatorId: auth.user?.id, pricePerMeter: pricePerMeter, }), }); const result = await response.json(); if (!response.ok) { throw new Error(result.error || '入库操作失败'); } const inventoryRecord = result.data?.inventoryRecord; // 创建或更新应付账款记录 if (inventoryRecord) { // 检查是否已存在应付账款记录 const { data: existingAP } = await supabase .from('accounts_payable') .select('*') .eq('plan_id', planId) .eq('textile_factory_id', auth.company!.id) .maybeSingle(); if (existingAP) { // 更新应付账款 await supabase .from('accounts_payable') .update({ total_quantity: existingAP.total_quantity + quantity, total_amount: existingAP.total_amount + amount, unpaid_quantity: existingAP.unpaid_quantity + quantity, unpaid_amount: existingAP.unpaid_amount + amount, updated_at: now }) .eq('id', existingAP.id); // 创建应付账款明细 await supabase .from('accounts_payable_items') .insert({ accounts_payable_id: existingAP.id, inventory_record_id: inventoryRecord.id, quantity: quantity, rolls: rolls || 0, price_per_meter: pricePerMeter, amount: amount, status: 'unpaid', created_at: now }); } else { // 创建新的应付账款 const { data: newAP } = await supabase .from('accounts_payable') .insert({ plan_id: planId, purchaser_id: plan.purchaser_id, textile_factory_id: auth.company!.id, total_quantity: quantity, total_amount: amount, unpaid_quantity: quantity, unpaid_amount: amount, price_per_meter: pricePerMeter, status: 'unpaid', created_at: now, updated_at: now }) .select() .single(); if (newAP) { // 创建应付账款明细 await supabase .from('accounts_payable_items') .insert({ accounts_payable_id: newAP.id, inventory_record_id: inventoryRecord.id, quantity: quantity, rolls: rolls || 0, price_per_meter: pricePerMeter, amount: amount, status: 'unpaid', created_at: now }); } } } // 如果进度达到97%或以上,标记步骤为完成,否则标记为进行中 if (progressPercent >= 97) { await supabase .from('plan_process_steps') .update({ status: 'completed' as const, timestamp: now, operator_id: auth.user?.id }) .eq('id', stepId); } else { // 进度未达到97%,标记步骤为进行中(active) await supabase .from('plan_process_steps') .update({ status: 'active' as const, timestamp: now, operator_id: auth.user?.id }) .eq('id', stepId); } } } else { // 其他步骤直接更新状态 const { error } = await supabase .from('plan_process_steps') .update({ status: 'completed' as const, timestamp: now, operator_id: auth.user?.id }) .eq('id', stepId); if (error) { console.error('确认失败:', error); alert('确认失败,请重试'); setConfirming(null); return; } } // 更新计划状态 if (step?.step_type === 'confirm') { await supabase .from('production_plans') .update({ status: 'producing' as const }) .eq('id', planId); // 发送计划开始生产通知 await notifyPlanUpdated(planId, 'producing', auth.company!.id, auth.user!.id); } // 检查是否所有步骤都已完成 const { data: allSteps } = await supabase .from('plan_process_steps') .select('status') .eq('plan_id', planId); const allCompleted = allSteps?.every(s => s.status === 'completed'); if (allCompleted) { await supabase .from('production_plans') .update({ status: 'completed' as const }) .eq('id', planId); // 发送计划完成通知 await notifyPlanUpdated(planId, 'completed', auth.company!.id, auth.user!.id); } // 发送节点完成通知 const stepNames: Record = { confirm: '确认计划', yarn_purchase: '采纱', dyeing: '染纱', machine_start: '上机', fabric_warehouse: '坯布生产' }; const stepName = stepNames[step?.step_type || ''] || '生产节点'; await notifyStepCompleted( planId, stepName, auth.company!.id, auth.user!.id, step?.step_type === 'fabric_warehouse' ? { quantity, rolls } : undefined ); // 刷新数据 await fetchPlans(); setConfirming(null); }; // 将步骤转换为 ProcessFlow 需要的格式 const formatProcessNodes = (steps: PlanProcessStep[]) => { return steps.map(step => ({ id: step.id, name: step.step_type, status: step.status, timestamp: step.timestamp, stepType: step.step_type, operatorName: step.operator_id ? operatorNames[step.operator_id] : undefined })); }; return (
navigate('/textile')} className="p-2 hover:bg-gray-200 rounded-full transition-colors will-change-transform" >

计划总览

{/* 导入链接区域 */}

导入关联计划

识别剪贴板
setImportLink(e.target.value)} placeholder="粘贴采购商分享的链接..." className="flex-1 px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-xs sm:text-sm" /> 导入
{loading ? (

加载中...

) : groups.length === 0 ? (

暂无关联计划

请使用上方输入框粘贴采购商分享的链接

) : (
{groups.map((group, groupIndex) => ( toggleGroup(group.purchaserId)} whileTap={{ scale: 0.995 }} className="flex items-center justify-between p-3 sm:p-4 cursor-pointer hover:bg-gray-50 transition-colors will-change-transform" >

{group.purchaserName}

计划 ({group.plans.length})
{expandedGroups.has(group.purchaserId) && (
{group.plans.map((plan, planIndex) => { const progress = plan.target_quantity > 0 ? Math.round((plan.completed_quantity / plan.target_quantity) * 100) : 0; return ( setExpandedPlan(expandedPlan === plan.id ? null : plan.id)} whileTap={{ scale: 0.995 }} className="p-3 sm:p-4 cursor-pointer hover:bg-gray-50 transition-colors will-change-transform" >
{statusMap[plan.status]?.label || plan.status}

{plan.fabric_code}

{plan.plan_code}

计划: {plan.target_quantity}米

已入库: {plan.completed_quantity}米

进度 {progress}%
{expandedPlan === plan.id && plan.processSteps.length > 0 && (
生产记录
handleConfirmStep(stepId, quantity, rolls)} onReject={handleOpenRejectModal} confirmLoading={confirming} showConfirmButton={true} planTargetQuantity={plan.target_quantity} planCompletedQuantity={plan.completed_quantity} isPlanLocked={plan.status === 'completed'} lastInventoryTime={plan.inventoryRecords[0]?.time} inventoryRecords={plan.inventoryRecords} />
)}
); })}
)}
))}
)}
{/* 拒绝计划弹窗 */} {showRejectModal && (

拒绝计划

请填写拒绝原因,采购商将收到通知并可以重新调整计划。