283 lines
10 KiB
TypeScript
283 lines
10 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
|||
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|||
|
|
import { X, AlertTriangle, Edit2, Trash2 } from 'lucide-react';
|
|||
|
|
import { useResponsive } from '../../hooks/useResponsive';
|
|||
|
|
import { supabase } from '../../supabase/client';
|
|||
|
|
import type { PlanWithSteps } from '../../types';
|
|||
|
|
|
|||
|
|
interface PlanEditModalProps {
|
|||
|
|
plan: PlanWithSteps;
|
|||
|
|
isOpen: boolean;
|
|||
|
|
onClose: () => void;
|
|||
|
|
onPlanUpdated?: () => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function PlanEditModal({ plan, isOpen, onClose, onPlanUpdated }: PlanEditModalProps) {
|
|||
|
|
const { isMobile } = useResponsive();
|
|||
|
|
const [activeTab, setActiveTab] = useState<'edit' | 'delete'>('edit');
|
|||
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|||
|
|
const [error, setError] = useState('');
|
|||
|
|
|
|||
|
|
const [editForm, setEditForm] = useState({
|
|||
|
|
product_name: plan.product_name,
|
|||
|
|
color: plan.color,
|
|||
|
|
target_quantity: plan.target_quantity,
|
|||
|
|
remark: plan.remark || ''
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 判断是否可以编辑/删除:只有当"确认计划"步骤状态为 pending 时才允许
|
|||
|
|
const confirmStep = plan.processSteps.find(s => s.step_type === 'confirm');
|
|||
|
|
const canModify = confirmStep?.status === 'pending';
|
|||
|
|
|
|||
|
|
const handleUpdate = async (e: React.FormEvent) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
if (!canModify) {
|
|||
|
|
setError('计划已被工厂确认,无法修改');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setIsSubmitting(true);
|
|||
|
|
setError('');
|
|||
|
|
|
|||
|
|
const { error: updateError } = await supabase
|
|||
|
|
.from('production_plans')
|
|||
|
|
.update({
|
|||
|
|
product_name: editForm.product_name,
|
|||
|
|
color: editForm.color,
|
|||
|
|
target_quantity: editForm.target_quantity,
|
|||
|
|
remark: editForm.remark || null,
|
|||
|
|
updated_at: new Date().toISOString()
|
|||
|
|
})
|
|||
|
|
.eq('id', plan.id);
|
|||
|
|
|
|||
|
|
setIsSubmitting(false);
|
|||
|
|
|
|||
|
|
if (updateError) {
|
|||
|
|
setError('更新失败: ' + updateError.message);
|
|||
|
|
} else {
|
|||
|
|
onPlanUpdated?.();
|
|||
|
|
onClose();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleDelete = async () => {
|
|||
|
|
if (!canModify) {
|
|||
|
|
setError('计划已被工厂确认,无法删除');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setIsSubmitting(true);
|
|||
|
|
setError('');
|
|||
|
|
|
|||
|
|
// 先删除关联的 process_steps
|
|||
|
|
await supabase.from('plan_process_steps').delete().eq('plan_id', plan.id);
|
|||
|
|
|
|||
|
|
// 再删除 plan_factories 关联
|
|||
|
|
await supabase.from('plan_factories').delete().eq('plan_id', plan.id);
|
|||
|
|
|
|||
|
|
// 最后删除计划
|
|||
|
|
const { error: deleteError } = await supabase
|
|||
|
|
.from('production_plans')
|
|||
|
|
.delete()
|
|||
|
|
.eq('id', plan.id);
|
|||
|
|
|
|||
|
|
setIsSubmitting(false);
|
|||
|
|
|
|||
|
|
if (deleteError) {
|
|||
|
|
setError('删除失败: ' + deleteError.message);
|
|||
|
|
} else {
|
|||
|
|
onPlanUpdated?.();
|
|||
|
|
onClose();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (!isOpen) return null;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
|||
|
|
<motion.div
|
|||
|
|
initial={{ opacity: 0, scale: 0.95 }}
|
|||
|
|
animate={{ opacity: 1, scale: 1 }}
|
|||
|
|
exit={{ opacity: 0, scale: 0.95 }}
|
|||
|
|
className="bg-white rounded-2xl w-full max-w-md shadow-2xl overflow-hidden"
|
|||
|
|
>
|
|||
|
|
{/* 头部 */}
|
|||
|
|
<div className="flex items-center justify-between p-4 border-b border-gray-100">
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
{activeTab === 'edit' ? (
|
|||
|
|
<Edit2 className="w-5 h-5 text-blue-600" />
|
|||
|
|
) : (
|
|||
|
|
<Trash2 className="w-5 h-5 text-red-600" />
|
|||
|
|
)}
|
|||
|
|
<h3 className="text-lg font-semibold text-gray-900">
|
|||
|
|
{activeTab === 'edit' ? '编辑计划' : '删除计划'}
|
|||
|
|
</h3>
|
|||
|
|
</div>
|
|||
|
|
<button
|
|||
|
|
onClick={onClose}
|
|||
|
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|||
|
|
>
|
|||
|
|
<X className="w-5 h-5 text-gray-500" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Tab 切换 */}
|
|||
|
|
{canModify && (
|
|||
|
|
<div className="flex border-b border-gray-100">
|
|||
|
|
<button
|
|||
|
|
onClick={() => setActiveTab('edit')}
|
|||
|
|
className={`flex-1 py-3 text-sm font-medium transition-colors ${
|
|||
|
|
activeTab === 'edit'
|
|||
|
|
? 'text-blue-600 border-b-2 border-blue-600'
|
|||
|
|
: 'text-gray-500 hover:text-gray-700'
|
|||
|
|
}`}
|
|||
|
|
>
|
|||
|
|
编辑
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() => setActiveTab('delete')}
|
|||
|
|
className={`flex-1 py-3 text-sm font-medium transition-colors ${
|
|||
|
|
activeTab === 'delete'
|
|||
|
|
? 'text-red-600 border-b-2 border-red-600'
|
|||
|
|
: 'text-gray-500 hover:text-gray-700'
|
|||
|
|
}`}
|
|||
|
|
>
|
|||
|
|
删除
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 内容区域 */}
|
|||
|
|
<div className="p-4">
|
|||
|
|
{!canModify ? (
|
|||
|
|
<div className="text-center py-8">
|
|||
|
|
<AlertTriangle className="w-12 h-12 text-amber-500 mx-auto mb-3" />
|
|||
|
|
<p className="text-gray-600 mb-2">计划已被工厂确认</p>
|
|||
|
|
<p className="text-sm text-gray-400">已确认的计划无法修改或删除</p>
|
|||
|
|
</div>
|
|||
|
|
) : activeTab === 'edit' ? (
|
|||
|
|
<form onSubmit={handleUpdate} className="space-y-4">
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
产品名称
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={editForm.product_name}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, product_name: e.target.value })}
|
|||
|
|
className="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
|
|||
|
|
required
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
颜色
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={editForm.color}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, color: e.target.value })}
|
|||
|
|
className="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
|
|||
|
|
required
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
计划产量(米)
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={editForm.target_quantity}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, target_quantity: parseInt(e.target.value) || 0 })}
|
|||
|
|
className="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
|
|||
|
|
min="1"
|
|||
|
|
required
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
备注
|
|||
|
|
</label>
|
|||
|
|
<textarea
|
|||
|
|
value={editForm.remark}
|
|||
|
|
onChange={(e) => setEditForm({ ...editForm, remark: e.target.value })}
|
|||
|
|
className="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all resize-none"
|
|||
|
|
rows={3}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{error && (
|
|||
|
|
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-600">
|
|||
|
|
{error}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<div className="flex gap-3 pt-2">
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={onClose}
|
|||
|
|
className="flex-1 py-2.5 border border-gray-200 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors font-medium"
|
|||
|
|
>
|
|||
|
|
取消
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
type="submit"
|
|||
|
|
disabled={isSubmitting}
|
|||
|
|
className="flex-1 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium disabled:opacity-50"
|
|||
|
|
>
|
|||
|
|
{isSubmitting ? '保存中...' : '保存修改'}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
) : (
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<div className="p-4 bg-red-50 border border-red-200 rounded-xl">
|
|||
|
|
<div className="flex items-start gap-3">
|
|||
|
|
<AlertTriangle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
|
|||
|
|
<div>
|
|||
|
|
<p className="font-medium text-red-900">确认删除此计划?</p>
|
|||
|
|
<p className="text-sm text-red-700 mt-1">
|
|||
|
|
计划编号: {plan.plan_code}
|
|||
|
|
</p>
|
|||
|
|
<p className="text-sm text-red-700">
|
|||
|
|
产品: {plan.product_name}-{plan.color}
|
|||
|
|
</p>
|
|||
|
|
<p className="text-xs text-red-600 mt-2">
|
|||
|
|
此操作不可恢复,请谨慎操作
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{error && (
|
|||
|
|
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-600">
|
|||
|
|
{error}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<div className="flex gap-3">
|
|||
|
|
<button
|
|||
|
|
onClick={onClose}
|
|||
|
|
className="flex-1 py-2.5 border border-gray-200 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors font-medium"
|
|||
|
|
>
|
|||
|
|
取消
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={handleDelete}
|
|||
|
|
disabled={isSubmitting}
|
|||
|
|
className="flex-1 py-2.5 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors font-medium disabled:opacity-50"
|
|||
|
|
>
|
|||
|
|
{isSubmitting ? '删除中...' : '确认删除'}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</motion.div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|