summaryrefslogtreecommitdiff
path: root/components/bidding/receive/bidding-participants-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/bidding/receive/bidding-participants-dialog.tsx')
-rw-r--r--components/bidding/receive/bidding-participants-dialog.tsx216
1 files changed, 216 insertions, 0 deletions
diff --git a/components/bidding/receive/bidding-participants-dialog.tsx b/components/bidding/receive/bidding-participants-dialog.tsx
new file mode 100644
index 00000000..5739a07e
--- /dev/null
+++ b/components/bidding/receive/bidding-participants-dialog.tsx
@@ -0,0 +1,216 @@
+'use client'
+
+import * as React from 'react'
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+import { DataTable } from '@/components/data-table/data-table'
+import { ColumnDef } from '@tanstack/react-table'
+import { Building2, User, Mail, Phone, Calendar, BadgeCheck } from 'lucide-react'
+import { formatDate } from '@/lib/utils'
+import { Badge } from '@/components/ui/badge'
+
+interface ParticipantCompany {
+ id: number
+ biddingId: number
+ companyId: number | null
+ vendorName: string
+ vendorCode: string
+ contactPerson: string | null
+ contactEmail: string | null
+ contactPhone: string | null
+ invitationStatus: string
+ updatedAt: Date | null
+}
+
+interface BiddingParticipantsDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ biddingId: number | null
+ participantType: 'expected' | 'participated' | 'declined' | 'pending' | null
+ companies: ParticipantCompany[]
+}
+
+const invitationStatusLabels: Record<string, { label: string; variant: 'default' | 'secondary' | 'destructive' | 'outline' }> = {
+ invited: { label: '초대됨', variant: 'outline' },
+ accepted: { label: '참여확정', variant: 'default' },
+ declined: { label: '포기', variant: 'destructive' },
+ pending: { label: '미제출', variant: 'secondary' },
+ submitted: { label: '제출완료', variant: 'default' },
+}
+
+const participantTypeLabels: Record<string, string> = {
+ expected: '참여예정협력사',
+ participated: '참여협력사',
+ declined: '포기협력사',
+ pending: '미제출협력사',
+}
+
+export function BiddingParticipantsDialog({
+ open,
+ onOpenChange,
+ biddingId,
+ participantType,
+ companies,
+}: BiddingParticipantsDialogProps) {
+ const columns = React.useMemo<ColumnDef<ParticipantCompany>[]>(
+ () => [
+ {
+ id: 'vendorCode',
+ accessorKey: 'vendorCode',
+ header: '협력사코드',
+ cell: ({ row }) => (
+ <div className="flex items-center gap-2">
+ <BadgeCheck className="h-4 w-4 text-muted-foreground" />
+ <span className="font-mono text-sm">{row.original.vendorCode}</span>
+ </div>
+ ),
+ size: 120,
+ },
+ {
+ id: 'vendorName',
+ accessorKey: 'vendorName',
+ header: '협력사명',
+ cell: ({ row }) => (
+ <div className="flex items-center gap-2">
+ <Building2 className="h-4 w-4 text-muted-foreground" />
+ <span className="font-medium">{row.original.vendorName}</span>
+ </div>
+ ),
+ size: 200,
+ },
+ {
+ id: 'invitationStatus',
+ accessorKey: 'invitationStatus',
+ header: '구분',
+ cell: ({ row }) => {
+ const status = row.original.invitationStatus
+ const statusInfo = invitationStatusLabels[status] || { label: status, variant: 'outline' as const }
+ return (
+ <Badge variant={statusInfo.variant}>
+ {statusInfo.label}
+ </Badge>
+ )
+ },
+ size: 100,
+ },
+ {
+ id: 'updatedAt',
+ accessorKey: 'updatedAt',
+ header: '응찰/포기일시',
+ cell: ({ row }) => (
+ <div className="flex items-center gap-2">
+ <Calendar className="h-4 w-4 text-muted-foreground" />
+ <span className="text-sm">
+ {row.original.updatedAt ? formatDate(row.original.updatedAt) : '-'}
+ </span>
+ </div>
+ ),
+ size: 150,
+ },
+ {
+ id: 'contactPerson',
+ accessorKey: 'contactPerson',
+ header: '협력사 담당자',
+ cell: ({ row }) => (
+ <div className="flex items-center gap-2">
+ <User className="h-4 w-4 text-muted-foreground" />
+ <span className="text-sm">{row.original.contactPerson || '-'}</span>
+ </div>
+ ),
+ size: 120,
+ },
+ {
+ id: 'contactEmail',
+ accessorKey: 'contactEmail',
+ header: '담당자 이메일',
+ cell: ({ row }) => (
+ <div className="flex items-center gap-2">
+ <Mail className="h-4 w-4 text-muted-foreground" />
+ <span className="text-sm text-muted-foreground">{row.original.contactEmail || '-'}</span>
+ </div>
+ ),
+ size: 200,
+ },
+ {
+ id: 'contactPhone',
+ accessorKey: 'contactPhone',
+ header: '담당자 전화번호',
+ cell: ({ row }) => (
+ <div className="flex items-center gap-2">
+ <Phone className="h-4 w-4 text-muted-foreground" />
+ <span className="text-sm text-muted-foreground">{row.original.contactPhone || '-'}</span>
+ </div>
+ ),
+ size: 150,
+ },
+ ],
+ []
+ )
+
+ const table = React.useMemo(
+ () => ({
+ data: companies,
+ columns,
+ pageCount: 1,
+ }),
+ [companies, columns]
+ )
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-6xl max-h-[80vh] overflow-hidden flex flex-col">
+ <DialogHeader>
+ <DialogTitle>
+ {'협력사 목록'}
+ {' '}
+ <span className="text-muted-foreground">({companies.length}개)</span>
+ </DialogTitle>
+ </DialogHeader>
+ <div className="flex-1 overflow-auto">
+ <div className="border rounded-lg">
+ <div className="overflow-x-auto">
+ <table className="w-full">
+ <thead className="bg-muted/50">
+ <tr>
+ {columns.map((column) => (
+ <th
+ key={column.id}
+ className="px-4 py-3 text-left text-sm font-medium"
+ style={{ width: column.size }}
+ >
+ {typeof column.header === 'function'
+ ? column.header({} as any)
+ : column.header}
+ </th>
+ ))}
+ </tr>
+ </thead>
+ <tbody>
+ {companies.length === 0 ? (
+ <tr>
+ <td colSpan={columns.length} className="px-4 py-8 text-center text-muted-foreground">
+ 협력사가 없습니다.
+ </td>
+ </tr>
+ ) : (
+ companies.map((company) => (
+ <tr key={company.id} className="border-t hover:bg-muted/50">
+ {columns.map((column) => (
+ <td key={column.id} className="px-4 py-3">
+ {column.cell
+ ? column.cell({ row: { original: company } } as any)
+ : (company as any)[column.accessorKey as string]}
+ </td>
+ ))}
+ </tr>
+ ))
+ )}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ </DialogContent>
+ </Dialog>
+ )
+}
+