diff options
Diffstat (limited to 'components')
| -rw-r--r-- | components/bidding/bidding-conditions-edit.tsx | 304 | ||||
| -rw-r--r-- | components/bidding/bidding-info-header.tsx | 149 | ||||
| -rw-r--r-- | components/bidding/price-adjustment-dialog.tsx | 200 |
3 files changed, 653 insertions, 0 deletions
diff --git a/components/bidding/bidding-conditions-edit.tsx b/components/bidding/bidding-conditions-edit.tsx new file mode 100644 index 00000000..a78bb0e0 --- /dev/null +++ b/components/bidding/bidding-conditions-edit.tsx @@ -0,0 +1,304 @@ +"use client" + +import * as React from "react" +import { useRouter } from "next/navigation" +import { useTransition } from "react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Label } from "@/components/ui/label" +import { Switch } from "@/components/ui/switch" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Pencil, Save, X } from "lucide-react" +import { getBiddingConditions, updateBiddingConditions } from "@/lib/bidding/service" +import { useToast } from "@/hooks/use-toast" + +interface BiddingConditionsEditProps { + biddingId: number + initialConditions?: any | null +} + +export function BiddingConditionsEdit({ biddingId, initialConditions }: BiddingConditionsEditProps) { + const router = useRouter() + const { toast } = useToast() + const [isPending, startTransition] = useTransition() + const [isEditing, setIsEditing] = React.useState(false) + const [conditions, setConditions] = React.useState({ + paymentTerms: initialConditions?.paymentTerms || "", + taxConditions: initialConditions?.taxConditions || "", + incoterms: initialConditions?.incoterms || "", + contractDeliveryDate: initialConditions?.contractDeliveryDate + ? new Date(initialConditions.contractDeliveryDate).toISOString().split('T')[0] + : "", + shippingPort: initialConditions?.shippingPort || "", + destinationPort: initialConditions?.destinationPort || "", + isPriceAdjustmentApplicable: initialConditions?.isPriceAdjustmentApplicable || false, + sparePartOptions: initialConditions?.sparePartOptions || "", + }) + + const handleSave = () => { + startTransition(async () => { + try { + const result = await updateBiddingConditions(biddingId, conditions) + + if (result.success) { + toast({ + title: "성공", + description: result.message, + }) + setIsEditing(false) + router.refresh() + } else { + toast({ + title: "오류", + description: result.error, + variant: "destructive", + }) + } + } catch (error) { + console.error('Error updating bidding conditions:', error) + toast({ + title: "오류", + description: "입찰 조건 업데이트 중 오류가 발생했습니다.", + variant: "destructive", + }) + } + }) + } + + const handleCancel = () => { + setConditions({ + paymentTerms: initialConditions?.paymentTerms || "", + taxConditions: initialConditions?.taxConditions || "", + incoterms: initialConditions?.incoterms || "", + contractDeliveryDate: initialConditions?.contractDeliveryDate + ? new Date(initialConditions.contractDeliveryDate).toISOString().split('T')[0] + : "", + shippingPort: initialConditions?.shippingPort || "", + destinationPort: initialConditions?.destinationPort || "", + isPriceAdjustmentApplicable: initialConditions?.isPriceAdjustmentApplicable || false, + sparePartOptions: initialConditions?.sparePartOptions || "", + }) + setIsEditing(false) + } + + if (!isEditing) { + return ( + <Card className="mt-6"> + <CardHeader className="flex flex-row items-center justify-between"> + <CardTitle>입찰 조건</CardTitle> + <Button + variant="outline" + size="sm" + onClick={() => setIsEditing(true)} + className="flex items-center gap-2" + > + <Pencil className="w-4 h-4" /> + 수정 + </Button> + </CardHeader> + <CardContent> + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 text-sm"> + <div> + <Label className="text-muted-foreground">지급조건</Label> + <p className="font-medium">{conditions.paymentTerms || "미설정"}</p> + </div> + <div> + <Label className="text-muted-foreground">세금조건</Label> + <p className="font-medium">{conditions.taxConditions || "미설정"}</p> + </div> + <div> + <Label className="text-muted-foreground">운송조건</Label> + <p className="font-medium">{conditions.incoterms || "미설정"}</p> + </div> + <div> + <Label className="text-muted-foreground">계약 납품일</Label> + <p className="font-medium"> + {conditions.contractDeliveryDate + ? new Date(conditions.contractDeliveryDate).toLocaleDateString('ko-KR') + : "미설정" + } + </p> + </div> + <div> + <Label className="text-muted-foreground">선적지</Label> + <p className="font-medium">{conditions.shippingPort || "미설정"}</p> + </div> + <div> + <Label className="text-muted-foreground">도착지</Label> + <p className="font-medium">{conditions.destinationPort || "미설정"}</p> + </div> + <div> + <Label className="text-muted-foreground">연동제 적용</Label> + <p className="font-medium">{conditions.isPriceAdjustmentApplicable ? "적용 가능" : "적용 불가"}</p> + </div> + {conditions.sparePartOptions && ( + <div className="col-span-full"> + <Label className="text-muted-foreground">스페어파트 옵션</Label> + <p className="font-medium">{conditions.sparePartOptions}</p> + </div> + )} + </div> + </CardContent> + </Card> + ) + } + + return ( + <Card className="mt-6"> + <CardHeader className="flex flex-row items-center justify-between"> + <CardTitle>입찰 조건 수정</CardTitle> + <div className="flex items-center gap-2"> + <Button + variant="outline" + size="sm" + onClick={handleCancel} + disabled={isPending} + className="flex items-center gap-2" + > + <X className="w-4 h-4" /> + 취소 + </Button> + <Button + size="sm" + onClick={handleSave} + disabled={isPending} + className="flex items-center gap-2" + > + <Save className="w-4 h-4" /> + 저장 + </Button> + </div> + </CardHeader> + <CardContent className="space-y-6"> + <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> + <div className="space-y-2"> + <Label htmlFor="paymentTerms">지급조건 *</Label> + <Input + id="paymentTerms" + placeholder="예: 월말결제, 60일" + value={conditions.paymentTerms} + onChange={(e) => setConditions(prev => ({ + ...prev, + paymentTerms: e.target.value + }))} + /> + </div> + + <div className="space-y-2"> + <Label htmlFor="taxConditions">세금조건 *</Label> + <Input + id="taxConditions" + placeholder="예: VAT 별도, 원천세 3.3%" + value={conditions.taxConditions} + onChange={(e) => setConditions(prev => ({ + ...prev, + taxConditions: e.target.value + }))} + /> + </div> + + <div className="space-y-2"> + <Label htmlFor="incoterms">운송조건(인코텀즈) *</Label> + <Select + value={conditions.incoterms} + onValueChange={(value) => setConditions(prev => ({ + ...prev, + incoterms: value + }))} + > + <SelectTrigger> + <SelectValue placeholder="인코텀즈 선택" /> + </SelectTrigger> + <SelectContent> + <SelectItem value="EXW">EXW (Ex Works)</SelectItem> + <SelectItem value="FCA">FCA (Free Carrier)</SelectItem> + <SelectItem value="CPT">CPT (Carriage Paid To)</SelectItem> + <SelectItem value="CIP">CIP (Carriage and Insurance Paid to)</SelectItem> + <SelectItem value="DAP">DAP (Delivered at Place)</SelectItem> + <SelectItem value="DPU">DPU (Delivered at Place Unloaded)</SelectItem> + <SelectItem value="DDP">DDP (Delivered Duty Paid)</SelectItem> + <SelectItem value="FAS">FAS (Free Alongside Ship)</SelectItem> + <SelectItem value="FOB">FOB (Free on Board)</SelectItem> + <SelectItem value="CFR">CFR (Cost and Freight)</SelectItem> + <SelectItem value="CIF">CIF (Cost, Insurance, and Freight)</SelectItem> + </SelectContent> + </Select> + </div> + + <div className="space-y-2"> + <Label htmlFor="contractDeliveryDate">계약 납품일</Label> + <Input + id="contractDeliveryDate" + type="date" + value={conditions.contractDeliveryDate} + onChange={(e) => setConditions(prev => ({ + ...prev, + contractDeliveryDate: e.target.value + }))} + /> + </div> + + <div className="space-y-2"> + <Label htmlFor="shippingPort">선적지</Label> + <Input + id="shippingPort" + placeholder="예: 부산항, 인천항" + value={conditions.shippingPort} + onChange={(e) => setConditions(prev => ({ + ...prev, + shippingPort: e.target.value + }))} + /> + </div> + + <div className="space-y-2"> + <Label htmlFor="destinationPort">도착지</Label> + <Input + id="destinationPort" + placeholder="예: 현장 직납, 창고 납품" + value={conditions.destinationPort} + onChange={(e) => setConditions(prev => ({ + ...prev, + destinationPort: e.target.value + }))} + /> + </div> + </div> + + <div className="flex items-center space-x-2"> + <Switch + id="isPriceAdjustmentApplicable" + checked={conditions.isPriceAdjustmentApplicable} + onCheckedChange={(checked) => setConditions(prev => ({ + ...prev, + isPriceAdjustmentApplicable: checked + }))} + /> + <Label htmlFor="isPriceAdjustmentApplicable">연동제 적용 가능</Label> + </div> + + <div className="space-y-2"> + <Label htmlFor="sparePartOptions">스페어파트 옵션</Label> + <Textarea + id="sparePartOptions" + placeholder="스페어파트 관련 옵션을 입력하세요" + value={conditions.sparePartOptions} + onChange={(e) => setConditions(prev => ({ + ...prev, + sparePartOptions: e.target.value + }))} + rows={3} + /> + </div> + </CardContent> + </Card> + ) +} diff --git a/components/bidding/bidding-info-header.tsx b/components/bidding/bidding-info-header.tsx new file mode 100644 index 00000000..c140920b --- /dev/null +++ b/components/bidding/bidding-info-header.tsx @@ -0,0 +1,149 @@ +import { Bidding } from '@/db/schema/bidding' +import { Building2, Package, User, DollarSign, Calendar } from 'lucide-react' +import { contractTypeLabels, biddingTypeLabels } from '@/db/schema/bidding' + +interface BiddingInfoHeaderProps { + bidding: Bidding +} + +function formatDate(date: Date | string | null | undefined, locale: 'KR' | 'EN' = 'KR'): string { + if (!date) return '' + + const dateObj = typeof date === 'string' ? new Date(date) : date + + if (locale === 'KR') { + return dateObj.toLocaleDateString('ko-KR', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }).replace(/\./g, '-').replace(/-$/, '') + } + + return dateObj.toLocaleDateString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }) +} + +export function BiddingInfoHeader({ bidding }: BiddingInfoHeaderProps) { + return ( + <div className="bg-white border rounded-lg p-6 mb-6 shadow-sm"> + {/* 주요 정보 섹션 */} + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6"> + {/* 프로젝트 정보 */} + {bidding.projectName && ( + <div className="space-y-1"> + <div className="flex items-center gap-2 text-sm text-gray-500"> + <Building2 className="w-4 h-4" /> + <span>프로젝트</span> + </div> + <div className="font-medium text-gray-900">{bidding.projectName}</div> + </div> + )} + + {/* 품목 정보 */} + {bidding.itemName && ( + <div className="space-y-1"> + <div className="flex items-center gap-2 text-sm text-gray-500"> + <Package className="w-4 h-4" /> + <span>품목</span> + </div> + <div className="font-medium text-gray-900">{bidding.itemName}</div> + </div> + )} + + {/* 담당자 정보 */} + {bidding.managerName && ( + <div className="space-y-1"> + <div className="flex items-center gap-2 text-sm text-gray-500"> + <User className="w-4 h-4" /> + <span>담당자</span> + </div> + <div className="font-medium text-gray-900">{bidding.managerName}</div> + </div> + )} + + {/* 예산 정보 */} + {bidding.budget && ( + <div className="space-y-1"> + <div className="flex items-center gap-2 text-sm text-gray-500"> + <DollarSign className="w-4 h-4" /> + <span>예산</span> + </div> + <div className="font-semibold text-gray-900"> + {new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: bidding.currency || 'KRW', + }).format(Number(bidding.budget))} + </div> + </div> + )} + </div> + + {/* 구분선 */} + <div className="border-t border-gray-100 pt-4 mb-4"> + {/* 계약 정보 */} + <div className="flex flex-wrap gap-8 text-sm"> + <div className="flex items-center gap-2"> + <span className="text-gray-500">계약</span> + <span className="font-medium">{contractTypeLabels[bidding.contractType]}</span> + </div> + + <div className="flex items-center gap-2"> + <span className="text-gray-500">유형</span> + <span className="font-medium">{biddingTypeLabels[bidding.biddingType]}</span> + </div> + + <div className="flex items-center gap-2"> + <span className="text-gray-500">낙찰</span> + <span className="font-medium">{bidding.awardCount === 'single' ? '단수' : '복수'}</span> + </div> + + <div className="flex items-center gap-2"> + <span className="text-gray-500">통화</span> + <span className="font-mono font-medium">{bidding.currency}</span> + </div> + </div> + </div> + + {/* 일정 정보 */} + {(bidding.submissionStartDate || bidding.evaluationDate || bidding.preQuoteDate || bidding.biddingRegistrationDate) && ( + <div className="border-t border-gray-100 pt-4"> + <div className="flex items-center gap-2 mb-3 text-sm text-gray-500"> + <Calendar className="w-4 h-4" /> + <span>일정 정보</span> + </div> + <div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm"> + {bidding.submissionStartDate && bidding.submissionEndDate && ( + <div> + <span className="text-gray-500">제출기간</span> + <div className="font-medium"> + {formatDate(bidding.submissionStartDate, 'KR')} ~ {formatDate(bidding.submissionEndDate, 'KR')} + </div> + </div> + )} + {bidding.biddingRegistrationDate && ( + <div> + <span className="text-gray-500">입찰등록일</span> + <div className="font-medium">{formatDate(bidding.biddingRegistrationDate, 'KR')}</div> + </div> + )} + {bidding.preQuoteDate && ( + <div> + <span className="text-gray-500">사전견적일</span> + <div className="font-medium">{formatDate(bidding.preQuoteDate, 'KR')}</div> + </div> + )} + {bidding.evaluationDate && ( + <div> + <span className="text-gray-500">평가일</span> + <div className="font-medium">{formatDate(bidding.evaluationDate, 'KR')}</div> + </div> + )} + </div> + </div> + )} + </div> + ) +} diff --git a/components/bidding/price-adjustment-dialog.tsx b/components/bidding/price-adjustment-dialog.tsx new file mode 100644 index 00000000..b53f9ef1 --- /dev/null +++ b/components/bidding/price-adjustment-dialog.tsx @@ -0,0 +1,200 @@ +'use client' + +import React from 'react' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Badge } from '@/components/ui/badge' +import { Separator } from '@/components/ui/separator' + +interface PriceAdjustmentData { + id: number + itemName?: string | null + adjustmentReflectionPoint?: string | null + majorApplicableRawMaterial?: string | null + adjustmentFormula?: string | null + rawMaterialPriceIndex?: string | null + referenceDate?: Date | null + comparisonDate?: Date | null + adjustmentRatio?: string | null + notes?: string | null + adjustmentConditions?: string | null + majorNonApplicableRawMaterial?: string | null + adjustmentPeriod?: string | null + contractorWriter?: string | null + adjustmentDate?: Date | null + nonApplicableReason?: string | null + createdAt: Date + updatedAt: Date +} + +interface PriceAdjustmentDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + data: PriceAdjustmentData | null + vendorName: string +} + +function formatDate(date: Date | null | undefined): string { + if (!date) return '-' + return new Date(date).toLocaleDateString('ko-KR', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) +} + +export function PriceAdjustmentDialog({ + open, + onOpenChange, + data, + vendorName, +}: PriceAdjustmentDialogProps) { + if (!data) return null + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <span>하도급대금등 연동표</span> + <Badge variant="secondary">{vendorName}</Badge> + </DialogTitle> + <DialogDescription> + 협력업체가 제출한 연동제 적용 정보입니다. + </DialogDescription> + </DialogHeader> + + <div className="space-y-6"> + {/* 기본 정보 */} + <div> + <h3 className="text-sm font-medium text-gray-900 mb-3">기본 정보</h3> + <div className="grid grid-cols-2 gap-4"> + <div> + <label className="text-xs text-gray-500">품목등의 명칭</label> + <p className="text-sm font-medium">{data.itemName || '-'}</p> + </div> + <div> + <label className="text-xs text-gray-500">조정대금 반영시점</label> + <p className="text-sm font-medium">{data.adjustmentReflectionPoint || '-'}</p> + </div> + </div> + </div> + + <Separator /> + + {/* 원재료 정보 */} + <div> + <h3 className="text-sm font-medium text-gray-900 mb-3">원재료 정보</h3> + <div className="space-y-4"> + <div> + <label className="text-xs text-gray-500">연동대상 주요 원재료</label> + <p className="text-sm font-medium whitespace-pre-wrap"> + {data.majorApplicableRawMaterial || '-'} + </p> + </div> + <div> + <label className="text-xs text-gray-500">연동 미적용 주요 원재료</label> + <p className="text-sm font-medium whitespace-pre-wrap"> + {data.majorNonApplicableRawMaterial || '-'} + </p> + </div> + <div> + <label className="text-xs text-gray-500">연동 미적용 사유</label> + <p className="text-sm font-medium whitespace-pre-wrap"> + {data.nonApplicableReason || '-'} + </p> + </div> + </div> + </div> + + <Separator /> + + {/* 연동 공식 및 지표 */} + <div> + <h3 className="text-sm font-medium text-gray-900 mb-3">연동 공식 및 지표</h3> + <div className="space-y-4"> + <div> + <label className="text-xs text-gray-500">하도급대금등 연동 산식</label> + <div className="p-3 bg-gray-50 rounded-md"> + <p className="text-sm font-mono whitespace-pre-wrap"> + {data.adjustmentFormula || '-'} + </p> + </div> + </div> + <div> + <label className="text-xs text-gray-500">원재료 가격 기준지표</label> + <p className="text-sm font-medium whitespace-pre-wrap"> + {data.rawMaterialPriceIndex || '-'} + </p> + </div> + <div className="grid grid-cols-2 gap-4"> + <div> + <label className="text-xs text-gray-500">기준시점</label> + <p className="text-sm font-medium">{formatDate(data.referenceDate)}</p> + </div> + <div> + <label className="text-xs text-gray-500">비교시점</label> + <p className="text-sm font-medium">{formatDate(data.comparisonDate)}</p> + </div> + </div> + <div> + <label className="text-xs text-gray-500">연동 비율</label> + <p className="text-sm font-medium"> + {data.adjustmentRatio ? `${data.adjustmentRatio}%` : '-'} + </p> + </div> + </div> + </div> + + <Separator /> + + {/* 조정 조건 및 기타 */} + <div> + <h3 className="text-sm font-medium text-gray-900 mb-3">조정 조건 및 기타</h3> + <div className="space-y-4"> + <div> + <label className="text-xs text-gray-500">조정요건</label> + <p className="text-sm font-medium whitespace-pre-wrap"> + {data.adjustmentConditions || '-'} + </p> + </div> + <div className="grid grid-cols-2 gap-4"> + <div> + <label className="text-xs text-gray-500">조정주기</label> + <p className="text-sm font-medium">{data.adjustmentPeriod || '-'}</p> + </div> + <div> + <label className="text-xs text-gray-500">조정일</label> + <p className="text-sm font-medium">{formatDate(data.adjustmentDate)}</p> + </div> + </div> + <div> + <label className="text-xs text-gray-500">수탁기업(협력사) 작성자</label> + <p className="text-sm font-medium">{data.contractorWriter || '-'}</p> + </div> + <div> + <label className="text-xs text-gray-500">기타 사항</label> + <p className="text-sm font-medium whitespace-pre-wrap"> + {data.notes || '-'} + </p> + </div> + </div> + </div> + + <Separator /> + + {/* 메타 정보 */} + <div className="text-xs text-gray-500 space-y-1"> + <p>작성일: {formatDate(data.createdAt)}</p> + <p>수정일: {formatDate(data.updatedAt)}</p> + </div> + </div> + </DialogContent> + </Dialog> + ) +} |
