diff options
Diffstat (limited to 'lib/techsales-rfq/vendor-response/detail')
4 files changed, 984 insertions, 0 deletions
diff --git a/lib/techsales-rfq/vendor-response/detail/communication-tab.tsx b/lib/techsales-rfq/vendor-response/detail/communication-tab.tsx new file mode 100644 index 00000000..0332232c --- /dev/null +++ b/lib/techsales-rfq/vendor-response/detail/communication-tab.tsx @@ -0,0 +1,215 @@ +"use client" + +import * as React from "react" +import { useState } from "react" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Separator } from "@/components/ui/separator" +import { Avatar, AvatarFallback } from "@/components/ui/avatar" +import { Send, MessageCircle } from "lucide-react" +import { formatDateTime } from "@/lib/utils" +import { toast } from "sonner" + +interface CommunicationTabProps { + quotation: { + id: number + rfq: { + id: number + rfqCode: string | null + createdByUser?: { + id: number + name: string | null + email: string | null + } | null + } | null + vendor: { + vendorName: string + } | null + } +} + +// 임시 코멘트 데이터 (실제로는 API에서 가져와야 함) +const MOCK_COMMENTS = [ + { + id: 1, + content: "안녕하세요. 해당 자재에 대한 견적 요청 드립니다. 납기일은 언제까지 가능한지 문의드립니다.", + createdAt: new Date("2024-01-15T09:00:00"), + author: { + name: "김구매", + email: "buyer@company.com", + role: "구매담당자" + } + }, + { + id: 2, + content: "안녕하세요. 견적 요청 확인했습니다. 해당 자재의 경우 약 2주 정도의 제작 기간이 필요합니다. 상세한 견적은 내일까지 제출하겠습니다.", + createdAt: new Date("2024-01-15T14:30:00"), + author: { + name: "이벤더", + email: "vendor@supplier.com", + role: "벤더" + } + }, + { + id: 3, + content: "감사합니다. 추가로 품질 인증서도 함께 제출 가능한지 확인 부탁드립니다.", + createdAt: new Date("2024-01-16T10:15:00"), + author: { + name: "김구매", + email: "buyer@company.com", + role: "구매담당자" + } + } +] + +export function CommunicationTab({ quotation }: CommunicationTabProps) { + const [newComment, setNewComment] = useState("") + const [isLoading, setIsLoading] = useState(false) + const [comments, setComments] = useState(MOCK_COMMENTS) + + const handleSendComment = async () => { + if (!newComment.trim()) { + toast.error("메시지를 입력해주세요.") + return + } + + setIsLoading(true) + try { + // TODO: API 호출로 코멘트 전송 + const newCommentData = { + id: comments.length + 1, + content: newComment, + createdAt: new Date(), + author: { + name: "현재사용자", // 실제로는 세션에서 가져와야 함 + email: "current@user.com", + role: "벤더" + } + } + + setComments([...comments, newCommentData]) + setNewComment("") + toast.success("메시지가 전송되었습니다.") + } catch { + toast.error("메시지 전송 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + const getAuthorInitials = (name: string) => { + return name + .split(" ") + .map(word => word[0]) + .join("") + .toUpperCase() + .slice(0, 2) + } + + const getRoleBadgeVariant = (role: string) => { + return role === "구매담당자" ? "default" : "secondary" + } + + return ( + <div className="h-full flex flex-col"> + {/* 헤더 */} + <Card className="mb-4"> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <MessageCircle className="h-5 w-5" /> + 커뮤니케이션 + </CardTitle> + <CardDescription> + RFQ {quotation.rfq?.rfqCode || "미할당"}에 대한 구매담당자와의 커뮤니케이션 + </CardDescription> + </CardHeader> + <CardContent> + <div className="flex items-center gap-4 text-sm text-muted-foreground"> + <span>구매담당자: {quotation.rfq?.createdByUser?.name || "N/A"}</span> + <span>•</span> + <span>벤더: {quotation.vendor?.vendorName}</span> + </div> + </CardContent> + </Card> + + {/* 메시지 목록 */} + <Card className="flex-1 flex flex-col min-h-0"> + <CardHeader> + <CardTitle className="text-lg">메시지 ({comments.length})</CardTitle> + </CardHeader> + <CardContent className="flex-1 flex flex-col min-h-0"> + <ScrollArea className="flex-1 pr-4"> + <div className="space-y-4"> + {comments.length === 0 ? ( + <div className="text-center py-8 text-muted-foreground"> + <MessageCircle className="h-12 w-12 mx-auto mb-4 opacity-50" /> + <p>아직 메시지가 없습니다.</p> + <p className="text-sm">첫 번째 메시지를 보내보세요.</p> + </div> + ) : ( + comments.map((comment) => ( + <div key={comment.id} className="flex gap-3"> + <Avatar className="h-8 w-8 mt-1"> + <AvatarFallback className="text-xs"> + {getAuthorInitials(comment.author.name)} + </AvatarFallback> + </Avatar> + <div className="flex-1 space-y-2"> + <div className="flex items-center gap-2"> + <span className="font-medium text-sm">{comment.author.name}</span> + <Badge variant={getRoleBadgeVariant(comment.author.role)} className="text-xs"> + {comment.author.role} + </Badge> + <span className="text-xs text-muted-foreground"> + {formatDateTime(comment.createdAt)} + </span> + </div> + <div className="bg-muted p-3 rounded-lg text-sm"> + {comment.content} + </div> + </div> + </div> + )) + )} + </div> + </ScrollArea> + + <Separator className="my-4" /> + + {/* 새 메시지 입력 */} + <div className="space-y-3"> + <Textarea + placeholder="메시지를 입력하세요..." + value={newComment} + onChange={(e) => setNewComment(e.target.value)} + rows={3} + className="resize-none" + onKeyDown={(e) => { + if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { + e.preventDefault() + handleSendComment() + } + }} + /> + <div className="flex justify-between items-center"> + <div className="text-xs text-muted-foreground"> + Ctrl + Enter로 빠른 전송 + </div> + <Button + onClick={handleSendComment} + disabled={isLoading || !newComment.trim()} + size="sm" + > + <Send className="h-4 w-4 mr-2" /> + 전송 + </Button> + </div> + </div> + </CardContent> + </Card> + </div> + ) +}
\ No newline at end of file diff --git a/lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx b/lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx new file mode 100644 index 00000000..7ba3320d --- /dev/null +++ b/lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx @@ -0,0 +1,269 @@ +"use client" + +import * as React from "react" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { ScrollArea } from "@/components/ui/scroll-area" +import { formatDateToQuarter, formatDate } from "@/lib/utils" + +interface ProjectSnapshot { + pspid?: string + projNm?: string + projMsrm?: number + kunnr?: string + kunnrNm?: string + cls1?: string + cls1Nm?: string + ptype?: string + ptypeNm?: string + estmPm?: string + scDt?: string + klDt?: string + lcDt?: string + dlDt?: string + dockNo?: string + dockNm?: string + projNo?: string + ownerNm?: string + pspUpdatedAt?: string | Date +} + +interface SeriesSnapshot { + sersNo?: string + klDt?: string +} + +interface ProjectInfoTabProps { + quotation: { + id: number + rfq: { + id: number + rfqCode: string | null + materialCode: string | null + dueDate: Date | null + status: string | null + remark: string | null + projectSnapshot?: ProjectSnapshot | null + seriesSnapshot?: SeriesSnapshot[] | null + item?: { + id: number + itemCode: string | null + itemName: string | null + } | null + biddingProject?: { + id: number + pspid: string | null + projNm: string | null + } | null + createdByUser?: { + id: number + name: string | null + email: string | null + } | null + } | null + vendor: { + id: number + vendorName: string + vendorCode: string | null + } | null + } +} + +export function ProjectInfoTab({ quotation }: ProjectInfoTabProps) { + const rfq = quotation.rfq + const projectSnapshot = rfq?.projectSnapshot + const seriesSnapshot = rfq?.seriesSnapshot + + if (!rfq) { + return ( + <div className="flex items-center justify-center h-full"> + <div className="text-center"> + <h3 className="text-lg font-medium">RFQ 정보를 찾을 수 없습니다</h3> + <p className="text-sm text-muted-foreground mt-1"> + 연결된 RFQ 정보가 없습니다. + </p> + </div> + </div> + ) + } + + return ( + <ScrollArea className="h-full"> + <div className="space-y-6 p-1"> + {/* RFQ 기본 정보 */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + RFQ 기본 정보 + <Badge variant="outline">{rfq.rfqCode || "미할당"}</Badge> + </CardTitle> + <CardDescription> + 요청서 기본 정보 및 자재 정보 + </CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">RFQ 번호</div> + <div className="text-sm">{rfq.rfqCode || "미할당"}</div> + </div> + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">자재 코드</div> + <div className="text-sm">{rfq.materialCode || "N/A"}</div> + </div> + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">품목명</div> + <div className="text-sm">{rfq.item?.itemName || "N/A"}</div> + </div> + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">마감일</div> + <div className="text-sm"> + {rfq.dueDate ? formatDate(rfq.dueDate) : "N/A"} + </div> + </div> + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">RFQ 상태</div> + <div className="text-sm">{rfq.status || "N/A"}</div> + </div> + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">담당자</div> + <div className="text-sm">{rfq.createdByUser?.name || "N/A"}</div> + </div> + </div> + {rfq.remark && ( + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">비고</div> + <div className="text-sm p-3 bg-muted rounded-md">{rfq.remark}</div> + </div> + )} + </CardContent> + </Card> + + {/* 프로젝트 기본 정보 */} + {rfq.biddingProject && ( + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + 프로젝트 기본 정보 + <Badge variant="outline">{rfq.biddingProject.pspid || "N/A"}</Badge> + </CardTitle> + <CardDescription> + 연결된 프로젝트의 기본 정보 + </CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">프로젝트 ID</div> + <div className="text-sm">{rfq.biddingProject.pspid || "N/A"}</div> + </div> + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">프로젝트명</div> + <div className="text-sm">{rfq.biddingProject.projNm || "N/A"}</div> + </div> + </div> + </CardContent> + </Card> + )} + + {/* 프로젝트 스냅샷 정보 */} + {projectSnapshot && ( + <Card> + <CardHeader> + <CardTitle>프로젝트 스냅샷</CardTitle> + <CardDescription> + RFQ 생성 시점의 프로젝트 상세 정보 + </CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> + {projectSnapshot.projNo && ( + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">공사번호</div> + <div className="text-sm">{projectSnapshot.projNo}</div> + </div> + )} + {projectSnapshot.projNm && ( + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">공사명</div> + <div className="text-sm">{projectSnapshot.projNm}</div> + </div> + )} + {projectSnapshot.estmPm && ( + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">견적 PM</div> + <div className="text-sm">{projectSnapshot.estmPm}</div> + </div> + )} + {projectSnapshot.kunnrNm && ( + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">선주</div> + <div className="text-sm">{projectSnapshot.kunnrNm}</div> + </div> + )} + {projectSnapshot.cls1Nm && ( + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">선급</div> + <div className="text-sm">{projectSnapshot.cls1Nm}</div> + </div> + )} + {projectSnapshot.projMsrm && ( + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">척수</div> + <div className="text-sm">{projectSnapshot.projMsrm}</div> + </div> + )} + {projectSnapshot.ptypeNm && ( + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">선종</div> + <div className="text-sm">{projectSnapshot.ptypeNm}</div> + </div> + )} + </div> + </CardContent> + </Card> + )} + + {/* 시리즈 스냅샷 정보 */} + {seriesSnapshot && Array.isArray(seriesSnapshot) && seriesSnapshot.length > 0 && ( + <Card> + <CardHeader> + <CardTitle>시리즈 정보 스냅샷</CardTitle> + <CardDescription> + 프로젝트의 시리즈별 K/L 일정 정보 + </CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + {seriesSnapshot.map((series: SeriesSnapshot, index: number) => ( + <div key={index} className="border rounded-lg p-4 space-y-3"> + <div className="flex items-center gap-2"> + <Badge variant="secondary">시리즈 {series.sersNo || index + 1}</Badge> + </div> + <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> + {series.klDt && ( + <div className="space-y-1"> + <div className="text-xs font-medium text-muted-foreground">K/L</div> + <div className="text-sm">{formatDateToQuarter(series.klDt)}</div> + </div> + )} + </div> + </div> + ))} + </CardContent> + </Card> + )} + + {/* 정보가 없는 경우 */} + {!projectSnapshot && !seriesSnapshot && ( + <Card> + <CardContent className="text-center py-8"> + <div className="text-muted-foreground"> + 추가 프로젝트 상세정보가 없습니다. + </div> + </CardContent> + </Card> + )} + </div> + </ScrollArea> + ) +}
\ No newline at end of file diff --git a/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx b/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx new file mode 100644 index 00000000..3449dcb6 --- /dev/null +++ b/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx @@ -0,0 +1,382 @@ +"use client" + +import * as React from "react" +import { useState } from "react" +import { useRouter } from "next/navigation" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { ScrollArea } from "@/components/ui/scroll-area" +import { CalendarIcon, Save, Send, AlertCircle } from "lucide-react" +import { Calendar } from "@/components/ui/calendar" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { formatDate, cn } from "@/lib/utils" +import { toast } from "sonner" + +interface QuotationResponseTabProps { + quotation: { + id: number + status: string + totalPrice: string | null + currency: string | null + validUntil: Date | null + remark: string | null + rfq: { + id: number + rfqCode: string | null + materialCode: string | null + dueDate: Date | null + status: string | null + item?: { + itemName: string | null + } | null + } | null + vendor: { + vendorName: string + } | null + } +} + +const CURRENCIES = [ + { value: "KRW", label: "KRW (원)" }, + { value: "USD", label: "USD (달러)" }, + { value: "EUR", label: "EUR (유로)" }, + { value: "JPY", label: "JPY (엔)" }, + { value: "CNY", label: "CNY (위안)" }, +] + +export function QuotationResponseTab({ quotation }: QuotationResponseTabProps) { + const [totalPrice, setTotalPrice] = useState(quotation.totalPrice?.toString() || "") + const [currency, setCurrency] = useState(quotation.currency || "KRW") + const [validUntil, setValidUntil] = useState<Date | undefined>( + quotation.validUntil ? new Date(quotation.validUntil) : undefined + ) + const [remark, setRemark] = useState(quotation.remark || "") + const [isLoading, setIsLoading] = useState(false) + const router = useRouter() + + const rfq = quotation.rfq + const isDueDatePassed = rfq?.dueDate ? new Date(rfq.dueDate) < new Date() : false + const canSubmit = quotation.status === "Draft" && !isDueDatePassed + const canEdit = ["Draft", "Revised"].includes(quotation.status) && !isDueDatePassed + + const handleSaveDraft = async () => { + setIsLoading(true) + try { + const { updateTechSalesVendorQuotation } = await import("@/lib/techsales-rfq/service") + + const result = await updateTechSalesVendorQuotation({ + id: quotation.id, + currency, + totalPrice, + validUntil: validUntil!, + remark, + updatedBy: 1 // TODO: 실제 사용자 ID로 변경 + }) + + if (result.error) { + toast.error(result.error) + } else { + toast.success("임시 저장되었습니다.") + // 페이지 새로고침 대신 router.refresh() 사용 + router.refresh() + } + } catch { + toast.error("저장 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + const handleSubmit = async () => { + if (!totalPrice || !currency || !validUntil) { + toast.error("모든 필수 항목을 입력해주세요.") + return + } + + setIsLoading(true) + try { + const { submitTechSalesVendorQuotation } = await import("@/lib/techsales-rfq/service") + + const result = await submitTechSalesVendorQuotation({ + id: quotation.id, + currency, + totalPrice, + validUntil: validUntil!, + remark, + updatedBy: 1 // TODO: 실제 사용자 ID로 변경 + }) + + if (result.error) { + toast.error(result.error) + } else { + toast.success("견적서가 제출되었습니다.") + // 페이지 새로고침 대신 router.refresh() 사용 + router.refresh() + } + } catch { + toast.error("제출 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + const getStatusBadgeVariant = (status: string) => { + switch (status) { + case "Draft": + return "secondary" + case "Submitted": + return "default" + case "Revised": + return "outline" + case "Rejected": + return "destructive" + case "Accepted": + return "success" + default: + return "secondary" + } + } + + const getStatusLabel = (status: string) => { + switch (status) { + case "Draft": + return "초안" + case "Submitted": + return "제출됨" + case "Revised": + return "수정됨" + case "Rejected": + return "반려됨" + case "Accepted": + return "승인됨" + default: + return status + } + } + + return ( + <ScrollArea className="h-full"> + <div className="space-y-6 p-1"> + {/* 견적서 상태 정보 */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + 견적서 상태 + <Badge variant={getStatusBadgeVariant(quotation.status)}> + {getStatusLabel(quotation.status)} + </Badge> + </CardTitle> + <CardDescription> + 현재 견적서 상태 및 마감일 정보 + </CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">견적서 상태</div> + <div className="text-sm">{getStatusLabel(quotation.status)}</div> + </div> + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">RFQ 마감일</div> + <div className="text-sm"> + {rfq?.dueDate ? formatDate(rfq.dueDate) : "N/A"} + </div> + </div> + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">남은 시간</div> + <div className="text-sm"> + {isDueDatePassed ? ( + <span className="text-destructive">마감됨</span> + ) : rfq?.dueDate ? ( + <span className="text-green-600"> + {Math.ceil((new Date(rfq.dueDate).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24))}일 + </span> + ) : ( + "N/A" + )} + </div> + </div> + </div> + + {isDueDatePassed && ( + <Alert> + <AlertCircle className="h-4 w-4" /> + <AlertDescription> + RFQ 마감일이 지났습니다. 견적서를 수정하거나 제출할 수 없습니다. + </AlertDescription> + </Alert> + )} + + {!canEdit && !isDueDatePassed && ( + <Alert> + <AlertCircle className="h-4 w-4" /> + <AlertDescription> + 현재 상태에서는 견적서를 수정할 수 없습니다. + </AlertDescription> + </Alert> + )} + </CardContent> + </Card> + + {/* 견적 응답 폼 */} + <Card> + <CardHeader> + <CardTitle>견적 응답</CardTitle> + <CardDescription> + 총 가격, 통화, 유효기간을 입력해주세요. + </CardDescription> + </CardHeader> + <CardContent className="space-y-6"> + {/* 총 가격 */} + <div className="space-y-2"> + <Label htmlFor="totalPrice"> + 총 가격 <span className="text-destructive">*</span> + </Label> + <Input + id="totalPrice" + type="number" + placeholder="총 가격을 입력하세요" + value={totalPrice} + onChange={(e) => setTotalPrice(e.target.value)} + disabled={!canEdit} + className="text-right" + /> + </div> + + {/* 통화 */} + <div className="space-y-2"> + <Label htmlFor="currency"> + 통화 <span className="text-destructive">*</span> + </Label> + <Select value={currency} onValueChange={setCurrency} disabled={!canEdit}> + <SelectTrigger> + <SelectValue placeholder="통화를 선택하세요" /> + </SelectTrigger> + <SelectContent> + {CURRENCIES.map((curr) => ( + <SelectItem key={curr.value} value={curr.value}> + {curr.label} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 유효기간 */} + <div className="space-y-2"> + <Label> + 견적 유효기간 <span className="text-destructive">*</span> + </Label> + <Popover> + <PopoverTrigger asChild> + <Button + variant="outline" + className={cn( + "w-full justify-start text-left font-normal", + !validUntil && "text-muted-foreground" + )} + disabled={!canEdit} + > + <CalendarIcon className="mr-2 h-4 w-4" /> + {validUntil ? formatDate(validUntil) : "날짜를 선택하세요"} + </Button> + </PopoverTrigger> + <PopoverContent className="w-auto p-0" align="start"> + <Calendar + mode="single" + selected={validUntil} + onSelect={setValidUntil} + disabled={(date) => date < new Date()} + initialFocus + /> + </PopoverContent> + </Popover> + </div> + + {/* 비고 */} + <div className="space-y-2"> + <Label htmlFor="remark">비고</Label> + <Textarea + id="remark" + placeholder="추가 설명이나 조건을 입력하세요" + value={remark} + onChange={(e) => setRemark(e.target.value)} + disabled={!canEdit} + rows={4} + /> + </div> + + {/* 액션 버튼 */} + {canEdit && ( + <div className="flex gap-2 pt-4"> + <Button + variant="outline" + onClick={handleSaveDraft} + disabled={isLoading} + className="flex-1" + > + <Save className="mr-2 h-4 w-4" /> + 임시 저장 + </Button> + {canSubmit && ( + <Button + onClick={handleSubmit} + disabled={isLoading || !totalPrice || !currency || !validUntil} + className="flex-1" + > + <Send className="mr-2 h-4 w-4" /> + 견적서 제출 + </Button> + )} + </div> + )} + </CardContent> + </Card> + + {/* 현재 견적 정보 (읽기 전용) */} + {quotation.totalPrice && ( + <Card> + <CardHeader> + <CardTitle>현재 견적 정보</CardTitle> + <CardDescription> + 저장된 견적 정보 + </CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">총 가격</div> + <div className="text-lg font-semibold"> + {parseFloat(quotation.totalPrice).toLocaleString()} {quotation.currency} + </div> + </div> + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">통화</div> + <div className="text-sm">{quotation.currency}</div> + </div> + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">유효기간</div> + <div className="text-sm"> + {quotation.validUntil ? formatDate(quotation.validUntil) : "N/A"} + </div> + </div> + </div> + {quotation.remark && ( + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">비고</div> + <div className="text-sm p-3 bg-muted rounded-md">{quotation.remark}</div> + </div> + )} + </CardContent> + </Card> + )} + </div> + </ScrollArea> + ) +}
\ No newline at end of file diff --git a/lib/techsales-rfq/vendor-response/detail/quotation-tabs.tsx b/lib/techsales-rfq/vendor-response/detail/quotation-tabs.tsx new file mode 100644 index 00000000..a800dd95 --- /dev/null +++ b/lib/techsales-rfq/vendor-response/detail/quotation-tabs.tsx @@ -0,0 +1,118 @@ +"use client" + +import * as React from "react" +import { useRouter, useSearchParams } from "next/navigation" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { ProjectInfoTab } from "./project-info-tab" +import { QuotationResponseTab } from "./quotation-response-tab" +import { CommunicationTab } from "./communication-tab" + +// 프로젝트 스냅샷 타입 정의 +interface ProjectSnapshot { + scDt?: string + klDt?: string + lcDt?: string + dlDt?: string + dockNo?: string + dockNm?: string + projNo?: string + projNm?: string + ownerNm?: string + kunnrNm?: string + cls1Nm?: string + projMsrm?: number + ptypeNm?: string + sector?: string + estmPm?: string +} + +// 시리즈 스냅샷 타입 정의 +interface SeriesSnapshot { + sersNo?: string + scDt?: string + klDt?: string + lcDt?: string + dlDt?: string + dockNo?: string + dockNm?: string +} + +interface QuotationData { + id: number + status: string + totalPrice: string | null + currency: string | null + validUntil: Date | null + remark: string | null + rfq: { + id: number + rfqCode: string | null + materialCode: string | null + dueDate: Date | null + status: string | null + remark: string | null + projectSnapshot?: ProjectSnapshot | null + seriesSnapshot?: SeriesSnapshot[] | null + item?: { + id: number + itemCode: string | null + itemName: string | null + } | null + biddingProject?: { + id: number + pspid: string | null + projNm: string | null + } | null + createdByUser?: { + id: number + name: string | null + email: string | null + } | null + } | null + vendor: { + id: number + vendorName: string + vendorCode: string | null + } | null +} + +interface TechSalesQuotationTabsProps { + quotation: QuotationData + defaultTab?: string +} + +export function TechSalesQuotationTabs({ quotation, defaultTab = "project" }: TechSalesQuotationTabsProps) { + const router = useRouter() + const searchParams = useSearchParams() + const currentTab = searchParams?.get("tab") || defaultTab + + const handleTabChange = (value: string) => { + const params = new URLSearchParams(searchParams?.toString() || "") + params.set("tab", value) + router.push(`?${params.toString()}`, { scroll: false }) + } + + return ( + <Tabs value={currentTab} onValueChange={handleTabChange} className="h-full flex flex-col"> + <TabsList className="grid w-full grid-cols-3"> + <TabsTrigger value="project">프로젝트 및 RFQ 정보</TabsTrigger> + <TabsTrigger value="quotation">견적 응답</TabsTrigger> + <TabsTrigger value="communication">커뮤니케이션</TabsTrigger> + </TabsList> + + <div className="flex-1 mt-4 overflow-hidden"> + <TabsContent value="project" className="h-full m-0"> + <ProjectInfoTab quotation={quotation} /> + </TabsContent> + + <TabsContent value="quotation" className="h-full m-0"> + <QuotationResponseTab quotation={quotation} /> + </TabsContent> + + <TabsContent value="communication" className="h-full m-0"> + <CommunicationTab quotation={quotation} /> + </TabsContent> + </div> + </Tabs> + ) +}
\ No newline at end of file |
