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

456 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}