summaryrefslogtreecommitdiff
path: root/lib/bidding/vendor
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-17 10:40:12 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-17 10:40:12 +0000
commit10cb50753ccf318024c4394282f9e8d968dcd1a5 (patch)
treecf4edb96aa172c3b90d88532aff1f536944a2283 /lib/bidding/vendor
parentf7117370b9cc0c7b96bd1eb23a1b9f5b16cc8ceb (diff)
(최겸) 구매 입찰 오류 수정 및 선적지,하역지 연동,TO Cont, TO PO 개발
Diffstat (limited to 'lib/bidding/vendor')
-rw-r--r--lib/bidding/vendor/components/pr-items-pricing-table.tsx5
-rw-r--r--lib/bidding/vendor/components/simple-file-upload.tsx3
-rw-r--r--lib/bidding/vendor/partners-bidding-attachments-dialog.tsx1
-rw-r--r--lib/bidding/vendor/partners-bidding-attendance-dialog.tsx19
-rw-r--r--lib/bidding/vendor/partners-bidding-detail.tsx68
-rw-r--r--lib/bidding/vendor/partners-bidding-list-columns.tsx55
-rw-r--r--lib/bidding/vendor/partners-bidding-list.tsx132
-rw-r--r--lib/bidding/vendor/partners-bidding-participation-dialog.tsx248
-rw-r--r--lib/bidding/vendor/partners-bidding-pre-quote.tsx178
-rw-r--r--lib/bidding/vendor/partners-bidding-toolbar-actions.tsx16
10 files changed, 227 insertions, 498 deletions
diff --git a/lib/bidding/vendor/components/pr-items-pricing-table.tsx b/lib/bidding/vendor/components/pr-items-pricing-table.tsx
index 13804251..483bce5c 100644
--- a/lib/bidding/vendor/components/pr-items-pricing-table.tsx
+++ b/lib/bidding/vendor/components/pr-items-pricing-table.tsx
@@ -4,8 +4,7 @@ import * as React from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
-import { Button } from '@/components/ui/button'
-import { Textarea } from '@/components/ui/textarea'
+
import { Badge } from '@/components/ui/badge'
import {
Table,
@@ -17,7 +16,7 @@ import {
} from '@/components/ui/table'
import {
Package,
- FileText,
+
Download,
Calculator
} from 'lucide-react'
diff --git a/lib/bidding/vendor/components/simple-file-upload.tsx b/lib/bidding/vendor/components/simple-file-upload.tsx
index 58b60bdf..1344a491 100644
--- a/lib/bidding/vendor/components/simple-file-upload.tsx
+++ b/lib/bidding/vendor/components/simple-file-upload.tsx
@@ -5,7 +5,6 @@ import { Card, CardContent, 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 { Badge } from '@/components/ui/badge'
import {
Table,
TableBody,
@@ -15,7 +14,6 @@ import {
TableRow,
} from '@/components/ui/table'
import {
- Upload,
FileText,
Download,
Trash2
@@ -171,6 +169,7 @@ export function SimpleFileUpload({
throw new Error('파일 정보가 없습니다.')
}
} catch (error) {
+ console.error('파일 다운로드 실패:', error)
toast({
title: '다운로드 실패',
description: '파일 다운로드에 실패했습니다.',
diff --git a/lib/bidding/vendor/partners-bidding-attachments-dialog.tsx b/lib/bidding/vendor/partners-bidding-attachments-dialog.tsx
index 951923ca..f5206c71 100644
--- a/lib/bidding/vendor/partners-bidding-attachments-dialog.tsx
+++ b/lib/bidding/vendor/partners-bidding-attachments-dialog.tsx
@@ -121,6 +121,7 @@ export function PartnersBiddingAttachmentsDialog({
throw new Error('파일 정보가 없습니다.')
}
} catch (error) {
+ console.error('파일 다운로드 실패:', error)
toast({
title: '다운로드 실패',
description: '파일 다운로드에 실패했습니다.',
diff --git a/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx b/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx
index 6276e433..d0ef97f1 100644
--- a/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx
+++ b/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx
@@ -13,28 +13,24 @@ import {
import { Label } from '@/components/ui/label'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Input } from '@/components/ui/input'
-import { Badge } from '@/components/ui/badge'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { ScrollArea } from '@/components/ui/scroll-area'
import {
Calendar,
Users,
- MapPin,
Clock,
- FileText,
CheckCircle,
XCircle,
Download,
- User,
- Phone
} from 'lucide-react'
import { formatDate } from '@/lib/utils'
import { updatePartnerAttendance, getSpecificationMeetingForPartners } from '../detail/service'
import { useToast } from '@/hooks/use-toast'
import { useTransition } from 'react'
+import { useRouter } from 'next/navigation'
-interface PartnersBiddingAttendanceDialogProps {
+interface PartnersSpecificationMeetingDialogProps {
biddingDetail: {
id: number
biddingNumber: string
@@ -48,22 +44,20 @@ interface PartnersBiddingAttendanceDialogProps {
isAttending: boolean | null
open: boolean
onOpenChange: (open: boolean) => void
- onSuccess: () => void
}
-export function PartnersBiddingAttendanceDialog({
+export function PartnersSpecificationMeetingDialog({
biddingDetail,
biddingCompanyId,
isAttending,
open,
onOpenChange,
- onSuccess,
-}: PartnersBiddingAttendanceDialogProps) {
+}: PartnersSpecificationMeetingDialogProps) {
const { toast } = useToast()
const [isPending, startTransition] = useTransition()
const [isLoading, setIsLoading] = React.useState(false)
const [meetingData, setMeetingData] = React.useState<any>(null)
-
+ const router = useRouter()
// 폼 상태
const [attendance, setAttendance] = React.useState<string>('')
const [attendeeCount, setAttendeeCount] = React.useState<string>('')
@@ -93,6 +87,7 @@ export function PartnersBiddingAttendanceDialog({
})
}
} catch (error) {
+ console.error('사양설명회 정보를 불러오는데 실패했습니다.', error)
toast({
title: '오류',
description: '사양설명회 정보를 불러오는데 실패했습니다.',
@@ -178,7 +173,7 @@ export function PartnersBiddingAttendanceDialog({
})
}
- onSuccess()
+ router.refresh()
onOpenChange(false)
} else {
toast({
diff --git a/lib/bidding/vendor/partners-bidding-detail.tsx b/lib/bidding/vendor/partners-bidding-detail.tsx
index 89ca426b..d134bc3b 100644
--- a/lib/bidding/vendor/partners-bidding-detail.tsx
+++ b/lib/bidding/vendor/partners-bidding-detail.tsx
@@ -5,30 +5,24 @@ import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
-import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
-import { Textarea } from '@/components/ui/textarea'
-import { Checkbox } from '@/components/ui/checkbox'
import {
ArrowLeft,
- Calendar,
- Building2,
- Package,
User,
- DollarSign,
- FileText,
Users,
Send,
CheckCircle,
XCircle,
- Save
+ Save,
+ FileText,
+ Building2,
+ Package
} from 'lucide-react'
import { formatDate } from '@/lib/utils'
import {
getBiddingDetailsForPartners,
submitPartnerResponse,
- updatePartnerAttendance,
updatePartnerBiddingParticipation,
saveBiddingDraft
} from '../detail/service'
@@ -61,7 +55,8 @@ interface BiddingDetail {
contractType: string
biddingType: string
awardCount: string | null
- contractPeriod: string | null
+ contractStartDate: Date | null
+ contractEndDate: Date | null
preQuoteDate: Date | null
biddingRegistrationDate: Date | null
submissionStartDate: Date | null
@@ -180,10 +175,10 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
setTotalQuotationAmount(total)
// 응찰 확정 시에만 사전견적 금액을 finalQuoteAmount로 설정
- if (total > 0 && result.isBiddingParticipated === true) {
+ if (totalQuotationAmount > 0 && result.isBiddingParticipated === true) {
setResponseData(prev => ({
...prev,
- finalQuoteAmount: total.toString()
+ finalQuoteAmount: totalQuotationAmount.toString()
}))
}
} catch (error) {
@@ -455,13 +450,6 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
})
}
- const formatCurrency = (amount: number) => {
- return new Intl.NumberFormat('ko-KR', {
- style: 'currency',
- currency: biddingDetail?.currency || 'KRW',
- }).format(amount)
- }
-
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
@@ -497,9 +485,11 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
<div>
<h1 className="text-2xl font-semibold">{biddingDetail.title}</h1>
<div className="flex items-center gap-2 mt-1">
- <Badge variant="outline" className="font-mono">
+ <Badge variant="outline" className="font-mono text-xs">
{biddingDetail.biddingNumber}
- {biddingDetail.revision && biddingDetail.revision > 0 && ` Rev.${biddingDetail.revision}`}
+ </Badge>
+ <Badge variant="outline" className="font-mono">
+ Rev. {biddingDetail.revision ?? 0}
</Badge>
<Badge variant={
biddingDetail.status === 'bidding_disposal' ? 'destructive' :
@@ -670,28 +660,6 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
- {/* 품목별 견적 섹션 */}
- {/* <div className="space-y-2">
- <Label htmlFor="finalQuoteAmount">총 견적금액 *</Label>
- <Input
- id="finalQuoteAmount"
- type="number"
- value={responseData.finalQuoteAmount}
- onChange={(e) => setResponseData({...responseData, finalQuoteAmount: e.target.value})}
- placeholder="총 견적금액을 입력하세요"
- />
- </div> */}
-
- {/* <div className="space-y-2">
- <Label htmlFor="proposedContractDeliveryDate">제안 납품일</Label>
- <Input
- id="proposedContractDeliveryDate"
- type="date"
- value={responseData.proposedContractDeliveryDate}
- onChange={(e) => setResponseData({...responseData, proposedContractDeliveryDate: e.target.value})}
- />
- </div> */}
-
{/* 품목별 상세 견적 테이블 */}
{prItems.length > 0 ? (
<PrItemsPricingTable
@@ -719,18 +687,6 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
readOnly={false}
/>
)}
-
- {/* 기타 사항 */}
- {/* <div className="space-y-2">
- <Label htmlFor="additionalProposals">기타 사항</Label>
- <Textarea
- id="additionalProposals"
- value={responseData.additionalProposals}
- onChange={(e) => setResponseData({...responseData, additionalProposals: e.target.value})}
- placeholder="기타 특이사항이나 제안사항을 입력하세요"
- rows={4}
- />
- </div> */}
{/* 응찰 제출 버튼 - 참여 확정 상태일 때만 표시 */}
<div className="flex justify-end pt-4 gap-2">
<Button
diff --git a/lib/bidding/vendor/partners-bidding-list-columns.tsx b/lib/bidding/vendor/partners-bidding-list-columns.tsx
index 534e8838..7fb62122 100644
--- a/lib/bidding/vendor/partners-bidding-list-columns.tsx
+++ b/lib/bidding/vendor/partners-bidding-list-columns.tsx
@@ -13,7 +13,6 @@ import {
import {
CheckCircle,
XCircle,
- Users,
FileText,
MoreHorizontal,
Calendar,
@@ -67,9 +66,7 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL
return (
<div className="font-mono text-sm">
<div>{biddingNumber}</div>
- {revision > 0 && (
- <div className="text-muted-foreground">Rev.{revision}</div>
- )}
+ <div className="text-muted-foreground">Rev. {revision ?? 0}</div>
</div>
)
},
@@ -160,15 +157,6 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL
}
}
- const handleAttendance = () => {
- if (setRowAction) {
- setRowAction({
- type: 'attendance',
- row: { original: row.original }
- })
- }
- }
-
const handlePreQuote = () => {
if (setRowAction) {
setRowAction({
@@ -262,24 +250,16 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL
}),
// 입찰 참여의사
- columnHelper.accessor('invitationStatus', {
+ columnHelper.accessor('isBiddingParticipated', {
header: '입찰 참여의사',
cell: ({ row }) => {
- const status = row.original.invitationStatus
- const statusLabels = {
- sent: '초대됨',
- submitted: '참여',
- declined: '불참',
- pending: '대기중'
+ const participated = row.original.isBiddingParticipated
+ if (participated === null) {
+ return <Badge variant="outline">미결정</Badge>
}
return (
- <Badge variant={
- status === 'submitted' ? 'default' :
- status === 'declined' ? 'destructive' :
- status === 'sent' ? 'secondary' :
- 'outline'
- }>
- {statusLabels[status as keyof typeof statusLabels] || status}
+ <Badge variant={participated ? 'default' : 'destructive'}>
+ {participated ? '참여' : '불참'}
</Badge>
)
},
@@ -340,13 +320,22 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL
}),
// 계약기간
- columnHelper.accessor('contractPeriod', {
+ columnHelper.accessor('contractStartDate', {
header: '계약기간',
- cell: ({ row }) => (
- <div className="max-w-24 truncate" title={row.original.contractPeriod || ''}>
- {row.original.contractPeriod || '-'}
- </div>
- ),
+ cell: ({ row }) => {
+ const startDate = row.original.contractStartDate
+ const endDate = row.original.contractEndDate
+
+ if (!startDate || !endDate) {
+ return <div className="max-w-24 truncate">-</div>
+ }
+
+ return (
+ <div className="max-w-24 truncate" title={`${formatDate(startDate, 'KR')} ~ ${formatDate(endDate, 'KR')}`}>
+ {formatDate(startDate, 'KR')} ~ {formatDate(endDate, 'KR')}
+ </div>
+ )
+ },
}),
// 참여회신 마감일
diff --git a/lib/bidding/vendor/partners-bidding-list.tsx b/lib/bidding/vendor/partners-bidding-list.tsx
index eb38ce71..fc3cd1f2 100644
--- a/lib/bidding/vendor/partners-bidding-list.tsx
+++ b/lib/bidding/vendor/partners-bidding-list.tsx
@@ -5,7 +5,6 @@ import { useRouter } from 'next/navigation'
import type {
DataTableAdvancedFilterField,
DataTableFilterField,
- DataTableRowAction,
} from '@/types/table'
import { useDataTable } from '@/hooks/use-data-table'
@@ -13,94 +12,36 @@ import { useToast } from '@/hooks/use-toast'
import { DataTable } from '@/components/data-table/data-table'
import { DataTableAdvancedToolbar } from '@/components/data-table/data-table-advanced-toolbar'
import { getPartnersBiddingListColumns } from './partners-bidding-list-columns'
-import { getBiddingListForPartners, PartnersBiddingListItem } from '../detail/service'
+import { PartnersBiddingListItem } from '../detail/service'
import { PartnersBiddingToolbarActions } from './partners-bidding-toolbar-actions'
-import { PartnersBiddingAttendanceDialog } from './partners-bidding-attendance-dialog'
-import { PartnersBiddingParticipationDialog } from './partners-bidding-participation-dialog'
+import { PartnersSpecificationMeetingDialog } from './partners-bidding-attendance-dialog'
import { PartnersBiddingAttachmentsDialog } from './partners-bidding-attachments-dialog'
-import { setPreQuoteParticipation, getBiddingCompaniesForPartners } from '../pre-quote/service'
interface PartnersBiddingListProps {
companyId: number
+ promises: Promise<[PartnersBiddingListItem[]]>
}
-export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) {
- const [data, setData] = React.useState<PartnersBiddingListItem[]>([])
- const [pageCount, setPageCount] = React.useState<number>(1)
- const [isLoading, setIsLoading] = React.useState(true)
+export function PartnersBiddingList({ promises }: PartnersBiddingListProps) {
+ const [biddingData] = React.use(promises)
const [rowAction, setRowAction] = React.useState<{ type: string; row: { original: PartnersBiddingListItem } } | null>(null)
- const [isParticipationDialogOpen, setIsParticipationDialogOpen] = React.useState(false)
- const [selectedBiddingForParticipation, setSelectedBiddingForParticipation] = React.useState<PartnersBiddingListItem | null>(null)
- const [selectedBiddingForPreQuoteParticipation, setSelectedBiddingForPreQuoteParticipation] = React.useState<any | null>(null)
+ // const [selectedBiddingForSpecificationMeetingParticipation] = React.useState<PartnersBiddingListItem | null>(null)
const [isAttachmentsDialogOpen, setIsAttachmentsDialogOpen] = React.useState(false)
const [selectedBiddingForAttachments, setSelectedBiddingForAttachments] = React.useState<PartnersBiddingListItem | null>(null)
-
const router = useRouter()
const { toast } = useToast()
- // 데이터 새로고침 함수
- const refreshData = React.useCallback(async () => {
- try {
- setIsLoading(true)
- const result = await getBiddingListForPartners(companyId)
- setData(result)
- } catch (error) {
- console.error('Failed to refresh bidding list:', error)
- } finally {
- setIsLoading(false)
- }
- }, [companyId])
-
- // 입찰 참여의사 결정 핸들러
- const handlePreQuoteParticipationDecision = React.useCallback(async (participate: boolean) => {
- if (!selectedBiddingForPreQuoteParticipation?.biddingCompanyId) {
- throw new Error('업체 정보를 찾을 수 없습니다.')
- }
-
- const result = await setPreQuoteParticipation(
- selectedBiddingForPreQuoteParticipation.biddingCompanyId,
- participate
- )
-
- if (result.success) {
- await refreshData() // 데이터 새로고침
- } else {
- throw new Error(result.error)
- }
- }, [selectedBiddingForPreQuoteParticipation?.biddingCompanyId, refreshData])
-
- // 데이터 로드
- React.useEffect(() => {
- const loadData = async () => {
- try {
- setIsLoading(true)
- const result = await getBiddingListForPartners(companyId)
- setData(result)
- setPageCount(1) // 클라이언트 사이드 페이징이므로 1로 설정
- } catch (error) {
- console.error('Failed to load bidding list:', error)
- setData([])
- } finally {
- setIsLoading(false)
- }
- }
-
- loadData()
- }, [companyId])
-
- // rowAction 변경 감지하여 해당 페이지로 이동 또는 다이얼로그 열기
- React.useEffect(() => {
+ React.useEffect(() => {
if (rowAction) {
+ const bidding = rowAction.row.original
switch (rowAction.type) {
case 'view':
- // 본입찰 초대 여부 확인
- const bidding = rowAction.row.original
// 사전견적 요청 상태에서는 상세보기 제한
if (bidding.status === 'request_for_quotation') {
toast({
title: '접근 제한',
- description: '사전견적 요청 상태에서는 상세보기를 이용할 수 없습니다.',
+ description: '사전견적 요청 상태에서는 본입찰을 이용할 수 없습니다.',
variant: 'destructive',
})
return
@@ -116,16 +57,21 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) {
return
}
// 상세 페이지로 이동 (biddingId 사용)
- router.push(`/partners/bid/${rowAction.row.original.biddingId}`)
+ router.push(`/partners/bid/${bidding.biddingId}`)
break
case 'pre-quote':
// 사전견적 페이지로 이동
- router.push(`/partners/bid/${rowAction.row.original.biddingId}/pre-quote`)
- break
- case 'participation':
- // 입찰 참여 의사 결정 다이얼로그 열기 - 상세 데이터 로드 필요
- handlePreQuoteParticipationDecision(true)
- setRowAction(null) // rowAction 초기화
+ // 사전견적에 초대받지 않은 벤더는 bidding status가 bidding_opened 이고 isPreQuoteParticipated이 null일 경우, 초대받지 않은 것으로 판단
+ if (bidding.status === 'bidding_opened' && bidding.isPreQuoteParticipated === null) {
+ toast({
+ title: '접근 제한',
+ description: '사전견적에 초대받지 않은 업체입니다.',
+ variant: 'destructive',
+ })
+ return
+ }
+
+ router.push(`/partners/bid/${bidding.biddingId}/pre-quote`)
break
case 'view-documents':
// 첨부파일 다이얼로그 열기
@@ -137,7 +83,7 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) {
break
}
}
- }, [rowAction, router, handlePreQuoteParticipationDecision])
+ }, [rowAction, router, toast])
const columns = React.useMemo(
() => getPartnersBiddingListColumns({ setRowAction }),
@@ -199,9 +145,9 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) {
]
const { table } = useDataTable({
- data,
+ data: biddingData,
columns,
- pageCount,
+ pageCount: 1,
filterFields,
enableAdvancedFilter: true,
initialState: {
@@ -213,16 +159,6 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) {
clearOnDefault: true,
})
- if (isLoading) {
- return (
- <div className="flex items-center justify-center py-12">
- <div className="text-center">
- <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
- <p className="text-muted-foreground">입찰 목록을 불러오는 중...</p>
- </div>
- </div>
- )
- }
return (
<>
@@ -232,12 +168,12 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) {
filterFields={advancedFilterFields}
shallow={false}
>
- <PartnersBiddingToolbarActions table={table} companyId={companyId} onRefresh={refreshData} setRowAction={setRowAction} />
+ <PartnersBiddingToolbarActions table={table} setRowAction={setRowAction} />
</DataTableAdvancedToolbar>
</DataTable>
- <PartnersBiddingAttendanceDialog
- open={rowAction?.type === "attendance"}
+ <PartnersSpecificationMeetingDialog
+ open={rowAction?.type === "specification_meeting"}
onOpenChange={() => setRowAction(null)}
biddingDetail={rowAction?.row.original ? {
id: rowAction.row.original.biddingId,
@@ -246,23 +182,11 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) {
preQuoteDate: null,
biddingRegistrationDate: rowAction.row.original.submissionStartDate?.toISOString() || null,
evaluationDate: null,
- hasSpecificationMeeting: (rowAction.row.original as any).hasSpecificationMeeting || false, // 사양설명회 여부 추가
+ hasSpecificationMeeting: rowAction.row.original.hasSpecificationMeeting || false,
} : null}
biddingCompanyId={rowAction?.row.original?.biddingCompanyId || 0}
isAttending={rowAction?.row.original?.isAttendingMeeting || null}
- onSuccess={refreshData}
/>
-{/*
- <PartnersBiddingParticipationDialog
- open={isParticipationDialogOpen}
- onOpenChange={setIsParticipationDialogOpen}
- bidding={selectedBiddingForParticipation}
- companyId={companyId}
- onSuccess={() => {
- refreshData()
- setSelectedBiddingForParticipation(null)
- }}
- /> */}
<PartnersBiddingAttachmentsDialog
open={isAttachmentsDialogOpen}
diff --git a/lib/bidding/vendor/partners-bidding-participation-dialog.tsx b/lib/bidding/vendor/partners-bidding-participation-dialog.tsx
deleted file mode 100644
index e2376863..00000000
--- a/lib/bidding/vendor/partners-bidding-participation-dialog.tsx
+++ /dev/null
@@ -1,248 +0,0 @@
-'use client'
-
-import * as React from 'react'
-import { Button } from '@/components/ui/button'
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@/components/ui/dialog'
-import { Badge } from '@/components/ui/badge'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
-import { CheckCircle, XCircle, AlertCircle, Calendar, Package } from 'lucide-react'
-import { PartnersBiddingListItem } from '../detail/service'
-import { respondToPreQuoteInvitation, getBiddingCompaniesForPartners } from '../pre-quote/service'
-import { useToast } from '@/hooks/use-toast'
-import { useTransition } from 'react'
-import { formatDate } from '@/lib/utils'
-
-interface PartnersBiddingParticipationDialogProps {
- open: boolean
- onOpenChange: (open: boolean) => void
- bidding: PartnersBiddingListItem | null
- companyId: number
- onSuccess: () => void
-}
-
-export function PartnersBiddingParticipationDialog({
- open,
- onOpenChange,
- bidding,
- companyId,
- onSuccess
-}: PartnersBiddingParticipationDialogProps) {
- const { toast } = useToast()
- const [isPending, startTransition] = useTransition()
- const [selectedResponse, setSelectedResponse] = React.useState<'accepted' | 'declined' | null>(null)
-
- const handleSubmit = () => {
- if (!bidding || !selectedResponse) {
- toast({
- title: '오류',
- description: '참여 의사를 선택해주세요.',
- variant: 'destructive',
- })
- return
- }
-
- startTransition(async () => {
- try {
- // 먼저 해당 업체의 biddingCompanyId를 조회
- const biddingCompanyData = await getBiddingCompaniesForPartners(bidding.biddingId, companyId)
-
- if (!biddingCompanyData || !biddingCompanyData.biddingCompanyId) {
- toast({
- title: '오류',
- description: '입찰 업체 정보를 찾을 수 없습니다.',
- variant: 'destructive',
- })
- return
- }
-
- const result = await respondToPreQuoteInvitation(
- biddingCompanyData.biddingCompanyId,
- selectedResponse
- )
-
- if (result.success) {
- toast({
- title: '성공',
- description: result.message,
- })
- setSelectedResponse(null)
- onOpenChange(false)
- onSuccess()
- } else {
- toast({
- title: '오류',
- description: result.error,
- variant: 'destructive',
- })
- }
- } catch (error) {
- toast({
- title: '오류',
- description: '처리 중 오류가 발생했습니다.',
- variant: 'destructive',
- })
- }
- })
- }
-
- const handleOpenChange = (open: boolean) => {
- onOpenChange(open)
- if (!open) {
- setSelectedResponse(null)
- }
- }
-
- if (!bidding) return null
-
- return (
- <Dialog open={open} onOpenChange={handleOpenChange}>
- <DialogContent className="sm:max-w-[600px]">
- <DialogHeader>
- <DialogTitle className="flex items-center gap-2">
- <AlertCircle className="w-5 h-5" />
- 사전견적 참여 의사 결정
- </DialogTitle>
- <DialogDescription>
- 아래 입찰건에 대한 사전견적 참여 여부를 결정해주세요.
- </DialogDescription>
- </DialogHeader>
-
- <div className="py-4">
- {/* 입찰 정보 카드 */}
- <Card className="mb-6">
- <CardHeader>
- <CardTitle className="text-lg flex items-center gap-2">
- <Package className="w-5 h-5" />
- 입찰 정보
- </CardTitle>
- </CardHeader>
- <CardContent>
- <div className="space-y-3">
- <div>
- <strong>입찰번호:</strong> {bidding.biddingNumber}
- {bidding.revision > 0 && (
- <Badge variant="outline" className="ml-2">
- Rev.{bidding.revision}
- </Badge>
- )}
- </div>
- <div>
- <strong>입찰명:</strong> {bidding.title}
- </div>
- <div>
- <strong>품목명:</strong> {bidding.itemName}
- </div>
- <div>
- <strong>프로젝트:</strong> {bidding.projectName}
- </div>
- {bidding.preQuoteDate && (
- <div className="flex items-center gap-2">
- <Calendar className="w-4 h-4" />
- <strong>사전견적 마감일:</strong>
- <span className="text-red-600 font-semibold">
- {formatDate(bidding.preQuoteDate, 'KR')}
- </span>
- </div>
- )}
- <div>
- <strong>담당자:</strong> {bidding.managerName}
- </div>
- </div>
- </CardContent>
- </Card>
-
- {/* 참여 의사 선택 */}
- <div className="space-y-4">
- <h3 className="text-lg font-semibold">참여 의사를 선택해주세요:</h3>
-
- <div className="grid grid-cols-2 gap-4">
- {/* 참여 수락 */}
- <Card
- className={`cursor-pointer transition-all ${
- selectedResponse === 'accepted'
- ? 'ring-2 ring-green-500 bg-green-50'
- : 'hover:shadow-md'
- }`}
- onClick={() => setSelectedResponse('accepted')}
- >
- <CardContent className="p-6 text-center">
- <CheckCircle className="w-12 h-12 text-green-600 mx-auto mb-4" />
- <h4 className="text-lg font-semibold text-green-700 mb-2">
- 참여 수락
- </h4>
- <p className="text-sm text-gray-600">
- 사전견적에 참여하겠습니다.
- </p>
- </CardContent>
- </Card>
-
- {/* 참여 거절 */}
- <Card
- className={`cursor-pointer transition-all ${
- selectedResponse === 'declined'
- ? 'ring-2 ring-red-500 bg-red-50'
- : 'hover:shadow-md'
- }`}
- onClick={() => setSelectedResponse('declined')}
- >
- <CardContent className="p-6 text-center">
- <XCircle className="w-12 h-12 text-red-600 mx-auto mb-4" />
- <h4 className="text-lg font-semibold text-red-700 mb-2">
- 참여 거절
- </h4>
- <p className="text-sm text-gray-600">
- 사전견적에 참여하지 않겠습니다.
- </p>
- </CardContent>
- </Card>
- </div>
-
- {selectedResponse && (
- <div className="mt-4 p-4 rounded-lg bg-blue-50 border border-blue-200">
- <div className="flex items-center gap-2">
- <AlertCircle className="w-5 h-5 text-blue-600" />
- <span className="font-medium text-blue-800">
- {selectedResponse === 'accepted'
- ? '참여 수락을 선택하셨습니다.'
- : '참여 거절을 선택하셨습니다.'
- }
- </span>
- </div>
- <p className="text-sm text-blue-600 mt-1">
- {selectedResponse === 'accepted'
- ? '수락 후 사전견적서를 작성하실 수 있습니다.'
- : '거절 후에는 이 입찰건에 참여할 수 없습니다.'
- }
- </p>
- </div>
- )}
- </div>
- </div>
-
- <DialogFooter>
- <Button variant="outline" onClick={() => handleOpenChange(false)}>
- 취소
- </Button>
- <Button
- onClick={handleSubmit}
- disabled={isPending || !selectedResponse}
- className={selectedResponse === 'accepted' ? 'bg-green-600 hover:bg-green-700' :
- selectedResponse === 'declined' ? 'bg-red-600 hover:bg-red-700' : ''}
- >
- {selectedResponse === 'accepted' && <CheckCircle className="w-4 h-4 mr-2" />}
- {selectedResponse === 'declined' && <XCircle className="w-4 h-4 mr-2" />}
- {selectedResponse === 'accepted' ? '참여 수락' :
- selectedResponse === 'declined' ? '참여 거절' : '선택하세요'}
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- )
-}
diff --git a/lib/bidding/vendor/partners-bidding-pre-quote.tsx b/lib/bidding/vendor/partners-bidding-pre-quote.tsx
index 4ec65413..6a76ffa1 100644
--- a/lib/bidding/vendor/partners-bidding-pre-quote.tsx
+++ b/lib/bidding/vendor/partners-bidding-pre-quote.tsx
@@ -10,12 +10,18 @@ import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Checkbox } from '@/components/ui/checkbox'
import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select'
+import {
ArrowLeft,
Calendar,
Building2,
Package,
User,
- DollarSign,
FileText,
Users,
Send,
@@ -35,12 +41,11 @@ import {
} from '../pre-quote/service'
import { getBiddingConditions } from '../service'
import { getPriceAdjustmentFormByBiddingCompanyId } from '../detail/service'
+import { getIncotermsForSelection, getPaymentTermsForSelection, getPlaceOfShippingForSelection, getPlaceOfDestinationForSelection } from '@/lib/procurement-select/service'
import { PrItemsPricingTable } from './components/pr-items-pricing-table'
import { SimpleFileUpload } from './components/simple-file-upload'
import {
biddingStatusLabels,
- contractTypeLabels,
- biddingTypeLabels
} from '@/db/schema'
import { useToast } from '@/hooks/use-toast'
import { useTransition } from 'react'
@@ -63,7 +68,8 @@ interface BiddingDetail {
contractType: string
biddingType: string
awardCount: string
- contractPeriod: string | null
+ contractStartDate: Date | null
+ contractEndDate: Date | null
preQuoteDate: string | null
biddingRegistrationDate: string | null
submissionStartDate: string | null
@@ -105,6 +111,12 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
const [biddingDetail, setBiddingDetail] = React.useState<BiddingDetail | null>(null)
const [isLoading, setIsLoading] = React.useState(true)
const [biddingConditions, setBiddingConditions] = React.useState<any | null>(null)
+
+ // Procurement 데이터 상태들
+ const [paymentTermsOptions, setPaymentTermsOptions] = React.useState<Array<{code: string, description: string}>>([])
+ const [incotermsOptions, setIncotermsOptions] = React.useState<Array<{code: string, description: string}>>([])
+ const [shippingPlaces, setShippingPlaces] = React.useState<Array<{code: string, description: string}>>([])
+ const [destinationPlaces, setDestinationPlaces] = React.useState<Array<{code: string, description: string}>>([])
// 품목별 견적 관련 상태
const [prItems, setPrItems] = React.useState<any[]>([])
@@ -151,6 +163,43 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
})
const userId = session.data?.user?.id || ''
+ // Procurement 데이터 로드 함수들
+ const loadPaymentTerms = React.useCallback(async () => {
+ try {
+ const data = await getPaymentTermsForSelection();
+ setPaymentTermsOptions(data);
+ } catch (error) {
+ console.error("Failed to load payment terms:", error);
+ }
+ }, []);
+
+ const loadIncoterms = React.useCallback(async () => {
+ try {
+ const data = await getIncotermsForSelection();
+ setIncotermsOptions(data);
+ } catch (error) {
+ console.error("Failed to load incoterms:", error);
+ }
+ }, []);
+
+ const loadShippingPlaces = React.useCallback(async () => {
+ try {
+ const data = await getPlaceOfShippingForSelection();
+ setShippingPlaces(data);
+ } catch (error) {
+ console.error("Failed to load shipping places:", error);
+ }
+ }, []);
+
+ const loadDestinationPlaces = React.useCallback(async () => {
+ try {
+ const data = await getPlaceOfDestinationForSelection();
+ setDestinationPlaces(data);
+ } catch (error) {
+ console.error("Failed to load destination places:", error);
+ }
+ }, []);
+
// 데이터 로드
React.useEffect(() => {
const loadData = async () => {
@@ -229,6 +278,14 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
if (prItemsData) {
setPrItems(prItemsData)
}
+
+ // Procurement 데이터 로드
+ await Promise.all([
+ loadPaymentTerms(),
+ loadIncoterms(),
+ loadShippingPlaces(),
+ loadDestinationPlaces()
+ ])
} catch (error) {
console.error('Failed to load bidding company:', error)
toast({
@@ -242,7 +299,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
}
loadData()
- }, [biddingId, companyId, toast])
+ }, [biddingId, companyId, toast, loadPaymentTerms, loadIncoterms, loadShippingPlaces, loadDestinationPlaces])
// 임시저장 기능
const handleTempSave = () => {
@@ -428,7 +485,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
{ value: responseData.taxConditionsResponse, name: '응답 세금조건' },
{ value: responseData.incotermsResponse, name: '응답 운송조건' },
{ value: responseData.proposedShippingPort, name: '제안 선적지' },
- { value: responseData.proposedDestinationPort, name: '제안 도착지' },
+ { value: responseData.proposedDestinationPort, name: '제안 하역지' },
{ value: responseData.sparePartResponse, name: '스페어파트 응답' },
]
@@ -775,7 +832,7 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
</div>
<div>
- <Label className="text-muted-foreground">도착지</Label>
+ <Label className="text-muted-foreground">하역지</Label>
<div className="mt-1 p-3 bg-muted rounded-md">
<p className="font-medium">{biddingConditions.destinationPort || "미설정"}</p>
</div>
@@ -849,12 +906,13 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
</span>
</div>
{participationDecision === false && (
+ <>
<div className="p-4 bg-muted rounded-lg">
<p className="text-muted-foreground">
미참여로 설정되어 견적 작성 섹션이 숨겨집니다. 참여하시려면 아래 버튼을 클릭해주세요.
</p>
</div>
- )}
+
<Button
variant="outline"
size="sm"
@@ -863,6 +921,8 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
>
결정 변경하기
</Button>
+ </>
+ )}
</div>
)}
</CardContent>
@@ -961,12 +1021,27 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="paymentTermsResponse">응답 지급조건 <span className="text-red-500">*</span></Label>
- <Input
- id="paymentTermsResponse"
+ <Select
value={responseData.paymentTermsResponse}
- onChange={(e) => setResponseData({...responseData, paymentTermsResponse: e.target.value})}
- placeholder={biddingConditions?.paymentTerms ? `참고: ${biddingConditions.paymentTerms}` : "지급조건에 대한 의견을 입력하세요"}
- />
+ onValueChange={(value) => setResponseData({...responseData, paymentTermsResponse: value})}
+ >
+ <SelectTrigger>
+ <SelectValue placeholder={biddingConditions?.paymentTerms ? `참고: ${biddingConditions.paymentTerms}` : "지급조건 선택"} />
+ </SelectTrigger>
+ <SelectContent>
+ {paymentTermsOptions.length > 0 ? (
+ paymentTermsOptions.map((option) => (
+ <SelectItem key={option.code} value={option.code}>
+ {option.code} {option.description && `(${option.description})`}
+ </SelectItem>
+ ))
+ ) : (
+ <SelectItem value="loading" disabled>
+ 데이터를 불러오는 중...
+ </SelectItem>
+ )}
+ </SelectContent>
+ </Select>
</div>
<div className="space-y-2">
@@ -983,34 +1058,79 @@ export function PartnersBiddingPreQuote({ biddingId, companyId }: PartnersBiddin
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="incotermsResponse">응답 운송조건 <span className="text-red-500">*</span></Label>
- <Input
- id="incotermsResponse"
+ <Select
value={responseData.incotermsResponse}
- onChange={(e) => setResponseData({...responseData, incotermsResponse: e.target.value})}
- placeholder={biddingConditions?.incoterms ? `참고: ${biddingConditions.incoterms}` : "운송조건에 대한 의견을 입력하세요"}
- />
+ onValueChange={(value) => setResponseData({...responseData, incotermsResponse: value})}
+ >
+ <SelectTrigger>
+ <SelectValue placeholder={biddingConditions?.incoterms ? `참고: ${biddingConditions.incoterms}` : "운송조건 선택"} />
+ </SelectTrigger>
+ <SelectContent>
+ {incotermsOptions.length > 0 ? (
+ incotermsOptions.map((option) => (
+ <SelectItem key={option.code} value={option.code}>
+ {option.code} {option.description && `(${option.description})`}
+ </SelectItem>
+ ))
+ ) : (
+ <SelectItem value="loading" disabled>
+ 데이터를 불러오는 중...
+ </SelectItem>
+ )}
+ </SelectContent>
+ </Select>
</div>
<div className="space-y-2">
<Label htmlFor="proposedShippingPort">제안 선적지 <span className="text-red-500">*</span></Label>
- <Input
- id="proposedShippingPort"
+ <Select
value={responseData.proposedShippingPort}
- onChange={(e) => setResponseData({...responseData, proposedShippingPort: e.target.value})}
- placeholder={biddingConditions?.shippingPort ? `참고: ${biddingConditions.shippingPort}` : "선적지를 입력하세요"}
- />
+ onValueChange={(value) => setResponseData({...responseData, proposedShippingPort: value})}
+ >
+ <SelectTrigger>
+ <SelectValue placeholder={biddingConditions?.shippingPort ? `참고: ${biddingConditions.shippingPort}` : "선적지 선택"} />
+ </SelectTrigger>
+ <SelectContent>
+ {shippingPlaces.length > 0 ? (
+ shippingPlaces.map((place) => (
+ <SelectItem key={place.code} value={place.code}>
+ {place.code} {place.description && `(${place.description})`}
+ </SelectItem>
+ ))
+ ) : (
+ <SelectItem value="loading" disabled>
+ 데이터를 불러오는 중...
+ </SelectItem>
+ )}
+ </SelectContent>
+ </Select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
- <Label htmlFor="proposedDestinationPort">제안 도착지 <span className="text-red-500">*</span></Label>
- <Input
- id="proposedDestinationPort"
+ <Label htmlFor="proposedDestinationPort">제안 하역지 <span className="text-red-500">*</span></Label>
+ <Select
value={responseData.proposedDestinationPort}
- onChange={(e) => setResponseData({...responseData, proposedDestinationPort: e.target.value})}
- placeholder={biddingConditions?.destinationPort ? `참고: ${biddingConditions.destinationPort}` : "도착지를 입력하세요"}
- />
+ onValueChange={(value) => setResponseData({...responseData, proposedDestinationPort: value})}
+ >
+ <SelectTrigger>
+ <SelectValue placeholder={biddingConditions?.destinationPort ? `참고: ${biddingConditions.destinationPort}` : "하역지 선택"} />
+ </SelectTrigger>
+ <SelectContent>
+ {destinationPlaces.length > 0 ? (
+ destinationPlaces.map((place) => (
+ <SelectItem key={place.code} value={place.code}>
+ {place.code} {place.description && `(${place.description})`}
+ </SelectItem>
+ ))
+ ) : (
+ <SelectItem value="loading" disabled>
+ 데이터를 불러오는 중...
+ </SelectItem>
+ )}
+ </SelectContent>
+ </Select>
</div>
<div className="space-y-2">
diff --git a/lib/bidding/vendor/partners-bidding-toolbar-actions.tsx b/lib/bidding/vendor/partners-bidding-toolbar-actions.tsx
index 1500f6a7..87b1367e 100644
--- a/lib/bidding/vendor/partners-bidding-toolbar-actions.tsx
+++ b/lib/bidding/vendor/partners-bidding-toolbar-actions.tsx
@@ -2,45 +2,39 @@
import * as React from "react"
import { type Table } from "@tanstack/react-table"
-import { Users, CheckCircle, XCircle } from "lucide-react"
+import { Users} from "lucide-react"
import { Button } from "@/components/ui/button"
import { PartnersBiddingListItem } from '../detail/service'
interface PartnersBiddingToolbarActionsProps {
table: Table<PartnersBiddingListItem>
- companyId: number
- onRefresh: () => void
setRowAction?: (action: { type: string; row: { original: PartnersBiddingListItem } }) => void
}
export function PartnersBiddingToolbarActions({
- table,
- companyId,
- onRefresh,
+ table,
setRowAction
}: PartnersBiddingToolbarActionsProps) {
// 선택된 행 가져오기 (단일 선택)
const selectedRows = table.getFilteredSelectedRowModel().rows
const selectedBidding = selectedRows.length === 1 ? selectedRows[0].original : null
- const handleAttendanceClick = () => {
+ const handleSpecificationMeetingClick = () => {
if (selectedBidding && setRowAction) {
setRowAction({
- type: 'attendance',
+ type: 'specification_meeting',
row: { original: selectedBidding }
})
}
}
-
-
return (
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
- onClick={handleAttendanceClick}
+ onClick={handleSpecificationMeetingClick}
className="flex items-center gap-2"
>
<Users className="w-4 h-4" />