748 lines
32 KiB
TypeScript
748 lines
32 KiB
TypeScript
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 { motion, AnimatePresence } from 'framer-motion';
|
||
import { useErrorHandler } from '../../hooks/useErrorHandler';
|
||
import type { Company, FactoryType } from '../../types';
|
||
import { PLAN_STATUS, STEP_STATUS } from '../../utils/constants';
|
||
|
||
interface YarnInput {
|
||
id: string;
|
||
name: string;
|
||
ratio: number;
|
||
yarn_type?: 'warp' | 'weft' | 'all';
|
||
}
|
||
|
||
// 产品类型定义(来自产品信息)
|
||
interface Product {
|
||
id: string;
|
||
product_name: string;
|
||
weight: number;
|
||
warp_weight: number | null;
|
||
weft_weight: number | null;
|
||
total_weight: number | null;
|
||
color: string;
|
||
fabric_code: string;
|
||
color_code: string;
|
||
production_price: number;
|
||
image_url: string | null;
|
||
yarn_usage_per_meter: number;
|
||
}
|
||
|
||
// 纱线配比类型
|
||
interface ProductYarnRatio {
|
||
id: string;
|
||
product_id: string;
|
||
yarn_name: string;
|
||
ratio: number;
|
||
yarn_type?: 'warp' | 'weft' | 'all';
|
||
}
|
||
|
||
// 纱线类型显示映射
|
||
const yarnTypeLabels: Record<string, string> = {
|
||
warp: '经线',
|
||
weft: '纬线',
|
||
all: '全部'
|
||
};
|
||
|
||
export function NewPlan() {
|
||
const navigate = useNavigate();
|
||
const { auth } = useAuth();
|
||
const handleError = useErrorHandler();
|
||
|
||
// 从产品信息选择的产品
|
||
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 [showProductModal, setShowProductModal] = 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(() => {
|
||
if (auth.company) {
|
||
fetchFactories();
|
||
fetchProducts();
|
||
}
|
||
}, [auth.company]);
|
||
|
||
// 从产品信息获取产品数据
|
||
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 () => {
|
||
if (!auth.company) return;
|
||
|
||
// 查询工厂管理中登记的纺织厂(companies表中role=textile的记录)
|
||
const { data } = await supabase
|
||
.from('companies')
|
||
.select('*')
|
||
.eq('role', 'textile')
|
||
.order('created_at', { ascending: false });
|
||
|
||
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);
|
||
setShowProductModal(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,
|
||
yarn_type: r.yarn_type || 'all'
|
||
})));
|
||
} else {
|
||
setYarns([{ id: '1', name: '', ratio: 1, yarn_type: 'all' }]);
|
||
}
|
||
};
|
||
|
||
// 清除选择的产品
|
||
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: PLAN_STATUS.PENDING,
|
||
created_by: auth.user?.id
|
||
})
|
||
.select()
|
||
.maybeSingle();
|
||
|
||
if (planError) {
|
||
handleError(planError, { userMessage: '创建计划失败' });
|
||
setSubmitting(false);
|
||
return;
|
||
}
|
||
|
||
if (plan) {
|
||
// 创建纱线配比
|
||
const yarnErrors: string[] = [];
|
||
for (const yarn of yarns) {
|
||
if (yarn.name) {
|
||
const amountPerMeter = selectedProduct?.weight || 0;
|
||
const totalAmount = amountPerMeter * yarn.ratio * (parseInt(targetQuantity) || 0);
|
||
const { error: yarnError } = 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
|
||
});
|
||
if (yarnError) {
|
||
yarnErrors.push(yarnError.message);
|
||
console.error('纱线配比创建失败:', yarnError);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 创建流程节点
|
||
const stepErrors: string[] = [];
|
||
const steps: ('confirm' | 'yarn_purchase' | 'dyeing' | 'machine_start' | 'fabric_warehouse')[] = ['confirm', 'yarn_purchase', 'dyeing', 'machine_start', 'fabric_warehouse'];
|
||
for (const step of steps) {
|
||
const { error: stepError } = await supabase.from('plan_process_steps').insert({
|
||
plan_id: plan.id,
|
||
step_type: step,
|
||
status: STEP_STATUS.PENDING
|
||
});
|
||
if (stepError) {
|
||
stepErrors.push(stepError.message);
|
||
console.error('流程节点创建失败:', stepError);
|
||
}
|
||
}
|
||
|
||
// 记录初始价格(旧价格为0,表示新建)
|
||
const finalPrice = parseFloat(productionPrice) || 0;
|
||
if (finalPrice > 0) {
|
||
const { error: priceError } = 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: '创建计划时设置初始价格'
|
||
});
|
||
if (priceError) {
|
||
console.error('价格历史记录失败:', priceError);
|
||
}
|
||
}
|
||
|
||
// 注意:工厂关联不再在创建计划时自动进行
|
||
// 而是通过分享链接或推送功能,由工厂确认后才建立关联
|
||
|
||
// 如果有子操作失败,提示用户但允许继续
|
||
if (yarnErrors.length > 0 || stepErrors.length > 0) {
|
||
console.warn('计划创建完成,但部分数据创建失败:', { yarnErrors, stepErrors });
|
||
}
|
||
}
|
||
|
||
alert('计划创建成功!');
|
||
navigate('/purchaser/plans');
|
||
} catch (err) {
|
||
handleError(err, { userMessage: '创建失败,请重试' });
|
||
}
|
||
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 && (
|
||
<div className="absolute z-20 w-full mt-1 bg-white border rounded-lg shadow-lg max-h-60 overflow-y-auto">
|
||
{filteredProducts.length > 0 ? (
|
||
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 className="flex items-center gap-3">
|
||
{product.image_url ? (
|
||
<img
|
||
src={product.image_url}
|
||
alt={product.product_name}
|
||
className="w-10 h-10 rounded-lg object-cover border border-gray-200 flex-shrink-0"
|
||
/>
|
||
) : (
|
||
<div className="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center flex-shrink-0">
|
||
<span className="text-gray-400 text-xs">无图</span>
|
||
</div>
|
||
)}
|
||
<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} | 价格: ¥{product.production_price || 0}/米
|
||
</p>
|
||
<p className="text-xs text-gray-400">
|
||
克重: {product.weight}g/m |
|
||
经线: {product.warp_weight || '-'}g/m |
|
||
纬线: {product.weft_weight || '-'}g/m
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<ChevronDown className="w-4 h-4 text-gray-400" />
|
||
</div>
|
||
</div>
|
||
))
|
||
) : searchQuery.trim() ? (
|
||
<div className="p-4 text-center text-gray-500">
|
||
未找到匹配的产品
|
||
</div>
|
||
) : (
|
||
<div className="p-4 text-center text-gray-400">
|
||
输入关键词搜索产品
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 已选择产品展示 */}
|
||
{selectedProduct && (
|
||
<div className="mt-4 p-4 bg-blue-50 rounded-lg border border-blue-200">
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex items-start gap-3">
|
||
{selectedProduct.image_url ? (
|
||
<img
|
||
src={selectedProduct.image_url}
|
||
alt={selectedProduct.product_name}
|
||
className="w-16 h-16 rounded-lg object-cover border border-gray-200 flex-shrink-0"
|
||
/>
|
||
) : (
|
||
<div className="w-16 h-16 rounded-lg bg-gray-200 flex items-center justify-center flex-shrink-0">
|
||
<span className="text-gray-400 text-xs">无图片</span>
|
||
</div>
|
||
)}
|
||
<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>
|
||
<p className="text-xs text-gray-500">
|
||
克重: {selectedProduct.weight}g/m |
|
||
经线: {selectedProduct.warp_weight || '-'}g/m |
|
||
纬线: {selectedProduct.weft_weight || '-'}g/m
|
||
</p>
|
||
</div>
|
||
</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">经线克重(g/m)</label>
|
||
<input
|
||
type="text"
|
||
value={selectedProduct?.warp_weight ? `${selectedProduct.warp_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">纬线克重(g/m)</label>
|
||
<input
|
||
type="text"
|
||
value={selectedProduct?.weft_weight ? `${selectedProduct.weft_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">每米纱用量(g/m)</label>
|
||
<input
|
||
type="text"
|
||
value={selectedProduct?.total_weight ? `${selectedProduct.total_weight}g/m` : (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 className="col-span-2">
|
||
<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] flex flex-wrap items-center gap-x-4">
|
||
{yarns.filter(y => y.name).map((yarn, idx) => (
|
||
<span key={yarn.id} className="inline-block whitespace-nowrap">
|
||
{yarnTypeLabels[yarn.yarn_type || 'all']}:{yarn.name} 占比:{Number(yarn.ratio).toFixed(0)}%
|
||
</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*40,60双股等"
|
||
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>
|
||
|
||
{/* 产品选择弹窗 */}
|
||
<AnimatePresence>
|
||
{showProductModal && (
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4"
|
||
onClick={() => setShowProductModal(false)}
|
||
>
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||
transition={{ duration: 0.2 }}
|
||
className="bg-white rounded-xl shadow-2xl w-full max-w-2xl max-h-[80vh] overflow-hidden"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
{/* 弹窗头部 */}
|
||
<div className="p-4 border-b flex items-center justify-between bg-gray-50">
|
||
<h3 className="text-lg font-semibold text-gray-900">选择产品</h3>
|
||
<button
|
||
onClick={() => setShowProductModal(false)}
|
||
className="p-2 hover:bg-gray-200 rounded-full transition-colors"
|
||
>
|
||
<X className="w-5 h-5 text-gray-500" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* 搜索框 */}
|
||
<div className="p-4 border-b">
|
||
<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)}
|
||
placeholder="搜索产品名称、坯布码、颜色、色号..."
|
||
className="w-full pl-10 pr-4 py-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 产品列表 */}
|
||
<div className="overflow-y-auto max-h-[50vh]">
|
||
{filteredProducts.length > 0 ? (
|
||
<div className="divide-y">
|
||
{filteredProducts.map((product) => (
|
||
<div
|
||
key={product.id}
|
||
onClick={() => handleSelectProduct(product)}
|
||
className="p-4 hover:bg-blue-50 cursor-pointer transition-colors"
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3 flex-1">
|
||
{product.image_url ? (
|
||
<img
|
||
src={product.image_url}
|
||
alt={product.product_name}
|
||
className="w-12 h-12 rounded-lg object-cover border border-gray-200 flex-shrink-0"
|
||
/>
|
||
) : (
|
||
<div className="w-12 h-12 rounded-lg bg-gray-100 flex items-center justify-center flex-shrink-0">
|
||
<span className="text-gray-400 text-xs">无图</span>
|
||
</div>
|
||
)}
|
||
<div className="flex-1">
|
||
<p className="font-medium text-gray-900">
|
||
{product.product_name}-{product.weight}g-{product.color}
|
||
</p>
|
||
<div className="flex items-center gap-4 mt-1 text-sm text-gray-500">
|
||
<span>产品码: {product.fabric_code}</span>
|
||
<span>色号: {product.color_code}</span>
|
||
<span>价格: ¥{product.production_price || 0}/米</span>
|
||
</div>
|
||
<div className="flex items-center gap-4 mt-1 text-xs text-gray-400">
|
||
<span>克重: {product.weight}g/m</span>
|
||
<span>经线: {product.warp_weight || '-'}g/m</span>
|
||
<span>纬线: {product.weft_weight || '-'}g/m</span>
|
||
<span>总克重: {product.total_weight || product.weight}g/m</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center">
|
||
<ChevronDown className="w-4 h-4 text-blue-600 -rotate-90" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : searchQuery.trim() ? (
|
||
<div className="p-8 text-center text-gray-500">
|
||
<Search className="w-12 h-12 mx-auto mb-3 text-gray-300" />
|
||
<p>未找到匹配的产品</p>
|
||
<p className="text-sm text-gray-400 mt-1">请尝试其他关键词</p>
|
||
</div>
|
||
) : (
|
||
<div className="p-8 text-center text-gray-500">
|
||
<p className="text-gray-400">输入关键词搜索产品</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 弹窗底部 */}
|
||
<div className="p-4 border-t bg-gray-50 flex justify-between items-center">
|
||
<span className="text-sm text-gray-500">
|
||
共 {products.length} 个产品
|
||
</span>
|
||
<button
|
||
onClick={() => setShowProductModal(false)}
|
||
className="px-4 py-2 text-gray-600 hover:text-gray-800 transition-colors"
|
||
>
|
||
取消
|
||
</button>
|
||
</div>
|
||
</motion.div>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
);
|
||
}
|