iloom-flatten/src/pages/purchaser/FactoryManage.tsx

456 lines
19 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, 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>
);
}