iloom-flatten/src/components/plans/PlanEditModal.tsx
Chever John b1c2c33aa2
refactor: replace Supabase SDK with self-hosted REST API client layer
WHY: migrating from Supabase BaaS to self-hosted iloom backend
(Go microservices) deployed on K3s. Supabase client and realtime
subscriptions no longer applicable.

HOW:
- add src/api/ layer (client.ts, auth.ts, purchaser.ts, textile.ts,
  washing.ts, websocket.ts) targeting iloom-gateway REST endpoints
- rewrite all hooks (usePlanData, useInventoryData, useDashboardData,
  useRealtime, etc.) to use new API client instead of Supabase queries
- rewrite all page/component data fetching and mutations accordingly
- delete src/supabase/client.ts, remove @supabase/supabase-js dep
- add Dockerfile (multi-stage node build + nginx) and nginx.conf
  with reverse proxy to iloom-gateway for /api/ and /ws/ routes
- adjust webpack.config.js for API_URL environment variable injection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-14 12:38:47 +08:00

269 lines
9.7 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 } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, AlertTriangle, Edit2, Trash2 } from 'lucide-react';
import { useResponsive } from '../../hooks/useResponsive';
import { purchaserApi } from '../../api';
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('');
try {
await purchaserApi.updatePlan(plan.id, {
product_name: editForm.product_name,
color_code: editForm.color,
target_quantity: editForm.target_quantity,
notes: editForm.remark || undefined,
});
setIsSubmitting(false);
onPlanUpdated?.();
onClose();
} catch (err: any) {
setIsSubmitting(false);
setError('更新失败: ' + (err.message || '未知错误'));
}
};
const handleDelete = async () => {
if (!canModify) {
setError('计划已被工厂确认,无法删除');
return;
}
setIsSubmitting(true);
setError('');
try {
await purchaserApi.deletePlan(plan.id);
setIsSubmitting(false);
onPlanUpdated?.();
onClose();
} catch (err: any) {
setIsSubmitting(false);
setError('删除失败: ' + (err.message || '未知错误'));
}
};
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>
);
}