summaryrefslogtreecommitdiff
path: root/lib/bidding/list
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-03 10:35:57 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-03 10:35:57 +0000
commita2bc455f654e011c53968b0d3a14389d7259847e (patch)
tree6ff60b8ef0880aaa4cf2c9d4f234772fb0a74537 /lib/bidding/list
parentbfe354f7633f62350e61eb784cbf1926079339d1 (diff)
(최겸) 구매 입찰 개발(벤더 응찰 개발 및 기본계약 요청 개발 필)
Diffstat (limited to 'lib/bidding/list')
-rw-r--r--lib/bidding/list/biddings-table-columns.tsx4
-rw-r--r--lib/bidding/list/create-bidding-dialog.tsx253
2 files changed, 238 insertions, 19 deletions
diff --git a/lib/bidding/list/biddings-table-columns.tsx b/lib/bidding/list/biddings-table-columns.tsx
index c936de33..48a77954 100644
--- a/lib/bidding/list/biddings-table-columns.tsx
+++ b/lib/bidding/list/biddings-table-columns.tsx
@@ -558,7 +558,7 @@ export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef
<Edit className="mr-2 h-4 w-4" />
수정
</DropdownMenuItem>
- <DropdownMenuSeparator />
+ {/* <DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setRowAction({ row, type: "copy" })}>
<Package className="mr-2 h-4 w-4" />
복사 생성
@@ -566,7 +566,7 @@ export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef
<DropdownMenuItem onClick={() => setRowAction({ row, type: "manage_companies" })}>
<Users className="mr-2 h-4 w-4" />
참여업체 관리
- </DropdownMenuItem>
+ </DropdownMenuItem> */}
</DropdownMenuContent>
</DropdownMenu>
),
diff --git a/lib/bidding/list/create-bidding-dialog.tsx b/lib/bidding/list/create-bidding-dialog.tsx
index 90204dc9..e5bfcae4 100644
--- a/lib/bidding/list/create-bidding-dialog.tsx
+++ b/lib/bidding/list/create-bidding-dialog.tsx
@@ -120,7 +120,7 @@ interface PRItemInfo {
}
// 탭 순서 정의
-const TAB_ORDER = ["basic", "contract", "schedule", "details", "manager"] as const
+const TAB_ORDER = ["basic", "contract", "schedule", "conditions", "details", "manager"] as const
type TabType = typeof TAB_ORDER[number]
export function CreateBiddingDialog() {
@@ -154,6 +154,18 @@ export function CreateBiddingDialog() {
// 파일 첨부를 위해 선택된 아이템 ID
const [selectedItemForFile, setSelectedItemForFile] = React.useState<string | null>(null)
+ // 입찰 조건 상태
+ const [biddingConditions, setBiddingConditions] = React.useState({
+ paymentTerms: "",
+ taxConditions: "",
+ incoterms: "",
+ contractDeliveryDate: "",
+ shippingPort: "",
+ destinationPort: "",
+ isPriceAdjustmentApplicable: false,
+ sparePartOptions: "",
+ })
+
// 사양설명회 파일 추가
const addMeetingFiles = (files: File[]) => {
setSpecMeetingInfo(prev => ({
@@ -257,6 +269,12 @@ export function CreateBiddingDialog() {
(specMeetingInfo.meetingDate && specMeetingInfo.location && specMeetingInfo.contactPerson)),
hasErrors: !!(formErrors.submissionStartDate || formErrors.submissionEndDate)
},
+ conditions: {
+ isValid: biddingConditions.paymentTerms.trim() !== "" &&
+ biddingConditions.taxConditions.trim() !== "" &&
+ biddingConditions.incoterms.trim() !== "",
+ hasErrors: false
+ },
details: {
isValid: true, // 세부내역은 선택사항
hasErrors: false
@@ -405,6 +423,8 @@ export function CreateBiddingDialog() {
} else {
toast.error("제출 시작일시와 마감일시를 입력해주세요")
}
+ } else if (activeTab === "conditions") {
+ toast.error("입찰 조건을 모두 입력해주세요 (지급조건, 세금조건, 운송조건)")
}
return
}
@@ -444,6 +464,7 @@ export function CreateBiddingDialog() {
meetingFiles: specMeetingInfo.meetingFiles
} : null,
prItems: prItems.length > 0 ? prItems : [],
+ biddingConditions: biddingConditions,
}
const result = await createBidding(extendedData, userId)
@@ -517,6 +538,16 @@ export function CreateBiddingDialog() {
})
setPrItems([])
setSelectedItemForFile(null)
+ setBiddingConditions({
+ paymentTerms: "",
+ taxConditions: "",
+ incoterms: "",
+ contractDeliveryDate: "",
+ shippingPort: "",
+ destinationPort: "",
+ isPriceAdjustmentApplicable: false,
+ sparePartOptions: "",
+ })
setActiveTab("basic")
setShowSuccessDialog(false) // 추가
setCreatedBiddingId(null) // 추가
@@ -545,7 +576,7 @@ export function CreateBiddingDialog() {
// 성공 다이얼로그 핸들러들
const handleNavigateToDetail = () => {
if (createdBiddingId) {
- router.push(`/evcp/biddings/${createdBiddingId}`)
+ router.push(`/evcp/bid/${createdBiddingId}`)
}
setShowSuccessDialog(false)
setCreatedBiddingId(null)
@@ -566,7 +597,7 @@ export function CreateBiddingDialog() {
신규 입찰
</Button>
</DialogTrigger>
- <DialogContent className="max-w-6xl h-[90vh] p-0 flex flex-col">
+ <DialogContent className="max-w-7xl h-[90vh] p-0 flex flex-col">
{/* 고정 헤더 */}
<div className="flex-shrink-0 p-6 border-b">
<DialogHeader>
@@ -586,29 +617,87 @@ export function CreateBiddingDialog() {
{/* 탭 영역 */}
<div className="flex-1 overflow-hidden">
<Tabs value={activeTab} onValueChange={setActiveTab} className="h-full flex flex-col">
- <div className="px-6 pt-4">
- <TabsList className="grid w-full grid-cols-5">
- <TabsTrigger value="basic" className="relative">
- 기본 정보
+ <div className="px-6">
+ <div className="flex space-x-1 bg-muted p-1 rounded-lg overflow-x-auto">
+ <button
+ type="button"
+ onClick={() => setActiveTab("basic")}
+ className={`relative px-3 py-2 text-sm font-medium rounded-md transition-all whitespace-nowrap flex-shrink-0 ${
+ activeTab === "basic"
+ ? "bg-background text-foreground shadow-sm"
+ : "text-muted-foreground hover:text-foreground"
+ }`}
+ >
+ 기본정보
{!tabValidation.basic.isValid && (
<span className="absolute -top-1 -right-1 h-2 w-2 bg-red-500 rounded-full"></span>
)}
- </TabsTrigger>
- <TabsTrigger value="contract" className="relative">
- 계약 정보
+ </button>
+ <button
+ type="button"
+ onClick={() => setActiveTab("contract")}
+ className={`relative px-3 py-2 text-sm font-medium rounded-md transition-all whitespace-nowrap flex-shrink-0 ${
+ activeTab === "contract"
+ ? "bg-background text-foreground shadow-sm"
+ : "text-muted-foreground hover:text-foreground"
+ }`}
+ >
+ 계약정보
{!tabValidation.contract.isValid && (
<span className="absolute -top-1 -right-1 h-2 w-2 bg-red-500 rounded-full"></span>
)}
- </TabsTrigger>
- <TabsTrigger value="schedule" className="relative">
- 일정 & 회의
+ </button>
+ <button
+ type="button"
+ onClick={() => setActiveTab("schedule")}
+ className={`relative px-3 py-2 text-sm font-medium rounded-md transition-all whitespace-nowrap flex-shrink-0 ${
+ activeTab === "schedule"
+ ? "bg-background text-foreground shadow-sm"
+ : "text-muted-foreground hover:text-foreground"
+ }`}
+ >
+ 일정회의
{!tabValidation.schedule.isValid && (
<span className="absolute -top-1 -right-1 h-2 w-2 bg-red-500 rounded-full"></span>
)}
- </TabsTrigger>
- <TabsTrigger value="details">세부내역</TabsTrigger>
- <TabsTrigger value="manager">담당자 & 기타</TabsTrigger>
- </TabsList>
+ </button>
+ <button
+ type="button"
+ onClick={() => setActiveTab("conditions")}
+ className={`relative px-3 py-2 text-sm font-medium rounded-md transition-all whitespace-nowrap flex-shrink-0 ${
+ activeTab === "conditions"
+ ? "bg-background text-foreground shadow-sm"
+ : "text-muted-foreground hover:text-foreground"
+ }`}
+ >
+ 입찰조건
+ {!tabValidation.conditions.isValid && (
+ <span className="absolute -top-1 -right-1 h-2 w-2 bg-red-500 rounded-full"></span>
+ )}
+ </button>
+ <button
+ type="button"
+ onClick={() => setActiveTab("details")}
+ className={`relative px-3 py-2 text-sm font-medium rounded-md transition-all whitespace-nowrap flex-shrink-0 ${
+ activeTab === "details"
+ ? "bg-background text-foreground shadow-sm"
+ : "text-muted-foreground hover:text-foreground"
+ }`}
+ >
+ 세부내역
+ </button>
+ <button
+ type="button"
+ onClick={() => setActiveTab("manager")}
+ className={`relative px-3 py-2 text-sm font-medium rounded-md transition-all whitespace-nowrap flex-shrink-0 ${
+ activeTab === "manager"
+ ? "bg-background text-foreground shadow-sm"
+ : "text-muted-foreground hover:text-foreground"
+ }`}
+ >
+ 담당자
+ </button>
+ </div>
</div>
<div className="flex-1 overflow-y-auto p-6">
@@ -1193,6 +1282,128 @@ export function CreateBiddingDialog() {
</Card>
</TabsContent>
+ {/* 입찰 조건 탭 */}
+ <TabsContent value="conditions" className="mt-0 space-y-6">
+ <Card>
+ <CardHeader>
+ <CardTitle>입찰 조건</CardTitle>
+ <p className="text-sm text-muted-foreground">
+ 벤더가 사전견적 시 참고할 입찰 조건을 설정하세요
+ </p>
+ </CardHeader>
+ <CardContent className="space-y-6">
+ <div className="grid grid-cols-2 gap-6">
+ <div className="space-y-2">
+ <label className="text-sm font-medium">
+ 지급조건 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ placeholder="예: 월말결제, 60일"
+ value={biddingConditions.paymentTerms}
+ onChange={(e) => setBiddingConditions(prev => ({
+ ...prev,
+ paymentTerms: e.target.value
+ }))}
+ />
+ </div>
+
+ <div className="space-y-2">
+ <label className="text-sm font-medium">
+ 세금조건 <span className="text-red-500">*</span>
+ </label>
+ <Input
+
+ value={biddingConditions.taxConditions}
+ onChange={(e) => setBiddingConditions(prev => ({
+ ...prev,
+ taxConditions: e.target.value
+ }))}
+ />
+ </div>
+
+ <div className="space-y-2">
+ <label className="text-sm font-medium">
+ 운송조건(인코텀즈) <span className="text-red-500">*</span>
+ </label>
+ <Input
+ placeholder="예: FOB, CIF 등"
+ value={biddingConditions.incoterms}
+ onChange={(e) => setBiddingConditions(prev => ({
+ ...prev,
+ incoterms: e.target.value
+ }))}
+ />
+ </div>
+
+ <div className="space-y-2">
+ <label className="text-sm font-medium">
+ 계약 납품일
+ </label>
+ <Input
+ type="date"
+ value={biddingConditions.contractDeliveryDate}
+ onChange={(e) => setBiddingConditions(prev => ({
+ ...prev,
+ contractDeliveryDate: e.target.value
+ }))}
+ />
+ </div>
+
+ <div className="space-y-2">
+ <label className="text-sm font-medium">선적지</label>
+ <Input
+ placeholder="예: 부산항, 인천항"
+ value={biddingConditions.shippingPort}
+ onChange={(e) => setBiddingConditions(prev => ({
+ ...prev,
+ shippingPort: e.target.value
+ }))}
+ />
+ </div>
+
+ <div className="space-y-2">
+ <label className="text-sm font-medium">도착지</label>
+ <Input
+ placeholder="예: 현장 직납, 창고 납품"
+ value={biddingConditions.destinationPort}
+ onChange={(e) => setBiddingConditions(prev => ({
+ ...prev,
+ destinationPort: e.target.value
+ }))}
+ />
+ </div>
+ </div>
+
+ <div className="flex items-center space-x-2">
+ <Switch
+ id="price-adjustment"
+ checked={biddingConditions.isPriceAdjustmentApplicable}
+ onCheckedChange={(checked) => setBiddingConditions(prev => ({
+ ...prev,
+ isPriceAdjustmentApplicable: checked
+ }))}
+ />
+ <label htmlFor="price-adjustment" className="text-sm font-medium">
+ 연동제 적용 가능
+ </label>
+ </div>
+
+ <div className="space-y-2">
+ <label className="text-sm font-medium">스페어파트 옵션</label>
+ <Textarea
+ placeholder="스페어파트 관련 옵션을 입력하세요"
+ value={biddingConditions.sparePartOptions}
+ onChange={(e) => setBiddingConditions(prev => ({
+ ...prev,
+ sparePartOptions: e.target.value
+ }))}
+ rows={3}
+ />
+ </div>
+ </CardContent>
+ </Card>
+ </TabsContent>
+
{/* 세부내역 탭 */}
<TabsContent value="details" className="mt-0 space-y-6">
<Card>
@@ -1657,6 +1868,14 @@ export function CreateBiddingDialog() {
)}
</span>
)}
+ {activeTab === "conditions" && (
+ <span>
+ 입찰 조건을 설정하세요
+ {!tabValidation.conditions.isValid && (
+ <span className="text-red-500 ml-2">• 필수 항목이 누락되었습니다</span>
+ )}
+ </span>
+ )}
{activeTab === "details" && "세부내역 아이템을 관리하세요 (선택사항)"}
{activeTab === "manager" && "담당자 정보를 확인하고 입찰을 생성하세요"}
</div>