feat: 생산/품질/자재/출고/주문 관리 페이지 구현
- 생산관리: 대시보드, 작업지시, 작업실적, 작업자화면 - 품질관리: 검사관리 (리스트/등록/상세) - 자재관리: 입고관리, 재고현황 - 출고관리: 출하관리 (리스트/등록/상세/수정) - 주문관리: 수주관리, 생산의뢰 - 기존 컴포넌트 개선: CardTransactionInquiry, VendorDetail, QuoteRegistration - IntegratedListTemplateV2 개선 - 공통 컴포넌트 분석 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
244
src/components/production/WorkerScreen/MaterialInputModal.tsx
Normal file
244
src/components/production/WorkerScreen/MaterialInputModal.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 자재투입 모달
|
||||
*
|
||||
* - FIFO 순위 표시
|
||||
* - 자재 테이블 (BOM 기준)
|
||||
* - 투입 등록 기능
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Package } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import type { WorkOrder } from '../ProductionDashboard/types';
|
||||
import type { MaterialInput } from './types';
|
||||
|
||||
// Mock 자재 데이터
|
||||
const MOCK_MATERIALS: MaterialInput[] = [
|
||||
{
|
||||
id: '1',
|
||||
materialCode: 'KD-RM-001',
|
||||
materialName: 'SPHC-SD 1.6T',
|
||||
unit: 'KG',
|
||||
currentStock: 500,
|
||||
fifoRank: 1,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
materialCode: 'KD-RM-002',
|
||||
materialName: 'EGI 1.55T',
|
||||
unit: 'KG',
|
||||
currentStock: 350,
|
||||
fifoRank: 2,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
materialCode: 'KD-SM-001',
|
||||
materialName: '볼트 M6x20',
|
||||
unit: 'EA',
|
||||
currentStock: 1200,
|
||||
fifoRank: 3,
|
||||
},
|
||||
];
|
||||
|
||||
interface MaterialInputModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
order: WorkOrder | null;
|
||||
/** 전량완료 흐름에서 사용 - 투입 등록/취소 후 완료 처리 */
|
||||
onComplete?: () => void;
|
||||
/** 전량완료 흐름 여부 (취소 시에도 완료 처리) */
|
||||
isCompletionFlow?: boolean;
|
||||
/** 자재 투입 저장 콜백 */
|
||||
onSaveMaterials?: (orderId: string, materials: MaterialInput[]) => void;
|
||||
/** 이미 투입된 자재 목록 */
|
||||
savedMaterials?: MaterialInput[];
|
||||
}
|
||||
|
||||
export function MaterialInputModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
order,
|
||||
onComplete,
|
||||
isCompletionFlow = false,
|
||||
onSaveMaterials,
|
||||
savedMaterials = [],
|
||||
}: MaterialInputModalProps) {
|
||||
const [selectedMaterials, setSelectedMaterials] = useState<Set<string>>(new Set());
|
||||
const [materials] = useState<MaterialInput[]>(MOCK_MATERIALS);
|
||||
|
||||
// 이미 투입된 자재가 있으면 선택 상태로 초기화
|
||||
const hasSavedMaterials = savedMaterials.length > 0;
|
||||
|
||||
const handleToggleMaterial = (materialId: string) => {
|
||||
setSelectedMaterials((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(materialId)) {
|
||||
next.delete(materialId);
|
||||
} else {
|
||||
next.add(materialId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// 자재 투입 등록
|
||||
const handleSubmit = () => {
|
||||
if (!order) return;
|
||||
|
||||
// 선택된 자재 정보 추출
|
||||
const selectedMaterialList = materials.filter((m) => selectedMaterials.has(m.id));
|
||||
console.log('[자재투입] 저장:', order.id, selectedMaterialList);
|
||||
|
||||
// 자재 저장 콜백
|
||||
if (onSaveMaterials) {
|
||||
onSaveMaterials(order.id, selectedMaterialList);
|
||||
}
|
||||
|
||||
setSelectedMaterials(new Set());
|
||||
onOpenChange(false);
|
||||
|
||||
// 전량완료 흐름이면 완료 처리
|
||||
if (isCompletionFlow && onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
};
|
||||
|
||||
// 건너뛰기 (자재 없이 완료) - 전량완료 흐름에서만 사용
|
||||
const handleSkip = () => {
|
||||
setSelectedMaterials(new Set());
|
||||
onOpenChange(false);
|
||||
// 전량완료 흐름이면 완료 처리
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
};
|
||||
|
||||
// 취소 (모달만 닫기)
|
||||
const handleCancel = () => {
|
||||
setSelectedMaterials(new Set());
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const getFifoRankBadge = (rank: number) => {
|
||||
const colors = {
|
||||
1: 'bg-red-100 text-red-800',
|
||||
2: 'bg-orange-100 text-orange-800',
|
||||
3: 'bg-gray-100 text-gray-800',
|
||||
};
|
||||
const labels = {
|
||||
1: '최우선',
|
||||
2: '차선',
|
||||
3: '대기',
|
||||
};
|
||||
return (
|
||||
<Badge className={colors[rank as 1 | 2 | 3] || colors[3]}>
|
||||
{rank}위 ({labels[rank as 1 | 2 | 3] || labels[3]})
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
if (!order) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Package className="h-5 w-5" />
|
||||
투입자재 등록
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
작업지시 {order.orderNo}에 투입할 자재를 선택하세요.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* FIFO 순위 안내 */}
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span>FIFO 순위:</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Badge className="bg-red-100 text-red-800">1</Badge> 최우선
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Badge className="bg-orange-100 text-orange-800">2</Badge> 차선
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Badge className="bg-gray-100 text-gray-800">3+</Badge> 대기
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 자재 테이블 */}
|
||||
{materials.length === 0 ? (
|
||||
<div className="py-8 text-center text-muted-foreground border rounded-lg">
|
||||
이 공정에 배정된 자재가 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-12">선택</TableHead>
|
||||
<TableHead>자재코드</TableHead>
|
||||
<TableHead>자재명</TableHead>
|
||||
<TableHead>단위</TableHead>
|
||||
<TableHead className="text-right">현재고</TableHead>
|
||||
<TableHead>FIFO</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{materials.map((material) => (
|
||||
<TableRow key={material.id}>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedMaterials.has(material.id)}
|
||||
onCheckedChange={() => handleToggleMaterial(material.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{material.materialCode}</TableCell>
|
||||
<TableCell>{material.materialName}</TableCell>
|
||||
<TableCell>{material.unit}</TableCell>
|
||||
<TableCell className="text-right">{material.currentStock.toLocaleString()}</TableCell>
|
||||
<TableCell>{getFifoRankBadge(material.fifoRank)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex-col sm:flex-row gap-2">
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
취소
|
||||
</Button>
|
||||
{isCompletionFlow && (
|
||||
<Button variant="secondary" onClick={handleSkip}>
|
||||
건너뛰기
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleSubmit} disabled={selectedMaterials.size === 0}>
|
||||
투입 등록
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user