456 lines
19 KiB
TypeScript
456 lines
19 KiB
TypeScript
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 { Factory, Droplets, FileText, ChevronRight, ArrowLeft, Plus, X, Building2, Phone, MapPin, User, Edit2 } from 'lucide-react';
|
||
import { useResponsive } from '../../hooks/useResponsive';
|
||
import type { Company } from '../../types';
|
||
|
||
export function FactoryManage() {
|
||
const navigate = useNavigate();
|
||
const { auth } = useAuth();
|
||
const { isMobile } = useResponsive();
|
||
const [activeTab, setActiveTab] = useState<'textile' | 'washing'>('textile');
|
||
const [factories, setFactories] = useState<Company[]>([]);
|
||
const [planCounts, setPlanCounts] = useState<Record<string, number>>({});
|
||
const [loading, setLoading] = useState(true);
|
||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||
|
||
// 新建/编辑工厂弹窗状态
|
||
const [showModal, setShowModal] = useState(false);
|
||
const [editingFactory, setEditingFactory] = useState<Company | null>(null);
|
||
const [formData, setFormData] = useState({
|
||
name: '',
|
||
contactPerson: '',
|
||
contactPhone: '',
|
||
address: ''
|
||
});
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [error, setError] = useState('');
|
||
|
||
useEffect(() => {
|
||
if (!auth.company) return;
|
||
fetchFactories();
|
||
}, [auth.company, activeTab]);
|
||
|
||
const fetchFactories = async () => {
|
||
setLoading(true);
|
||
// 直接查询所有该类型的工厂(不再通过 plan_factories 关联)
|
||
const { data: factoryData } = await supabase
|
||
.from('companies')
|
||
.select('*')
|
||
.eq('role', activeTab)
|
||
.order('created_at', { ascending: false });
|
||
|
||
setFactories(factoryData || []);
|
||
|
||
// 计算每个工厂的计划数(通过 plan_factories 统计)
|
||
if (factoryData && factoryData.length > 0) {
|
||
const factoryIds = factoryData.map(f => f.id);
|
||
const { data: planFactories } = await supabase
|
||
.from('plan_factories')
|
||
.select('factory_id')
|
||
.in('factory_id', factoryIds)
|
||
.eq('factory_type', activeTab);
|
||
|
||
const counts: Record<string, number> = {};
|
||
planFactories?.forEach(pf => {
|
||
counts[pf.factory_id] = (counts[pf.factory_id] || 0) + 1;
|
||
});
|
||
setPlanCounts(counts);
|
||
} else {
|
||
setPlanCounts({});
|
||
}
|
||
setLoading(false);
|
||
};
|
||
|
||
const handleOpenModal = () => {
|
||
setEditingFactory(null);
|
||
setFormData({ name: '', contactPerson: '', contactPhone: '', address: '' });
|
||
setError('');
|
||
setShowModal(true);
|
||
};
|
||
|
||
const handleOpenEditModal = (factory: Company) => {
|
||
setEditingFactory(factory);
|
||
setFormData({
|
||
name: factory.name,
|
||
contactPerson: '',
|
||
contactPhone: factory.contact_phone || '',
|
||
address: factory.address || ''
|
||
});
|
||
setError('');
|
||
setShowModal(true);
|
||
};
|
||
|
||
const handleCloseModal = () => {
|
||
setShowModal(false);
|
||
setEditingFactory(null);
|
||
setFormData({ name: '', contactPerson: '', contactPhone: '', address: '' });
|
||
setError('');
|
||
};
|
||
|
||
const validateForm = () => {
|
||
if (!formData.name.trim()) return '请输入工厂名称';
|
||
if (!formData.contactPhone.trim()) return '请输入联系电话';
|
||
return '';
|
||
};
|
||
|
||
// 检查工厂名称是否已存在(同一类型下)
|
||
const checkDuplicateName = async (name: string, excludeId?: string): Promise<boolean> => {
|
||
const { data, error } = await supabase
|
||
.from('companies')
|
||
.select('id')
|
||
.eq('role', activeTab)
|
||
.ilike('name', name.trim())
|
||
.limit(1);
|
||
|
||
if (error) {
|
||
console.error('检查重名失败:', error);
|
||
return false;
|
||
}
|
||
|
||
// 如果存在记录且不是当前编辑的工厂,则认为是重名
|
||
if (data && data.length > 0) {
|
||
if (excludeId && data[0].id === excludeId) {
|
||
return false; // 是自己,不算重名
|
||
}
|
||
return true;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
const validationError = validateForm();
|
||
if (validationError) {
|
||
setError(validationError);
|
||
return;
|
||
}
|
||
|
||
if (!auth.company) {
|
||
setError('请先登录');
|
||
return;
|
||
}
|
||
|
||
setSubmitting(true);
|
||
setError('');
|
||
|
||
try {
|
||
// 检查重名
|
||
const isDuplicate = await checkDuplicateName(formData.name, editingFactory?.id);
|
||
if (isDuplicate) {
|
||
setError(`该${activeTab === 'textile' ? '织布厂' : '水洗厂'}名称已存在,请使用其他名称`);
|
||
setSubmitting(false);
|
||
return;
|
||
}
|
||
|
||
if (editingFactory) {
|
||
// 更新工厂信息
|
||
const { error: updateError } = await supabase
|
||
.from('companies')
|
||
.update({
|
||
name: formData.name,
|
||
contact_phone: formData.contactPhone,
|
||
address: formData.address
|
||
})
|
||
.eq('id', editingFactory.id);
|
||
|
||
if (updateError) {
|
||
setError('更新工厂失败:' + updateError.message);
|
||
setSubmitting(false);
|
||
return;
|
||
}
|
||
} else {
|
||
// 创建工厂(作为公司)
|
||
const { data: newFactory, error: factoryError } = await supabase
|
||
.from('companies')
|
||
.insert({
|
||
name: formData.name,
|
||
role: activeTab,
|
||
contact_phone: formData.contactPhone,
|
||
address: formData.address
|
||
})
|
||
.select()
|
||
.single();
|
||
|
||
if (factoryError || !newFactory) {
|
||
setError('创建工厂失败:' + factoryError?.message);
|
||
setSubmitting(false);
|
||
return;
|
||
}
|
||
// 工厂创建成功,直接显示在列表中(不再需要通过 plan_factories 关联)
|
||
}
|
||
|
||
// 刷新工厂列表
|
||
await fetchFactories();
|
||
handleCloseModal();
|
||
} catch (err) {
|
||
setError(editingFactory ? '更新工厂时发生错误' : '创建工厂时发生错误');
|
||
console.error(err);
|
||
}
|
||
|
||
setSubmitting(false);
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 p-4 md:p-6">
|
||
<div className="max-w-4xl mx-auto">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<div className="flex items-center gap-3 md:gap-4">
|
||
<button onClick={() => navigate('/purchaser')} className="p-2 hover:bg-gray-200 rounded-full transition-colors">
|
||
<ArrowLeft className="w-5 h-5 md:w-6 md:h-6 text-gray-600" />
|
||
</button>
|
||
<h1 className="text-xl md:text-2xl font-bold text-gray-800">工厂管理</h1>
|
||
</div>
|
||
<motion.button
|
||
whileHover={{ scale: isMobile ? 1 : 1.05 }}
|
||
whileTap={{ scale: 0.95 }}
|
||
onClick={handleOpenModal}
|
||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm md:text-base"
|
||
>
|
||
<Plus size={18} />
|
||
新建工厂
|
||
</motion.button>
|
||
</div>
|
||
|
||
<div className="flex gap-3 md:gap-4 mb-6">
|
||
<button
|
||
onClick={() => setActiveTab('textile')}
|
||
className={`flex items-center gap-2 px-4 md:px-6 py-2.5 md:py-3 rounded-lg font-medium transition-all text-sm md:text-base ${
|
||
activeTab === 'textile' ? 'bg-blue-600 text-white shadow-lg' : 'bg-white text-gray-600 hover:bg-gray-100'
|
||
}`}
|
||
>
|
||
<Factory size={18} className="md:w-5 md:h-5" />
|
||
织布厂
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('washing')}
|
||
className={`flex items-center gap-2 px-4 md:px-6 py-2.5 md:py-3 rounded-lg font-medium transition-all text-sm md:text-base ${
|
||
activeTab === 'washing' ? 'bg-blue-600 text-white shadow-lg' : 'bg-white text-gray-600 hover:bg-gray-100'
|
||
}`}
|
||
>
|
||
<Droplets size={18} className="md:w-5 md:h-5" />
|
||
水洗厂
|
||
</button>
|
||
</div>
|
||
|
||
{loading ? (
|
||
<div className="text-center py-12 text-gray-400">加载中...</div>
|
||
) : factories.length === 0 ? (
|
||
<div className="bg-white rounded-lg shadow-sm p-8 text-center">
|
||
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||
<Building2 className="w-8 h-8 text-gray-400" />
|
||
</div>
|
||
<p className="text-gray-500 mb-2">暂无关联的{activeTab === 'textile' ? '织布厂' : '水洗厂'}</p>
|
||
<p className="text-gray-400 text-sm mb-4">点击右上角"新建工厂"按钮添加</p>
|
||
<motion.button
|
||
whileHover={{ scale: isMobile ? 1 : 1.05 }}
|
||
whileTap={{ scale: 0.95 }}
|
||
onClick={handleOpenModal}
|
||
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||
>
|
||
新建工厂
|
||
</motion.button>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-3">
|
||
{factories.map(factory => (
|
||
<div key={factory.id} className="bg-white rounded-lg shadow-sm border border-gray-100">
|
||
<div
|
||
onClick={() => setExpandedId(expandedId === factory.id ? null : factory.id)}
|
||
className="flex items-center justify-between p-4 cursor-pointer hover:bg-gray-50 transition-colors"
|
||
>
|
||
<div className="flex items-center gap-3 md:gap-4">
|
||
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
||
{activeTab === 'textile' ? <Factory className="w-5 h-5 text-blue-600" /> : <Droplets className="w-5 h-5 text-blue-600" />}
|
||
</div>
|
||
<div>
|
||
<h3 className="font-semibold text-gray-800 text-sm md:text-base">{factory.name}</h3>
|
||
<p className="text-xs md:text-sm text-gray-500">{factory.address || '-'}</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-3 md:gap-4">
|
||
<span className="flex items-center gap-1 text-xs md:text-sm text-gray-600">
|
||
<FileText size={14} className="md:w-4 md:h-4" />
|
||
{planCounts[factory.id] || 0}个计划
|
||
</span>
|
||
<ChevronRight className={`w-5 h-5 text-gray-400 transition-transform ${expandedId === factory.id ? 'rotate-90' : ''}`} />
|
||
</div>
|
||
</div>
|
||
|
||
<AnimatePresence>
|
||
{expandedId === factory.id && (
|
||
<motion.div
|
||
initial={{ height: 0, opacity: 0 }}
|
||
animate={{ height: 'auto', opacity: 1 }}
|
||
exit={{ height: 0, opacity: 0 }}
|
||
transition={{ duration: 0.2 }}
|
||
className="border-t border-gray-100 overflow-hidden"
|
||
>
|
||
<div className="p-4">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm mb-4">
|
||
<div className="flex items-center gap-2">
|
||
<User size={14} className="text-gray-400" />
|
||
<span className="text-gray-500">联系人:</span>
|
||
<span className="text-gray-800">-</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Phone size={14} className="text-gray-400" />
|
||
<span className="text-gray-500">联系电话:</span>
|
||
<span className="text-gray-800">{factory.contact_phone || '-'}</span>
|
||
</div>
|
||
<div className="flex items-center gap-2 md:col-span-2">
|
||
<MapPin size={14} className="text-gray-400" />
|
||
<span className="text-gray-500">地址:</span>
|
||
<span className="text-gray-800">{factory.address || '-'}</span>
|
||
</div>
|
||
</div>
|
||
<div className="flex justify-end">
|
||
<motion.button
|
||
whileTap={{ scale: 0.98 }}
|
||
onClick={() => handleOpenEditModal(factory)}
|
||
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors"
|
||
>
|
||
<Edit2 className="w-4 h-4" />
|
||
编辑工厂
|
||
</motion.button>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 新建工厂弹窗 */}
|
||
<AnimatePresence>
|
||
{showModal && (
|
||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||
<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-2xl w-full max-w-md shadow-xl"
|
||
>
|
||
<div className="flex items-center justify-between p-4 md:p-6 border-b border-gray-100">
|
||
<h2 className="text-lg md:text-xl font-bold text-gray-800">
|
||
{editingFactory ? '编辑' : '新建'}{activeTab === 'textile' ? '织布厂' : '水洗厂'}
|
||
</h2>
|
||
<button
|
||
onClick={handleCloseModal}
|
||
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
|
||
>
|
||
<X size={20} className="text-gray-500" />
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmit} className="p-4 md:p-6 space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||
工厂名称 <span className="text-red-500">*</span>
|
||
</label>
|
||
<div className="relative">
|
||
<Building2 className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||
<input
|
||
type="text"
|
||
value={formData.name}
|
||
onChange={e => setFormData({ ...formData, name: e.target.value })}
|
||
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
|
||
placeholder={`请输入${activeTab === 'textile' ? '织布厂' : '水洗厂'}名称`}
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||
联系人
|
||
</label>
|
||
<div className="relative">
|
||
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||
<input
|
||
type="text"
|
||
value={formData.contactPerson}
|
||
onChange={e => setFormData({ ...formData, contactPerson: e.target.value })}
|
||
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
|
||
placeholder="请输入联系人姓名"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||
联系电话 <span className="text-red-500">*</span>
|
||
</label>
|
||
<div className="relative">
|
||
<Phone className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||
<input
|
||
type="tel"
|
||
value={formData.contactPhone}
|
||
onChange={e => setFormData({ ...formData, contactPhone: e.target.value })}
|
||
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
|
||
placeholder="请输入联系电话"
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||
地址
|
||
</label>
|
||
<div className="relative">
|
||
<MapPin className="absolute left-3 top-3 w-5 h-5 text-gray-400" />
|
||
<textarea
|
||
value={formData.address}
|
||
onChange={e => setFormData({ ...formData, address: e.target.value })}
|
||
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all resize-none"
|
||
placeholder="请输入工厂地址"
|
||
rows={3}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{error && (
|
||
<motion.p
|
||
initial={{ opacity: 0, y: -10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
className="text-red-500 text-sm text-center bg-red-50 py-2 rounded-lg"
|
||
>
|
||
{error}
|
||
</motion.p>
|
||
)}
|
||
|
||
<div className="flex gap-3 pt-2">
|
||
<button
|
||
type="button"
|
||
onClick={handleCloseModal}
|
||
className="flex-1 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors font-medium"
|
||
>
|
||
取消
|
||
</button>
|
||
<motion.button
|
||
whileHover={{ scale: isMobile ? 1 : 1.02 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
type="submit"
|
||
disabled={submitting}
|
||
className="flex-1 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
{submitting ? (editingFactory ? '保存中...' : '创建中...') : (editingFactory ? '保存修改' : '创建工厂')}
|
||
</motion.button>
|
||
</div>
|
||
</form>
|
||
</motion.div>
|
||
</div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
);
|
||
}
|