Files
sam-react-prod/src/components/production/WorkOrders/WorkOrderEdit.tsx

390 lines
13 KiB
TypeScript
Raw Normal View History

'use client';
/**
*
* WorkOrderCreate
* IntegratedDetailTemplate (2025-01-20)
*/
import { useState, useEffect, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { AssigneeSelectModal } from './AssigneeSelectModal';
import { toast } from 'sonner';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { getWorkOrderById, updateWorkOrder, getProcessOptions, type ProcessOption } from './actions';
import type { WorkOrder } from './types';
import { workOrderEditConfig } from './workOrderConfig';
// Validation 에러 타입
interface ValidationErrors {
[key: string]: string;
}
// 필드명 매핑
const FIELD_NAME_MAP: Record<string, string> = {
processId: '공정',
scheduledDate: '출고예정일',
};
interface FormData {
// 기본 정보 (읽기 전용)
client: string;
projectName: string;
orderNo: string;
itemCount: number;
// 수정 가능 정보
processId: number | null;
scheduledDate: string;
priority: number;
assignees: string[];
// 비고
note: string;
}
interface WorkOrderEditProps {
orderId: string;
}
export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
const router = useRouter();
const [workOrder, setWorkOrder] = useState<WorkOrder | null>(null);
const [formData, setFormData] = useState<FormData>({
client: '',
projectName: '',
orderNo: '',
itemCount: 0,
processId: null,
scheduledDate: '',
priority: 5,
assignees: [],
note: '',
});
const [isAssigneeModalOpen, setIsAssigneeModalOpen] = useState(false);
const [assigneeNames, setAssigneeNames] = useState<string[]>([]);
const [validationErrors, setValidationErrors] = useState<ValidationErrors>({});
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [processOptions, setProcessOptions] = useState<ProcessOption[]>([]);
const [isLoadingProcesses, setIsLoadingProcesses] = useState(true);
// 데이터 로드
const loadData = useCallback(async () => {
setIsLoading(true);
try {
const [orderResult, processResult] = await Promise.all([
getWorkOrderById(orderId),
getProcessOptions(),
]);
if (orderResult.success && orderResult.data) {
const order = orderResult.data;
setWorkOrder(order);
setFormData({
client: order.client,
projectName: order.projectName,
orderNo: order.lotNo,
itemCount: order.items?.length || 0,
processId: order.processId,
scheduledDate: order.scheduledDate || '',
priority: order.priority || 5,
assignees: order.assignees?.map(a => a.id) || [],
note: order.note || '',
});
// 담당자 이름 설정
if (order.assignees) {
setAssigneeNames(order.assignees.map(a => a.name));
}
} else {
toast.error(orderResult.error || '작업지시 조회에 실패했습니다.');
router.push('/production/work-orders');
return;
}
if (processResult.success) {
setProcessOptions(processResult.data);
} else {
toast.error(processResult.error || '공정 목록을 불러오는데 실패했습니다.');
}
setIsLoadingProcesses(false);
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[WorkOrderEdit] loadData error:', error);
toast.error('데이터 로드 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
}, [orderId, router]);
useEffect(() => {
loadData();
}, [loadData]);
// 폼 제출
const handleSubmit = async (): Promise<{ success: boolean; error?: string }> => {
// Validation 체크
const errors: ValidationErrors = {};
if (!formData.processId) {
errors.processId = '공정을 선택해주세요';
}
if (!formData.scheduledDate) {
errors.scheduledDate = '출고예정일을 선택해주세요';
}
// 에러가 있으면 상태 업데이트 후 리턴
if (Object.keys(errors).length > 0) {
setValidationErrors(errors);
window.scrollTo({ top: 0, behavior: 'smooth' });
return { success: false, error: '입력 정보를 확인해주세요.' };
}
// 에러 초기화
setValidationErrors({});
setIsSubmitting(true);
try {
// 담당자 ID 배열 변환 (string[] → number[])
const assigneeIds = formData.assignees
.map(id => parseInt(id, 10))
.filter(id => !isNaN(id));
const result = await updateWorkOrder(orderId, {
projectName: formData.projectName,
processId: formData.processId!,
scheduledDate: formData.scheduledDate,
priority: formData.priority,
assigneeIds: assigneeIds.length > 0 ? assigneeIds : undefined,
note: formData.note || undefined,
});
if (result.success) {
toast.success('작업지시가 수정되었습니다.');
router.push(`/production/work-orders/${orderId}?mode=view`);
return { success: true };
} else {
return { success: false, error: result.error || '작업지시 수정에 실패했습니다.' };
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[WorkOrderEdit] handleSubmit error:', error);
return { success: false, error: '작업지시 수정 중 오류가 발생했습니다.' };
} finally {
setIsSubmitting(false);
}
};
// 취소
const handleCancel = () => {
router.back();
};
// 선택된 공정의 코드 가져오기
const getSelectedProcessCode = (): string => {
const selectedProcess = processOptions.find(p => p.id === formData.processId);
return selectedProcess?.processCode || '-';
};
// 동적 config (작업지시 번호 포함)
const dynamicConfig = {
...workOrderEditConfig,
title: workOrder ? `작업지시 (${workOrder.workOrderNo})` : '작업지시',
};
// 폼 컨텐츠 렌더링
const renderFormContent = useCallback(() => (
<div className="space-y-6">
{/* Validation 에러 표시 */}
{Object.keys(validationErrors).length > 0 && (
<Alert className="bg-red-50 border-red-200">
<AlertDescription className="text-red-900">
<div className="flex items-start gap-2">
<span className="text-lg"></span>
<div className="flex-1">
<strong className="block mb-2">
({Object.keys(validationErrors).length} )
</strong>
<ul className="space-y-1 text-sm">
{Object.entries(validationErrors).map(([field, message]) => {
const fieldName = FIELD_NAME_MAP[field] || field;
return (
<li key={field} className="flex items-start gap-1">
<span></span>
<span>
<strong>{fieldName}</strong>: {message}
</span>
</li>
);
})}
</ul>
</div>
</div>
</AlertDescription>
</Alert>
)}
{/* 기본 정보 (읽기 전용) */}
<section className="bg-muted/30 border rounded-lg p-6">
<h3 className="font-semibold mb-4"> </h3>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Input
value={formData.client}
disabled
className="bg-muted"
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value={formData.projectName}
onChange={(e) => setFormData({ ...formData, projectName: e.target.value })}
className="bg-white"
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value={formData.orderNo}
disabled
className="bg-muted"
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value={formData.itemCount || '-'}
disabled
className="bg-muted"
/>
</div>
</div>
</section>
{/* 작업지시 정보 */}
<section className="bg-muted/30 border rounded-lg p-6">
<h3 className="font-semibold mb-4"> </h3>
<div className="space-y-4">
<div className="space-y-2">
<Label> *</Label>
<Select
value={formData.processId?.toString() || ''}
onValueChange={(value) => setFormData({ ...formData, processId: parseInt(value) })}
disabled={isLoadingProcesses}
>
<SelectTrigger className="bg-white">
<SelectValue placeholder={isLoadingProcesses ? '로딩 중...' : '공정을 선택하세요'} />
</SelectTrigger>
<SelectContent>
{processOptions.map((process) => (
<SelectItem key={process.id} value={process.id.toString()}>
{process.processName}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
: {getSelectedProcessCode()}
</p>
</div>
<div className="space-y-2">
<Label> *</Label>
<Input
type="date"
value={formData.scheduledDate}
onChange={(e) => setFormData({ ...formData, scheduledDate: e.target.value })}
className="bg-white"
/>
</div>
<div className="space-y-2">
<Label> (1=, 9=)</Label>
<Select
value={formData.priority.toString()}
onValueChange={(value) => setFormData({ ...formData, priority: parseInt(value) })}
>
<SelectTrigger className="bg-white">
<SelectValue />
</SelectTrigger>
<SelectContent>
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map((n) => (
<SelectItem key={n} value={n.toString()}>
{n} {n === 5 ? '(일반)' : n === 1 ? '(긴급)' : ''}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label> ( )</Label>
<div
onClick={() => setIsAssigneeModalOpen(true)}
className="flex min-h-10 w-full cursor-pointer items-center rounded-md border border-input bg-white px-3 py-2 text-sm ring-offset-background hover:bg-accent/50"
>
{assigneeNames.length > 0 ? (
<span>{assigneeNames.join(', ')}</span>
) : (
<span className="text-muted-foreground"> (/)</span>
)}
</div>
</div>
</div>
</section>
{/* 비고 */}
<section className="bg-muted/30 border rounded-lg p-6">
<h3 className="font-semibold mb-4"></h3>
<Textarea
value={formData.note}
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
placeholder="특이사항이나 메모를 입력하세요"
rows={4}
className="bg-white"
/>
</section>
</div>
), [formData, validationErrors, processOptions, isLoadingProcesses, assigneeNames, getSelectedProcessCode]);
return (
<>
<IntegratedDetailTemplate
config={dynamicConfig}
mode="edit"
isLoading={isLoading}
isSubmitting={isSubmitting}
onBack={handleCancel}
onCancel={handleCancel}
onSubmit={handleSubmit}
renderForm={renderFormContent}
/>
{/* 담당자 선택 모달 */}
<AssigneeSelectModal
open={isAssigneeModalOpen}
onOpenChange={setIsAssigneeModalOpen}
selectedIds={formData.assignees}
onSelect={(ids, names) => {
setFormData({ ...formData, assignees: ids });
setAssigneeNames(names);
}}
/>
</>
);
}