diff options
Diffstat (limited to 'components/bidding/receive')
| -rw-r--r-- | components/bidding/receive/bidding-participants-dialog.tsx | 216 |
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> + ) +} + |
