summaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
Diffstat (limited to 'components')
-rw-r--r--components/bidding/bidding-conditions-edit.tsx304
-rw-r--r--components/bidding/bidding-info-header.tsx149
-rw-r--r--components/bidding/price-adjustment-dialog.tsx200
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>
+ )
+}