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:
261
src/components/orders/QuotationSelectDialog.tsx
Normal file
261
src/components/orders/QuotationSelectDialog.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 견적 선택 팝업
|
||||
*
|
||||
* 확정된 견적 목록에서 수주 전환할 견적을 선택하는 다이얼로그
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Search, FileText, Check } from "lucide-react";
|
||||
import { formatAmount } from "@/utils/formatAmount";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// 견적 타입
|
||||
export interface QuotationForSelect {
|
||||
id: string;
|
||||
quoteNumber: string; // KD-PR-XXXXXX-XX
|
||||
grade: string; // A(우량), B(관리), C(주의)
|
||||
client: string; // 발주처
|
||||
siteName: string; // 현장명
|
||||
amount: number; // 총 금액
|
||||
itemCount: number; // 품목 수
|
||||
registrationDate: string; // 견적일
|
||||
manager?: string; // 담당자
|
||||
contact?: string; // 연락처
|
||||
items?: QuotationItem[]; // 품목 내역
|
||||
}
|
||||
|
||||
export interface QuotationItem {
|
||||
id: string;
|
||||
itemCode: string;
|
||||
itemName: string;
|
||||
type: string; // 종
|
||||
symbol: string; // 부호
|
||||
spec: string; // 규격
|
||||
quantity: number;
|
||||
unit: string;
|
||||
unitPrice: number;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
interface QuotationSelectDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSelect: (quotation: QuotationForSelect) => void;
|
||||
selectedId?: string;
|
||||
}
|
||||
|
||||
// 샘플 견적 데이터 (실제 구현에서는 API 연동)
|
||||
const SAMPLE_QUOTATIONS: QuotationForSelect[] = [
|
||||
{
|
||||
id: "QT-001",
|
||||
quoteNumber: "KD-PR-251210-01",
|
||||
grade: "A",
|
||||
client: "태영건설(주)",
|
||||
siteName: "데시앙 동탄 파크뷰",
|
||||
amount: 38800000,
|
||||
itemCount: 5,
|
||||
registrationDate: "2024-12-10",
|
||||
manager: "김철수",
|
||||
contact: "010-1234-5678",
|
||||
items: [
|
||||
{ id: "1", itemCode: "PRD-001", itemName: "국민방화스크린세터", type: "B1", symbol: "FSS1", spec: "7260×2600", quantity: 2, unit: "EA", unitPrice: 8000000, amount: 16000000 },
|
||||
{ id: "2", itemCode: "PRD-002", itemName: "국민방화스크린세터", type: "B1", symbol: "FSS2", spec: "5000×2400", quantity: 3, unit: "EA", unitPrice: 7600000, amount: 22800000 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "QT-002",
|
||||
quoteNumber: "KD-PR-251211-02",
|
||||
grade: "A",
|
||||
client: "현대건설(주)",
|
||||
siteName: "힐스테이트 판교역",
|
||||
amount: 52500000,
|
||||
itemCount: 8,
|
||||
registrationDate: "2024-12-11",
|
||||
manager: "이영희",
|
||||
contact: "010-2345-6789",
|
||||
items: [
|
||||
{ id: "1", itemCode: "PRD-003", itemName: "국민방화스크린세터", type: "B2", symbol: "FSS1", spec: "6000×3000", quantity: 4, unit: "EA", unitPrice: 9500000, amount: 38000000 },
|
||||
{ id: "2", itemCode: "PRD-004", itemName: "국민방화스크린세터", type: "B1", symbol: "FSS2", spec: "4500×2500", quantity: 2, unit: "EA", unitPrice: 7250000, amount: 14500000 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "QT-003",
|
||||
quoteNumber: "KD-PR-251208-03",
|
||||
grade: "B",
|
||||
client: "GS건설(주)",
|
||||
siteName: "자이 강남센터",
|
||||
amount: 45000000,
|
||||
itemCount: 6,
|
||||
registrationDate: "2024-12-08",
|
||||
manager: "박민수",
|
||||
contact: "010-3456-7890",
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
id: "QT-004",
|
||||
quoteNumber: "KD-PR-251205-04",
|
||||
grade: "B",
|
||||
client: "대우건설(주)",
|
||||
siteName: "푸르지오 송도",
|
||||
amount: 28900000,
|
||||
itemCount: 4,
|
||||
registrationDate: "2024-12-05",
|
||||
manager: "최지원",
|
||||
contact: "010-4567-8901",
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
id: "QT-005",
|
||||
quoteNumber: "KD-PR-251201-05",
|
||||
grade: "A",
|
||||
client: "포스코건설",
|
||||
siteName: "더샵 분당센트럴",
|
||||
amount: 62000000,
|
||||
itemCount: 10,
|
||||
registrationDate: "2024-12-01",
|
||||
manager: "정수민",
|
||||
contact: "010-5678-9012",
|
||||
items: [],
|
||||
},
|
||||
];
|
||||
|
||||
// 등급 배지 컴포넌트
|
||||
function GradeBadge({ grade }: { grade: string }) {
|
||||
const config: Record<string, { label: string; className: string }> = {
|
||||
A: { label: "A (우량)", className: "bg-green-100 text-green-700 border-green-200" },
|
||||
B: { label: "B (관리)", className: "bg-yellow-100 text-yellow-700 border-yellow-200" },
|
||||
C: { label: "C (주의)", className: "bg-red-100 text-red-700 border-red-200" },
|
||||
};
|
||||
const cfg = config[grade] || config.B;
|
||||
return (
|
||||
<Badge variant="outline" className={cn("text-xs", cfg.className)}>
|
||||
{cfg.label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
export function QuotationSelectDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSelect,
|
||||
selectedId,
|
||||
}: QuotationSelectDialogProps) {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [quotations] = useState<QuotationForSelect[]>(SAMPLE_QUOTATIONS);
|
||||
|
||||
// 검색 필터링
|
||||
const filteredQuotations = quotations.filter((q) => {
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
return (
|
||||
!searchTerm ||
|
||||
q.quoteNumber.toLowerCase().includes(searchLower) ||
|
||||
q.client.toLowerCase().includes(searchLower) ||
|
||||
q.siteName.toLowerCase().includes(searchLower)
|
||||
);
|
||||
});
|
||||
|
||||
// 다이얼로그 열릴 때 검색어 초기화
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setSearchTerm("");
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleSelect = (quotation: QuotationForSelect) => {
|
||||
onSelect(quotation);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FileText className="h-5 w-5" />
|
||||
견적 선택
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* 검색창 */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="견적번호, 거래처, 현장명 검색..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 안내 문구 */}
|
||||
<div className="text-sm text-muted-foreground">
|
||||
전환 가능한 견적 {filteredQuotations.length}건 (최종확정 상태)
|
||||
</div>
|
||||
|
||||
{/* 견적 목록 */}
|
||||
<div className="flex-1 overflow-y-auto space-y-3 pr-2">
|
||||
{filteredQuotations.map((quotation) => (
|
||||
<div
|
||||
key={quotation.id}
|
||||
onClick={() => handleSelect(quotation)}
|
||||
className={cn(
|
||||
"p-4 border rounded-lg cursor-pointer transition-colors",
|
||||
"hover:bg-muted/50 hover:border-primary/50",
|
||||
selectedId === quotation.id && "border-primary bg-primary/5"
|
||||
)}
|
||||
>
|
||||
{/* 상단: 견적번호 + 등급 */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="text-sm font-mono bg-gray-100 px-2 py-0.5 rounded">
|
||||
{quotation.quoteNumber}
|
||||
</code>
|
||||
<GradeBadge grade={quotation.grade} />
|
||||
</div>
|
||||
{selectedId === quotation.id && (
|
||||
<Check className="h-5 w-5 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 발주처 */}
|
||||
<div className="font-medium text-base mb-1">
|
||||
{quotation.client}
|
||||
</div>
|
||||
|
||||
{/* 현장명 + 금액 */}
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
[{quotation.siteName}]
|
||||
</span>
|
||||
<span className="font-medium text-green-600">
|
||||
{formatAmount(quotation.amount)}원
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 품목 수 */}
|
||||
<div className="text-xs text-muted-foreground mt-1 text-right">
|
||||
{quotation.itemCount}개 품목
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{filteredQuotations.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
검색 결과가 없습니다.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user