790 lines
29 KiB
TypeScript
790 lines
29 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* 제품검사 등록 페이지
|
|
*
|
|
* 기획서 기반 전면 재구축:
|
|
* - 기본정보 입력
|
|
* - 건축공사장, 자재유통업자, 공사시공자, 공사감리자 정보
|
|
* - 검사 정보 (검사방문요청일, 기간, 검사자, 현장주소)
|
|
* - 수주 설정 정보 (수주 선택 → 규격 비교 테이블)
|
|
*/
|
|
|
|
import { useState, useCallback, useMemo } from 'react';
|
|
import dynamic from 'next/dynamic';
|
|
import { useRouter } from 'next/navigation';
|
|
import { Plus, Trash2, ClipboardCheck } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Input } from '@/components/ui/input';
|
|
import { DatePicker } from '@/components/ui/date-picker';
|
|
import { Label } from '@/components/ui/label';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import {
|
|
Accordion,
|
|
AccordionContent,
|
|
AccordionItem,
|
|
AccordionTrigger,
|
|
} from '@/components/ui/accordion';
|
|
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
|
import { qualityInspectionCreateConfig } from './inspectionConfig';
|
|
import { toast } from 'sonner';
|
|
import { createInspection } from './actions';
|
|
import { calculateOrderSummary } from './mockData';
|
|
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
|
|
|
const OrderSelectModal = dynamic(
|
|
() => import('./OrderSelectModal').then(mod => ({ default: mod.OrderSelectModal })),
|
|
);
|
|
const ProductInspectionInputModal = dynamic(
|
|
() => import('./ProductInspectionInputModal').then(mod => ({ default: mod.ProductInspectionInputModal })),
|
|
);
|
|
const SupplierSearchModal = dynamic(
|
|
() => import('@/components/material/ReceivingManagement/SupplierSearchModal').then(mod => ({ default: mod.SupplierSearchModal })),
|
|
);
|
|
import type { InspectionFormData, OrderSettingItem, OrderSelectItem, OrderGroup, ProductInspectionData } from './types';
|
|
import {
|
|
emptyConstructionSite,
|
|
emptyMaterialDistributor,
|
|
emptyConstructor,
|
|
emptySupervisor,
|
|
emptyScheduleInfo,
|
|
} from './mockData';
|
|
|
|
export function InspectionCreate() {
|
|
const router = useRouter();
|
|
|
|
// 폼 상태
|
|
const [formData, setFormData] = useState<InspectionFormData>({
|
|
qualityDocNumber: '',
|
|
siteName: '',
|
|
client: '',
|
|
manager: '',
|
|
managerContact: '',
|
|
receptionDate: new Date().toISOString().slice(0, 10),
|
|
constructionSite: { ...emptyConstructionSite },
|
|
materialDistributor: { ...emptyMaterialDistributor },
|
|
constructorInfo: { ...emptyConstructor },
|
|
supervisor: { ...emptySupervisor },
|
|
scheduleInfo: { ...emptyScheduleInfo },
|
|
orderItems: [],
|
|
});
|
|
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [orderModalOpen, setOrderModalOpen] = useState(false);
|
|
const [clientModalOpen, setClientModalOpen] = useState(false);
|
|
|
|
// 제품검사 입력 모달
|
|
const [inspectionInputOpen, setInspectionInputOpen] = useState(false);
|
|
const [selectedOrderItem, setSelectedOrderItem] = useState<OrderSettingItem | null>(null);
|
|
|
|
// ===== 수주 선택 처리 =====
|
|
const handleOrderSelect = useCallback((items: OrderSelectItem[]) => {
|
|
const newOrderItems: OrderSettingItem[] = items.flatMap((item) =>
|
|
item.locations.length > 0
|
|
? item.locations.map((loc) => ({
|
|
id: `${item.id}-${loc.nodeId}`,
|
|
orderId: Number(item.id),
|
|
orderNumber: item.orderNumber,
|
|
siteName: item.siteName,
|
|
clientId: item.clientId,
|
|
clientName: item.clientName,
|
|
itemId: item.itemId,
|
|
itemName: item.itemName,
|
|
deliveryDate: item.deliveryDate,
|
|
floor: loc.floor,
|
|
symbol: loc.symbol,
|
|
orderWidth: loc.orderWidth,
|
|
orderHeight: loc.orderHeight,
|
|
constructionWidth: 0,
|
|
constructionHeight: 0,
|
|
changeReason: '',
|
|
}))
|
|
: [{
|
|
id: item.id,
|
|
orderId: Number(item.id),
|
|
orderNumber: item.orderNumber,
|
|
siteName: item.siteName,
|
|
clientId: item.clientId,
|
|
clientName: item.clientName,
|
|
itemId: item.itemId,
|
|
itemName: item.itemName,
|
|
deliveryDate: item.deliveryDate,
|
|
floor: '',
|
|
symbol: '',
|
|
orderWidth: 0,
|
|
orderHeight: 0,
|
|
constructionWidth: 0,
|
|
constructionHeight: 0,
|
|
changeReason: '',
|
|
}]
|
|
);
|
|
setFormData((prev) => {
|
|
const updated = { ...prev, orderItems: [...prev.orderItems, ...newOrderItems] };
|
|
// 수주처 미선택 상태에서 수주 선택 시 → 수주처 자동 채움
|
|
if (!prev.clientId && items.length > 0 && items[0].clientId) {
|
|
updated.clientId = items[0].clientId ?? undefined;
|
|
updated.client = items[0].clientName || '';
|
|
}
|
|
return updated;
|
|
});
|
|
}, []);
|
|
|
|
// ===== 수주 항목 삭제 =====
|
|
const handleRemoveOrderItem = useCallback((itemId: string) => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
orderItems: prev.orderItems.filter((item) => item.id !== itemId),
|
|
}));
|
|
}, []);
|
|
|
|
// ===== 폼 필드 변경 헬퍼 =====
|
|
const updateField = useCallback(<K extends keyof InspectionFormData>(
|
|
key: K,
|
|
value: InspectionFormData[K]
|
|
) => {
|
|
setFormData((prev) => ({ ...prev, [key]: value }));
|
|
}, []);
|
|
|
|
const updateNested = useCallback((
|
|
section: 'constructionSite' | 'materialDistributor' | 'constructorInfo' | 'supervisor' | 'scheduleInfo',
|
|
field: string,
|
|
value: string
|
|
) => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
[section]: {
|
|
...(prev[section] as unknown as Record<string, unknown>),
|
|
[field]: value,
|
|
},
|
|
}));
|
|
}, []);
|
|
|
|
// ===== 수주 설정 요약 =====
|
|
const orderSummary = useMemo(
|
|
() => calculateOrderSummary(formData.orderItems),
|
|
[formData.orderItems]
|
|
);
|
|
|
|
// ===== 수주 항목을 그룹별로 묶기 (아코디언용) =====
|
|
const groupOrderItems = useCallback((items: OrderSettingItem[]): OrderGroup[] => {
|
|
const groups: Record<string, OrderGroup> = {};
|
|
items.forEach((item) => {
|
|
const key = item.orderNumber;
|
|
if (!groups[key]) {
|
|
groups[key] = {
|
|
orderNumber: item.orderNumber,
|
|
siteName: item.siteName,
|
|
deliveryDate: item.deliveryDate,
|
|
locationCount: 0,
|
|
items: [],
|
|
};
|
|
}
|
|
groups[key].items.push(item);
|
|
groups[key].locationCount = groups[key].items.length;
|
|
});
|
|
return Object.values(groups);
|
|
}, []);
|
|
|
|
const orderGroups = useMemo(
|
|
() => groupOrderItems(formData.orderItems),
|
|
[formData.orderItems, groupOrderItems]
|
|
);
|
|
|
|
// ===== 제품검사 입력 핸들러 =====
|
|
const handleOpenInspectionInput = useCallback((item: OrderSettingItem) => {
|
|
setSelectedOrderItem(item);
|
|
setInspectionInputOpen(true);
|
|
}, []);
|
|
|
|
const handleInspectionComplete = useCallback((data: ProductInspectionData) => {
|
|
if (!selectedOrderItem) return;
|
|
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
orderItems: prev.orderItems.map((item) =>
|
|
item.id === selectedOrderItem.id
|
|
? { ...item, inspectionData: data }
|
|
: item
|
|
),
|
|
}));
|
|
|
|
toast.success('검사 데이터가 저장되었습니다.');
|
|
setSelectedOrderItem(null);
|
|
}, [selectedOrderItem]);
|
|
|
|
// ===== 시공규격/변경사유 수정 핸들러 =====
|
|
const handleUpdateOrderItemField = useCallback((
|
|
itemId: string,
|
|
field: 'constructionWidth' | 'constructionHeight' | 'changeReason',
|
|
value: string | number
|
|
) => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
orderItems: prev.orderItems.map((item) =>
|
|
item.id === itemId ? { ...item, [field]: value } : item
|
|
),
|
|
}));
|
|
}, []);
|
|
|
|
// ===== 취소 =====
|
|
const handleCancel = useCallback(() => {
|
|
router.push('/quality/inspections');
|
|
}, [router]);
|
|
|
|
// ===== 등록 제출 =====
|
|
const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
|
|
// 필수 필드 검증
|
|
if (!formData.siteName.trim()) {
|
|
toast.error('현장명은 필수 입력 항목입니다.');
|
|
return { success: false, error: '현장명을 입력해주세요.' };
|
|
}
|
|
if (!formData.clientId) {
|
|
toast.error('수주처는 필수 선택 항목입니다.');
|
|
return { success: false, error: '수주처를 선택해주세요.' };
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
try {
|
|
const result = await createInspection(formData);
|
|
if (result.success) {
|
|
toast.success('제품검사가 등록되었습니다.');
|
|
router.push('/quality/inspections');
|
|
return { success: true };
|
|
}
|
|
return { success: false, error: result.error || '등록에 실패했습니다.' };
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
return { success: false, error: '등록 중 오류가 발생했습니다.' };
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}, [formData, router]);
|
|
|
|
// ===== 수주 설정 아코디언 =====
|
|
const renderOrderAccordion = (groups: OrderGroup[]) => {
|
|
if (groups.length === 0) {
|
|
return (
|
|
<div className="text-center text-muted-foreground py-8">
|
|
수주를 선택해주세요.
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Accordion type="multiple" className="w-full">
|
|
{groups.map((group) => (
|
|
<AccordionItem key={group.orderNumber} value={group.orderNumber} className="border rounded-lg mb-2">
|
|
{/* 상위 레벨: 수주번호, 현장명, 납품일, 개소, 삭제 */}
|
|
<div className="flex items-center">
|
|
<AccordionTrigger className="px-4 py-3 hover:no-underline flex-1">
|
|
<div className="flex items-center gap-6 text-sm w-full">
|
|
<span className="font-medium w-32">{group.orderNumber}</span>
|
|
<span className="text-muted-foreground flex-1">{group.siteName}</span>
|
|
<span className="text-muted-foreground w-28">{group.deliveryDate}</span>
|
|
<span className="text-muted-foreground w-16">{group.locationCount}개소</span>
|
|
</div>
|
|
</AccordionTrigger>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-7 w-7 mr-4 text-muted-foreground hover:text-red-600"
|
|
type="button"
|
|
onClick={() => {
|
|
group.items.forEach((item) => handleRemoveOrderItem(item.id));
|
|
}}
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
<AccordionContent className="px-4 pb-4">
|
|
{/* 하위 레벨: 테이블 */}
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-12 text-center">No.</TableHead>
|
|
<TableHead>층수</TableHead>
|
|
<TableHead>부호</TableHead>
|
|
<TableHead className="text-center">수주 가로</TableHead>
|
|
<TableHead className="text-center">수주 세로</TableHead>
|
|
<TableHead className="text-center w-24">시공 가로</TableHead>
|
|
<TableHead className="text-center w-24">시공 세로</TableHead>
|
|
<TableHead className="w-40">변경사유</TableHead>
|
|
<TableHead className="w-24 text-center">검사</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{group.items.map((item, index) => (
|
|
<TableRow key={item.id}>
|
|
<TableCell className="text-center">{index + 1}</TableCell>
|
|
<TableCell>{item.floor || '-'}</TableCell>
|
|
<TableCell>{item.symbol || '-'}</TableCell>
|
|
<TableCell className="text-center">{item.orderWidth}</TableCell>
|
|
<TableCell className="text-center">{item.orderHeight}</TableCell>
|
|
<TableCell className="text-center">
|
|
<Input
|
|
type="number"
|
|
value={item.constructionWidth || ''}
|
|
onChange={(e) =>
|
|
handleUpdateOrderItemField(item.id, 'constructionWidth', Number(e.target.value))
|
|
}
|
|
className="h-8 w-20 text-center"
|
|
/>
|
|
</TableCell>
|
|
<TableCell className="text-center">
|
|
<Input
|
|
type="number"
|
|
value={item.constructionHeight || ''}
|
|
onChange={(e) =>
|
|
handleUpdateOrderItemField(item.id, 'constructionHeight', Number(e.target.value))
|
|
}
|
|
className="h-8 w-20 text-center"
|
|
/>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Input
|
|
value={item.changeReason || ''}
|
|
onChange={(e) =>
|
|
handleUpdateOrderItemField(item.id, 'changeReason', e.target.value)
|
|
}
|
|
className="h-8"
|
|
placeholder="변경사유 입력"
|
|
/>
|
|
</TableCell>
|
|
<TableCell className="text-center">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleOpenInspectionInput(item)}
|
|
className="h-7"
|
|
>
|
|
<ClipboardCheck className="w-3.5 h-3.5 mr-1" />
|
|
검사하기
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
))}
|
|
</Accordion>
|
|
);
|
|
};
|
|
|
|
// ===== 폼 렌더링 =====
|
|
const renderFormContent = useCallback(() => (
|
|
<div className="space-y-6">
|
|
{/* 기본 정보 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">기본 정보</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>품질관리서 번호</Label>
|
|
<Input
|
|
value={formData.qualityDocNumber}
|
|
onChange={(e) => updateField('qualityDocNumber', e.target.value)}
|
|
placeholder="품질관리서 번호 입력"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>현장명 <span className="text-red-500">*</span></Label>
|
|
<Input
|
|
value={formData.siteName}
|
|
onChange={(e) => updateField('siteName', e.target.value)}
|
|
placeholder="현장명 입력"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>수주처 <span className="text-red-500">*</span></Label>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
value={formData.client}
|
|
readOnly
|
|
placeholder="거래처를 선택하세요"
|
|
className="cursor-pointer bg-muted/30"
|
|
onClick={() => setClientModalOpen(true)}
|
|
/>
|
|
{formData.clientId && (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-9 w-9 shrink-0"
|
|
onClick={() => {
|
|
updateField('client', '');
|
|
updateField('clientId', undefined);
|
|
}}
|
|
>
|
|
<Trash2 className="w-4 h-4 text-muted-foreground" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>담당자</Label>
|
|
<Input
|
|
value={formData.manager}
|
|
onChange={(e) => updateField('manager', e.target.value)}
|
|
placeholder="담당자 입력"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>담당자 연락처</Label>
|
|
<Input
|
|
value={formData.managerContact}
|
|
onChange={(e) => updateField('managerContact', e.target.value)}
|
|
placeholder="담당자 연락처 입력"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 건축공사장 정보 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">건축공사장 정보</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>현장명</Label>
|
|
<Input
|
|
value={formData.constructionSite.siteName}
|
|
onChange={(e) => updateNested('constructionSite', 'siteName', e.target.value)}
|
|
placeholder="현장명 입력"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>대지위치</Label>
|
|
<Input
|
|
value={formData.constructionSite.landLocation}
|
|
onChange={(e) => updateNested('constructionSite', 'landLocation', e.target.value)}
|
|
placeholder="대지위치 입력"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>지번</Label>
|
|
<Input
|
|
value={formData.constructionSite.lotNumber}
|
|
onChange={(e) => updateNested('constructionSite', 'lotNumber', e.target.value)}
|
|
placeholder="지번 입력"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 자재유통업자 정보 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">자재유통업자 정보</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>회사명</Label>
|
|
<Input
|
|
value={formData.materialDistributor.companyName}
|
|
onChange={(e) => updateNested('materialDistributor', 'companyName', e.target.value)}
|
|
placeholder="회사명 입력"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>회사주소</Label>
|
|
<Input
|
|
value={formData.materialDistributor.companyAddress}
|
|
onChange={(e) => updateNested('materialDistributor', 'companyAddress', e.target.value)}
|
|
placeholder="회사주소 입력"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>대표자명</Label>
|
|
<Input
|
|
value={formData.materialDistributor.representativeName}
|
|
onChange={(e) => updateNested('materialDistributor', 'representativeName', e.target.value)}
|
|
placeholder="대표자명 입력"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>전화번호</Label>
|
|
<Input
|
|
value={formData.materialDistributor.phone}
|
|
onChange={(e) => updateNested('materialDistributor', 'phone', e.target.value)}
|
|
placeholder="전화번호 입력"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 공사시공자 정보 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">공사시공자 정보</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>회사명</Label>
|
|
<Input
|
|
value={formData.constructorInfo.companyName}
|
|
onChange={(e) => updateNested('constructorInfo', 'companyName', e.target.value)}
|
|
placeholder="회사명 입력"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>회사주소</Label>
|
|
<Input
|
|
value={formData.constructorInfo.companyAddress}
|
|
onChange={(e) => updateNested('constructorInfo', 'companyAddress', e.target.value)}
|
|
placeholder="회사주소 입력"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>성명</Label>
|
|
<Input
|
|
value={formData.constructorInfo.name}
|
|
onChange={(e) => updateNested('constructorInfo', 'name', e.target.value)}
|
|
placeholder="성명 입력"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>전화번호</Label>
|
|
<Input
|
|
value={formData.constructorInfo.phone}
|
|
onChange={(e) => updateNested('constructorInfo', 'phone', e.target.value)}
|
|
placeholder="전화번호 입력"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 공사감리자 정보 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">공사감리자 정보</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>사무소명</Label>
|
|
<Input
|
|
value={formData.supervisor.officeName}
|
|
onChange={(e) => updateNested('supervisor', 'officeName', e.target.value)}
|
|
placeholder="사무소명 입력"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>사무소주소</Label>
|
|
<Input
|
|
value={formData.supervisor.officeAddress}
|
|
onChange={(e) => updateNested('supervisor', 'officeAddress', e.target.value)}
|
|
placeholder="사무소주소 입력"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>성명</Label>
|
|
<Input
|
|
value={formData.supervisor.name}
|
|
onChange={(e) => updateNested('supervisor', 'name', e.target.value)}
|
|
placeholder="성명 입력"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>전화번호</Label>
|
|
<Input
|
|
value={formData.supervisor.phone}
|
|
onChange={(e) => updateNested('supervisor', 'phone', e.target.value)}
|
|
placeholder="전화번호 입력"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 검사 정보 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">검사 정보</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>검사방문요청일</Label>
|
|
<DatePicker
|
|
value={formData.scheduleInfo.visitRequestDate}
|
|
onChange={(date) => updateNested('scheduleInfo', 'visitRequestDate', date)}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>검사시작일</Label>
|
|
<DatePicker
|
|
value={formData.scheduleInfo.startDate}
|
|
onChange={(date) => updateNested('scheduleInfo', 'startDate', date)}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>검사종료일</Label>
|
|
<DatePicker
|
|
value={formData.scheduleInfo.endDate}
|
|
onChange={(date) => updateNested('scheduleInfo', 'endDate', date)}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>검사자</Label>
|
|
<Input
|
|
value={formData.scheduleInfo.inspector}
|
|
onChange={(e) => updateNested('scheduleInfo', 'inspector', e.target.value)}
|
|
placeholder="검사자 입력"
|
|
/>
|
|
</div>
|
|
</div>
|
|
{/* 현장 주소 */}
|
|
<div className="mt-4 grid grid-cols-4 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>우편번호</Label>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
value={formData.scheduleInfo.sitePostalCode}
|
|
onChange={(e) => updateNested('scheduleInfo', 'sitePostalCode', e.target.value)}
|
|
className="w-28"
|
|
/>
|
|
<Button variant="outline" size="sm" type="button">
|
|
우편번호 찾기
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2 col-span-2">
|
|
<Label>주소</Label>
|
|
<Input
|
|
value={formData.scheduleInfo.siteAddress}
|
|
onChange={(e) => updateNested('scheduleInfo', 'siteAddress', e.target.value)}
|
|
placeholder="주소 입력"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>상세주소</Label>
|
|
<Input
|
|
value={formData.scheduleInfo.siteAddressDetail}
|
|
onChange={(e) => updateNested('scheduleInfo', 'siteAddressDetail', e.target.value)}
|
|
placeholder="상세주소 입력"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 수주 설정 정보 */}
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<CardTitle className="text-base">수주 설정 정보</CardTitle>
|
|
<Button variant="outline" size="sm" type="button" onClick={() => setOrderModalOpen(true)}>
|
|
<Plus className="w-4 h-4 mr-1" />
|
|
수주 선택
|
|
</Button>
|
|
</div>
|
|
<div className="flex items-center gap-3 text-sm">
|
|
<span>전체: <strong>{orderSummary.total}</strong>건</span>
|
|
<span className="text-green-600">일치: <strong>{orderSummary.same}</strong>건</span>
|
|
<span className="text-red-600">불일치: <strong>{orderSummary.changed}</strong>건</span>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="p-4">
|
|
{renderOrderAccordion(orderGroups)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
), [formData, orderSummary, orderGroups, updateField, updateNested, handleRemoveOrderItem, handleOpenInspectionInput, handleUpdateOrderItemField, orderModalOpen]);
|
|
|
|
// 이미 선택된 수주 ID 목록 (orderId 기준, 중복 제거)
|
|
const excludeOrderIds = useMemo(
|
|
() => [...new Set(formData.orderItems.map((item) => String(item.orderId ?? item.id)))],
|
|
[formData.orderItems]
|
|
);
|
|
|
|
// 수주 선택 필터: 기본정보 수주처 또는 이미 선택된 수주 기준
|
|
const orderFilter = useMemo(() => {
|
|
// 기본정보에서 수주처가 선택된 경우 → 해당 거래처 필터
|
|
if (formData.clientId) {
|
|
const firstItem = formData.orderItems[0];
|
|
return {
|
|
clientId: formData.clientId,
|
|
itemId: firstItem?.itemId ?? undefined,
|
|
label: [formData.client, firstItem?.itemName].filter(Boolean).join(' / ') || undefined,
|
|
};
|
|
}
|
|
// 수주가 선택된 경우 → 첫 수주 기준 필터
|
|
if (formData.orderItems.length > 0) {
|
|
const first = formData.orderItems[0];
|
|
return {
|
|
clientId: first.clientId ?? undefined,
|
|
itemId: first.itemId ?? undefined,
|
|
label: [first.clientName, first.itemName].filter(Boolean).join(' / ') || undefined,
|
|
};
|
|
}
|
|
return { clientId: undefined, itemId: undefined, label: undefined };
|
|
}, [formData.clientId, formData.client, formData.orderItems]);
|
|
|
|
return (
|
|
<>
|
|
<IntegratedDetailTemplate
|
|
config={qualityInspectionCreateConfig}
|
|
mode="create"
|
|
isLoading={false}
|
|
isSubmitting={isSubmitting}
|
|
onBack={handleCancel}
|
|
onCancel={handleCancel}
|
|
onSubmit={handleSubmit}
|
|
renderForm={renderFormContent}
|
|
/>
|
|
|
|
<OrderSelectModal
|
|
open={orderModalOpen}
|
|
onOpenChange={setOrderModalOpen}
|
|
onSelect={handleOrderSelect}
|
|
excludeIds={excludeOrderIds}
|
|
filterClientId={orderFilter.clientId}
|
|
filterItemId={orderFilter.itemId}
|
|
filterLabel={orderFilter.label}
|
|
/>
|
|
|
|
{/* 거래처(수주처) 검색 모달 */}
|
|
<SupplierSearchModal
|
|
open={clientModalOpen}
|
|
onOpenChange={setClientModalOpen}
|
|
onSelectSupplier={(supplier) => {
|
|
updateField('clientId', Number(supplier.id));
|
|
updateField('client', supplier.name);
|
|
setClientModalOpen(false);
|
|
}}
|
|
/>
|
|
|
|
{/* 제품검사 입력 모달 */}
|
|
<ProductInspectionInputModal
|
|
open={inspectionInputOpen}
|
|
onOpenChange={setInspectionInputOpen}
|
|
orderItemId={selectedOrderItem?.id || ''}
|
|
productName="방화셔터"
|
|
specification={selectedOrderItem ? `${selectedOrderItem.orderWidth}x${selectedOrderItem.orderHeight}` : ''}
|
|
initialData={selectedOrderItem?.inspectionData}
|
|
onComplete={handleInspectionComplete}
|
|
/>
|
|
</>
|
|
);
|
|
}
|