516 lines
20 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import { supabase } from '../../supabase/client';
import { ArrowLeft, Plus, Trash2, Search, X, ChevronDown } from 'lucide-react';
import type { Company } from '../../types';
interface YarnInput {
id: string;
name: string;
ratio: number;
}
// 产品类型定义(来自原坯布仓库)
interface Product {
id: string;
product_name: string;
weight: number;
color: string;
fabric_code: string;
color_code: string;
production_price: number;
}
// 纱线配比类型
interface ProductYarnRatio {
id: string;
product_id: string;
yarn_name: string;
ratio: number;
}
export function NewPlan() {
const navigate = useNavigate();
const { auth } = useAuth();
// 从原坯布仓库选择的产品
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
const [products, setProducts] = useState<Product[]>([]);
const [productRatios, setProductRatios] = useState<Record<string, ProductYarnRatio[]>>({});
// 搜索和筛选状态
const [searchQuery, setSearchQuery] = useState('');
const [showProductDropdown, setShowProductDropdown] = useState(false);
// 表单字段
const [productName, setProductName] = useState('');
const [color, setColor] = useState('');
const [fabricCode, setFabricCode] = useState('');
const [colorCode, setColorCode] = useState('01');
const [remark, setRemark] = useState('');
const [textileFactory, setTextileFactory] = useState('');
const [targetQuantity, setTargetQuantity] = useState('');
const [productionPrice, setProductionPrice] = useState('');
const [yarns, setYarns] = useState<YarnInput[]>([{ id: '1', name: '', ratio: 1 }]);
const [factories, setFactories] = useState<Company[]>([]);
const [submitting, setSubmitting] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchFactories();
fetchProducts();
}, []);
// 从原坯布仓库获取产品数据
const fetchProducts = async () => {
if (!auth.company) return;
setLoading(true);
const { data: prodData } = await supabase
.from('products')
.select('*')
.eq('company_id', auth.company.id)
.order('created_at', { ascending: false });
if (prodData) {
setProducts(prodData as Product[]);
// 获取每个产品的纱线配比
const productIds = prodData.map(p => p.id);
if (productIds.length > 0) {
const { data: ratiosData } = await supabase
.from('product_yarn_ratios')
.select('*')
.in('product_id', productIds);
if (ratiosData) {
const ratiosMap: Record<string, ProductYarnRatio[]> = {};
ratiosData.forEach((r: any) => {
if (!ratiosMap[r.product_id]) ratiosMap[r.product_id] = [];
ratiosMap[r.product_id].push(r as ProductYarnRatio);
});
setProductRatios(ratiosMap);
}
}
}
setLoading(false);
};
const fetchFactories = async () => {
const { data } = await supabase
.from('companies')
.select('*')
.eq('role', 'textile');
setFactories(data || []);
};
// 根据搜索词筛选产品
const filteredProducts = useMemo(() => {
if (!searchQuery.trim()) return [];
const query = searchQuery.toLowerCase();
return products.filter(product =>
product.product_name?.toLowerCase().includes(query) ||
product.fabric_code?.toLowerCase().includes(query) ||
product.color?.toLowerCase().includes(query) ||
product.color_code?.toLowerCase().includes(query)
).slice(0, 10); // 最多显示10条
}, [searchQuery, products]);
// 选择产品后自动填充表单
const handleSelectProduct = (product: Product) => {
setSelectedProduct(product);
setProductName(product.product_name);
setColor(product.color);
setFabricCode(product.fabric_code);
setColorCode(product.color_code);
setProductionPrice(String(product.production_price || ''));
setSearchQuery(`${product.product_name}-${product.weight}g-${product.color}`);
setShowProductDropdown(false);
// 自动填充纱线配比
const ratios = productRatios[product.id];
if (ratios && ratios.length > 0) {
setYarns(ratios.map((r, i) => ({
id: i.toString(),
name: r.yarn_name,
ratio: r.ratio
})));
} else {
setYarns([{ id: '1', name: '', ratio: 1 }]);
}
};
// 清除选择的产品
const handleClearProduct = () => {
setSelectedProduct(null);
setProductName('');
setColor('');
setFabricCode('');
setColorCode('01');
setProductionPrice('');
setSearchQuery('');
setYarns([{ id: '1', name: '', ratio: 1 }]);
};
const addYarn = () => {
setYarns([...yarns, { id: Date.now().toString(), name: '', ratio: 1 }]);
};
const removeYarn = (id: string) => {
if (yarns.length > 1) setYarns(yarns.filter(y => y.id !== id));
};
const updateYarn = (id: string, field: 'name' | 'ratio', value: string | number) => {
setYarns(yarns.map(y => y.id === id ? { ...y, [field]: value } : y));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!auth.company) return;
setSubmitting(true);
try {
// 生成计划编号
const timestamp = Date.now().toString().slice(-6);
const planCode = `PLAN-2026-${timestamp}`;
// 创建计划
const { data: plan, error: planError } = await supabase
.from('production_plans')
.insert({
plan_code: planCode,
product_name: productName,
color: color,
fabric_code: fabricCode.toUpperCase(),
color_code: colorCode,
remark: remark,
purchaser_id: auth.company.id,
target_quantity: parseInt(targetQuantity) || 0,
yarn_usage_per_meter: selectedProduct?.weight || 0, // 使用产品克重作为纱用量
production_price: parseFloat(productionPrice) || 0,
status: 'pending',
created_by: auth.user?.id
})
.select()
.maybeSingle();
if (planError) {
alert('创建计划失败: ' + planError.message);
setSubmitting(false);
return;
}
if (plan && textileFactory) {
// 关联纺织厂
await supabase.from('plan_factories').insert({
plan_id: plan.id,
factory_id: textileFactory,
factory_type: 'textile'
});
// 创建纱线配比
for (const yarn of yarns) {
if (yarn.name) {
const amountPerMeter = selectedProduct?.weight || 0;
const totalAmount = amountPerMeter * yarn.ratio * (parseInt(targetQuantity) || 0);
await supabase.from('yarn_ratios').insert({
plan_id: plan.id,
yarn_name: yarn.name,
ratio: yarn.ratio,
amount_per_meter: amountPerMeter * yarn.ratio,
total_amount: totalAmount
});
}
}
// 创建流程节点
const steps: ('confirm' | 'yarn_purchase' | 'dyeing' | 'machine_start' | 'fabric_warehouse')[] = ['confirm', 'yarn_purchase', 'dyeing', 'machine_start', 'fabric_warehouse'];
for (const step of steps) {
await supabase.from('plan_process_steps').insert({
plan_id: plan.id,
step_type: step,
status: 'pending' as const
});
}
// 记录初始价格旧价格为0表示新建
const finalPrice = parseFloat(productionPrice) || 0;
if (finalPrice > 0) {
await supabase.from('production_price_history').insert({
plan_id: plan.id,
old_price: 0,
new_price: finalPrice,
change_amount: finalPrice,
operator_id: auth.user?.id,
operator_name: auth.user?.username || '',
change_reason: '创建计划时设置初始价格'
});
}
}
alert('计划创建成功!');
navigate('/purchaser/plans');
} catch (err) {
alert('创建失败,请重试');
}
setSubmitting(false);
};
return (
<div className="min-h-screen bg-gray-50">
<div className="bg-white shadow-sm sticky top-0 z-10">
<div className="max-w-4xl mx-auto px-4 py-4 flex items-center gap-4">
<button onClick={() => navigate('/purchaser')} className="p-2 hover:bg-gray-100 rounded-full">
<ArrowLeft className="w-5 h-5" />
</button>
<h1 className="text-lg font-semibold"></h1>
</div>
</div>
<div className="max-w-4xl mx-auto px-4 py-6">
<form onSubmit={handleSubmit} className="space-y-6">
{/* 原坯布仓库产品选择 - 搜索+筛选形式 */}
<div className="bg-white rounded-lg shadow-sm p-6">
<h2 className="text-base font-semibold mb-4 flex items-center gap-2">
<span className="w-1 h-4 bg-blue-500 rounded"></span>
</h2>
{loading ? (
<div className="text-center py-8 text-gray-400">...</div>
) : products.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<p></p>
<button
type="button"
onClick={() => navigate('/purchaser/warehouse')}
className="mt-3 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700"
>
</button>
</div>
) : (
<div className="relative">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
setShowProductDropdown(true);
if (!e.target.value) {
handleClearProduct();
}
}}
onFocus={() => setShowProductDropdown(true)}
placeholder="搜索产品名称、坯布码、颜色、色号..."
className="w-full pl-10 pr-10 py-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
{searchQuery && (
<button
type="button"
onClick={handleClearProduct}
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 hover:bg-gray-100 rounded"
>
<X className="w-4 h-4 text-gray-400" />
</button>
)}
</div>
{/* 搜索结果下拉框 */}
{showProductDropdown && searchQuery.trim() && filteredProducts.length > 0 && (
<div className="absolute z-20 w-full mt-1 bg-white border rounded-lg shadow-lg max-h-60 overflow-y-auto">
{filteredProducts.map(product => (
<div
key={product.id}
onClick={() => handleSelectProduct(product)}
className="p-3 hover:bg-blue-50 cursor-pointer border-b last:border-b-0"
>
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900">
{product.product_name}-{product.weight}g-{product.color}
</p>
<p className="text-xs text-gray-500 mt-1">
: {product.fabric_code} | : {product.color_code}
</p>
</div>
<ChevronDown className="w-4 h-4 text-gray-400" />
</div>
</div>
))}
</div>
)}
{/* 无搜索结果提示 */}
{showProductDropdown && searchQuery.trim() && filteredProducts.length === 0 && (
<div className="absolute z-20 w-full mt-1 bg-white border rounded-lg shadow-lg p-4 text-center text-gray-500">
</div>
)}
{/* 已选择产品展示 */}
{selectedProduct && (
<div className="mt-4 p-4 bg-blue-50 rounded-lg border border-blue-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500"></p>
<p className="font-medium text-gray-900">
{selectedProduct.product_name}-{selectedProduct.weight}g-{selectedProduct.color}
</p>
<p className="text-xs text-gray-500 mt-1">
: {selectedProduct.fabric_code} | : {selectedProduct.color_code}
</p>
</div>
<button
type="button"
onClick={handleClearProduct}
className="p-2 text-red-500 hover:bg-red-50 rounded-lg"
>
<X className="w-4 h-4" />
</button>
</div>
</div>
)}
</div>
)}
</div>
{/* 产品信息 - 自动从原坯布产品获取 */}
<div className="bg-white rounded-lg shadow-sm p-6">
<h2 className="text-base font-semibold mb-4 flex items-center gap-2">
<span className="w-1 h-4 bg-blue-500 rounded"></span>
</h2>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-gray-600 mb-1"></label>
<input
type="text"
value={productName}
disabled
className="w-full px-3 py-2 border rounded-lg bg-gray-100 text-gray-700"
/>
<p className="text-xs text-gray-400 mt-1"></p>
</div>
<div>
<label className="block text-sm text-gray-600 mb-1"></label>
<input
type="text"
value={color}
disabled
className="w-full px-3 py-2 border rounded-lg bg-gray-100 text-gray-700"
/>
<p className="text-xs text-gray-400 mt-1"></p>
</div>
<div>
<label className="block text-sm text-gray-600 mb-1"></label>
<input
type="text"
value={fabricCode}
disabled
className="w-full px-3 py-2 border rounded-lg bg-gray-100 text-gray-700"
/>
<p className="text-xs text-gray-400 mt-1"></p>
</div>
<div>
<label className="block text-sm text-gray-600 mb-1"></label>
<input
type="text"
value={colorCode}
disabled
className="w-full px-3 py-2 border rounded-lg bg-gray-100 text-gray-700"
/>
<p className="text-xs text-gray-400 mt-1"></p>
</div>
<div>
<label className="block text-sm text-gray-600 mb-1">g/m</label>
<input
type="text"
value={selectedProduct?.weight ? `${selectedProduct.weight}g/m` : ''}
disabled
className="w-full px-3 py-2 border rounded-lg bg-gray-100 text-gray-700"
/>
<p className="text-xs text-gray-400 mt-1"></p>
</div>
<div>
<label className="block text-sm text-gray-600 mb-1">线</label>
<div className="w-full px-3 py-2 border rounded-lg bg-gray-100 text-gray-700 min-h-[42px]">
{yarns.filter(y => y.name).map((yarn, idx) => (
<span key={yarn.id} className="inline-block mr-2">
{yarn.name}:{yarn.ratio}{idx < yarns.filter(y => y.name).length - 1 ? '、' : ''}
</span>
))}
{!yarns.some(y => y.name) && <span className="text-gray-400">-</span>}
</div>
<p className="text-xs text-gray-400 mt-1"></p>
</div>
<div className="col-span-2">
<label className="block text-sm text-gray-600 mb-1"></label>
<input
type="text"
value={remark}
onChange={(e) => setRemark(e.target.value)}
placeholder="如60*4060双股等"
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
</div>
{/* 生产信息 */}
<div className="bg-white rounded-lg shadow-sm p-6">
<h2 className="text-base font-semibold mb-4 flex items-center gap-2">
<span className="w-1 h-4 bg-green-500 rounded"></span>
</h2>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-gray-600 mb-1"></label>
<select
value={textileFactory}
onChange={(e) => setTextileFactory(e.target.value)}
required
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value=""></option>
{factories.map(f => <option key={f.id} value={f.id}>{f.name}</option>)}
</select>
</div>
<div>
<label className="block text-sm text-gray-600 mb-1"></label>
<input
type="number"
value={targetQuantity}
onChange={(e) => setTargetQuantity(e.target.value)}
placeholder="请输入计划产量"
required
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div className="col-span-2">
<label className="block text-sm text-gray-600 mb-1">/</label>
<input
type="number"
value={productionPrice}
onChange={(e) => setProductionPrice(e.target.value)}
placeholder={selectedProduct ? `原坯布价格: ${selectedProduct.production_price || 0} 元/米` : "请输入采购价"}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
{selectedProduct && (
<p className="text-xs text-gray-400 mt-1"></p>
)}
</div>
</div>
</div>
<div className="flex gap-4">
<button type="button" onClick={() => navigate('/purchaser')} className="flex-1 py-3 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50"></button>
<button type="submit" disabled={submitting || !selectedProduct} className="flex-1 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50">
{submitting ? '创建中...' : '创建计划'}
</button>
</div>
</form>
</div>
</div>
);
}