summaryrefslogtreecommitdiff
path: root/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-columns.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-04 08:31:31 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-04 08:31:31 +0000
commitb67e36df49f067cbd5ba899f9fbcc755f38d4b4f (patch)
tree5a71c5960f90d988cd509e3ef26bff497a277661 /lib/bidding/pre-quote/table/bidding-pre-quote-vendor-columns.tsx
parentb7f54b06c1ef9e619f5358fb0a5caad9703c8905 (diff)
(대표님, 최겸, 임수민) 작업사항 커밋
Diffstat (limited to 'lib/bidding/pre-quote/table/bidding-pre-quote-vendor-columns.tsx')
-rw-r--r--lib/bidding/pre-quote/table/bidding-pre-quote-vendor-columns.tsx303
1 files changed, 303 insertions, 0 deletions
diff --git a/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-columns.tsx b/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-columns.tsx
new file mode 100644
index 00000000..30cddbce
--- /dev/null
+++ b/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-columns.tsx
@@ -0,0 +1,303 @@
+"use client"
+
+import * as React from "react"
+import { type ColumnDef } from "@tanstack/react-table"
+import { Checkbox } from "@/components/ui/checkbox"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import {
+ MoreHorizontal, Edit, Trash2, UserPlus
+} from "lucide-react"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+
+// bidding_companies 테이블 타입 정의 (company_condition_responses와 join)
+export interface BiddingCompany {
+ id: number
+ biddingId: number
+ companyId: number
+ invitationStatus: 'pending' | 'sent' | 'accepted' | 'declined' | 'submitted'
+ invitedAt: Date | null
+ respondedAt: Date | null
+ preQuoteAmount: string | null
+ preQuoteSubmittedAt: Date | null
+ isPreQuoteSelected: boolean
+ isAttendingMeeting: boolean | null
+ notes: string | null
+ contactPerson: string | null
+ contactEmail: string | null
+ contactPhone: string | null
+ createdAt: Date
+ updatedAt: Date
+
+ // company_condition_responses 필드들
+ paymentTermsResponse: string | null
+ taxConditionsResponse: string | null
+ proposedContractDeliveryDate: string | null
+ priceAdjustmentResponse: boolean | null
+ isInitialResponse: boolean | null
+ incotermsResponse: string | null
+ proposedShippingPort: string | null
+ proposedDestinationPort: string | null
+ sparePartResponse: string | null
+ additionalProposals: string | null
+
+ // 조인된 업체 정보
+ companyName?: string
+ companyCode?: string
+}
+
+interface GetBiddingCompanyColumnsProps {
+ onEdit: (company: BiddingCompany) => void
+ onDelete: (company: BiddingCompany) => void
+ onInvite: (company: BiddingCompany) => void
+}
+
+export function getBiddingPreQuoteVendorColumns({
+ onEdit,
+ onDelete,
+ onInvite
+}: GetBiddingCompanyColumnsProps): ColumnDef<BiddingCompany>[] {
+ return [
+ {
+ id: 'select',
+ header: ({ table }) => (
+ <Checkbox
+ checked={table.getIsAllPageRowsSelected()}
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
+ aria-label="모두 선택"
+ />
+ ),
+ cell: ({ row }) => (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
+ aria-label="행 선택"
+ />
+ ),
+ enableSorting: false,
+ enableHiding: false,
+ },
+ {
+ accessorKey: 'companyName',
+ header: '업체명',
+ cell: ({ row }) => (
+ <div className="font-medium">{row.original.companyName || '-'}</div>
+ ),
+ },
+ {
+ accessorKey: 'companyCode',
+ header: '업체코드',
+ cell: ({ row }) => (
+ <div className="font-mono text-sm">{row.original.companyCode || '-'}</div>
+ ),
+ },
+ {
+ accessorKey: 'invitationStatus',
+ header: '초대 상태',
+ cell: ({ row }) => {
+ const status = row.original.invitationStatus
+ const variant = status === 'accepted' ? 'default' :
+ status === 'declined' ? 'destructive' : 'outline'
+
+ const label = status === 'accepted' ? '수락' :
+ status === 'declined' ? '거절' : '대기중'
+
+ return <Badge variant={variant}>{label}</Badge>
+ },
+ },
+ {
+ accessorKey: 'preQuoteAmount',
+ header: '사전견적금액',
+ cell: ({ row }) => (
+ <div className="text-right font-mono">
+ {row.original.preQuoteAmount ? Number(row.original.preQuoteAmount).toLocaleString() : '-'} KRW
+ </div>
+ ),
+ },
+ {
+ accessorKey: 'preQuoteSubmittedAt',
+ header: '사전견적 제출일',
+ cell: ({ row }) => (
+ <div className="text-sm">
+ {row.original.preQuoteSubmittedAt ? new Date(row.original.preQuoteSubmittedAt).toLocaleDateString('ko-KR') : '-'}
+ </div>
+ ),
+ },
+ {
+ accessorKey: 'isPreQuoteSelected',
+ header: '본입찰 선정',
+ cell: ({ row }) => (
+ <Badge variant={row.original.isPreQuoteSelected ? 'default' : 'secondary'}>
+ {row.original.isPreQuoteSelected ? '선정' : '미선정'}
+ </Badge>
+ ),
+ },
+ {
+ accessorKey: 'isAttendingMeeting',
+ header: '사양설명회 참석',
+ cell: ({ row }) => {
+ const isAttending = row.original.isAttendingMeeting
+ if (isAttending === null) return <div className="text-sm">-</div>
+ return (
+ <Badge variant={isAttending ? 'default' : 'secondary'}>
+ {isAttending ? '참석' : '불참석'}
+ </Badge>
+ )
+ },
+ },
+ {
+ accessorKey: 'paymentTermsResponse',
+ header: '지급조건',
+ cell: ({ row }) => (
+ <div className="text-sm max-w-32 truncate" title={row.original.paymentTermsResponse || ''}>
+ {row.original.paymentTermsResponse || '-'}
+ </div>
+ ),
+ },
+ {
+ accessorKey: 'taxConditionsResponse',
+ header: '세금조건',
+ cell: ({ row }) => (
+ <div className="text-sm max-w-32 truncate" title={row.original.taxConditionsResponse || ''}>
+ {row.original.taxConditionsResponse || '-'}
+ </div>
+ ),
+ },
+ {
+ accessorKey: 'incotermsResponse',
+ header: '운송조건',
+ cell: ({ row }) => (
+ <div className="text-sm max-w-24 truncate" title={row.original.incotermsResponse || ''}>
+ {row.original.incotermsResponse || '-'}
+ </div>
+ ),
+ },
+ {
+ accessorKey: 'isInitialResponse',
+ header: '초도여부',
+ cell: ({ row }) => {
+ const isInitial = row.original.isInitialResponse
+ if (isInitial === null) return <div className="text-sm">-</div>
+ return (
+ <Badge variant={isInitial ? 'default' : 'secondary'}>
+ {isInitial ? 'Y' : 'N'}
+ </Badge>
+ )
+ },
+ },
+ {
+ accessorKey: 'priceAdjustmentResponse',
+ header: '연동제',
+ cell: ({ row }) => {
+ const hasPriceAdjustment = row.original.priceAdjustmentResponse
+ if (hasPriceAdjustment === null) return <div className="text-sm">-</div>
+ return (
+ <Badge variant={hasPriceAdjustment ? 'default' : 'secondary'}>
+ {hasPriceAdjustment ? '적용' : '미적용'}
+ </Badge>
+ )
+ },
+ },
+ {
+ accessorKey: 'proposedContractDeliveryDate',
+ header: '제안납기일',
+ cell: ({ row }) => (
+ <div className="text-sm">
+ {row.original.proposedContractDeliveryDate ?
+ new Date(row.original.proposedContractDeliveryDate).toLocaleDateString('ko-KR') : '-'}
+ </div>
+ ),
+ },
+ {
+ accessorKey: 'proposedShippingPort',
+ header: '제안선적지',
+ cell: ({ row }) => (
+ <div className="text-sm max-w-24 truncate" title={row.original.proposedShippingPort || ''}>
+ {row.original.proposedShippingPort || '-'}
+ </div>
+ ),
+ },
+ {
+ accessorKey: 'proposedDestinationPort',
+ header: '제안도착지',
+ cell: ({ row }) => (
+ <div className="text-sm max-w-24 truncate" title={row.original.proposedDestinationPort || ''}>
+ {row.original.proposedDestinationPort || '-'}
+ </div>
+ ),
+ },
+ {
+ accessorKey: 'sparePartResponse',
+ header: '스페어파트',
+ cell: ({ row }) => (
+ <div className="text-sm max-w-24 truncate" title={row.original.sparePartResponse || ''}>
+ {row.original.sparePartResponse || '-'}
+ </div>
+ ),
+ },
+ {
+ accessorKey: 'additionalProposals',
+ header: '추가제안',
+ cell: ({ row }) => (
+ <div className="text-sm max-w-32 truncate" title={row.original.additionalProposals || ''}>
+ {row.original.additionalProposals || '-'}
+ </div>
+ ),
+ },
+ {
+ accessorKey: 'notes',
+ header: '특이사항',
+ cell: ({ row }) => (
+ <div className="text-sm max-w-32 truncate" title={row.original.notes || ''}>
+ {row.original.notes || '-'}
+ </div>
+ ),
+ },
+ {
+ id: 'actions',
+ header: '작업',
+ cell: ({ row }) => {
+ const company = row.original
+
+ return (
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button variant="ghost" className="h-8 w-8 p-0">
+ <span className="sr-only">메뉴 열기</span>
+ <MoreHorizontal className="h-4 w-4" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end">
+ <DropdownMenuLabel>작업</DropdownMenuLabel>
+ <DropdownMenuItem onClick={() => onEdit(company)}>
+ <Edit className="mr-2 h-4 w-4" />
+ 수정
+ </DropdownMenuItem>
+ {company.invitationStatus === 'pending' && (
+ <DropdownMenuItem onClick={() => onInvite(company)}>
+ <UserPlus className="mr-2 h-4 w-4" />
+ 초대 발송
+ </DropdownMenuItem>
+ )}
+ <DropdownMenuSeparator />
+ <DropdownMenuItem
+ onClick={() => onDelete(company)}
+ className="text-destructive"
+ >
+ <Trash2 className="mr-2 h-4 w-4" />
+ 삭제
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ )
+ },
+ },
+ ]
+}