diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-17 10:40:12 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-17 10:40:12 +0000 |
| commit | 10cb50753ccf318024c4394282f9e8d968dcd1a5 (patch) | |
| tree | cf4edb96aa172c3b90d88532aff1f536944a2283 /lib/bidding/vendor | |
| parent | f7117370b9cc0c7b96bd1eb23a1b9f5b16cc8ceb (diff) | |
(최겸) 구매 입찰 오류 수정 및 선적지,하역지 연동,TO Cont, TO PO 개발
Diffstat (limited to 'lib/bidding/vendor')
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" /> |
