diff options
Diffstat (limited to 'lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx')
| -rw-r--r-- | lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx | 382 |
1 files changed, 382 insertions, 0 deletions
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 |
