diff options
Diffstat (limited to 'lib/bidding/detail/table')
6 files changed, 131 insertions, 79 deletions
diff --git a/lib/bidding/detail/table/bidding-detail-content.tsx b/lib/bidding/detail/table/bidding-detail-content.tsx index 91bea2f4..a96509a9 100644 --- a/lib/bidding/detail/table/bidding-detail-content.tsx +++ b/lib/bidding/detail/table/bidding-detail-content.tsx @@ -7,6 +7,10 @@ import { QuotationDetails, QuotationVendor } from '@/lib/bidding/detail/service' import { BiddingDetailVendorTableContent } from './bidding-detail-vendor-table' import { BiddingDetailItemsDialog } from './bidding-detail-items-dialog' import { BiddingDetailTargetPriceDialog } from './bidding-detail-target-price-dialog' +import { BiddingPreQuoteItemDetailsDialog } from '../../../bidding/pre-quote/table/bidding-pre-quote-item-details-dialog' +import { getPrItemsForBidding } from '../../../bidding/pre-quote/service' +import { useToast } from '@/hooks/use-toast' +import { useTransition } from 'react' interface BiddingDetailContentProps { bidding: Bidding @@ -21,6 +25,9 @@ export function BiddingDetailContent({ quotationVendors, prItems }: BiddingDetailContentProps) { + const { toast } = useToast() + const [isPending, startTransition] = useTransition() + const [dialogStates, setDialogStates] = React.useState({ items: false, targetPrice: false, @@ -29,6 +36,11 @@ export function BiddingDetailContent({ }) const [refreshTrigger, setRefreshTrigger] = React.useState(0) + + // PR 아이템 다이얼로그 관련 state + const [isItemDetailsDialogOpen, setIsItemDetailsDialogOpen] = React.useState(false) + const [selectedVendorForDetails, setSelectedVendorForDetails] = React.useState<QuotationVendor | null>(null) + const [prItemsForDialog, setPrItemsForDialog] = React.useState<any[]>([]) const handleRefresh = React.useCallback(() => { setRefreshTrigger(prev => prev + 1) @@ -42,6 +54,25 @@ export function BiddingDetailContent({ setDialogStates(prev => ({ ...prev, [type]: false })) }, []) + const handleViewItemDetails = React.useCallback((vendor: QuotationVendor) => { + startTransition(async () => { + try { + // PR 아이템 정보 로드 + const prItemsData = await getPrItemsForBidding(bidding.id) + setPrItemsForDialog(prItemsData) + setSelectedVendorForDetails(vendor) + setIsItemDetailsDialogOpen(true) + } catch (error) { + console.error('Failed to load PR items:', error) + toast({ + title: '오류', + description: '품목 정보를 불러오는데 실패했습니다.', + variant: 'destructive', + }) + } + }) + }, [bidding.id, toast]) + return ( <div className="space-y-6"> <BiddingDetailVendorTableContent @@ -53,6 +84,7 @@ export function BiddingDetailContent({ onOpenTargetPriceDialog={() => openDialog('targetPrice')} onOpenSelectionReasonDialog={() => openDialog('selectionReason')} onOpenAwardDialog={() => openDialog('award')} + onViewItemDetails={handleViewItemDetails} onEdit={undefined} onDelete={undefined} onSelectWinner={undefined} @@ -72,6 +104,16 @@ export function BiddingDetailContent({ bidding={bidding} onSuccess={handleRefresh} /> + + <BiddingPreQuoteItemDetailsDialog + open={isItemDetailsDialogOpen} + onOpenChange={setIsItemDetailsDialogOpen} + biddingId={bidding.id} + biddingCompanyId={selectedVendorForDetails?.id || 0} + companyName={selectedVendorForDetails?.vendorName || ''} + prItems={prItemsForDialog} + currency={bidding.currency || 'KRW'} + /> </div> ) } diff --git a/lib/bidding/detail/table/bidding-detail-header.tsx b/lib/bidding/detail/table/bidding-detail-header.tsx index fcbbeb9a..2798478c 100644 --- a/lib/bidding/detail/table/bidding-detail-header.tsx +++ b/lib/bidding/detail/table/bidding-detail-header.tsx @@ -141,50 +141,6 @@ export function BiddingDetailHeader({ bidding }: BiddingDetailHeaderProps) { }) } - const getActionButtons = () => { - const buttons = [] - - // 기본 액션 버튼들 (항상 표시) - - - // 모든 액션 버튼을 항상 표시 (상태 검증은 각 핸들러에서) - buttons.push( - <Button - key="register" - onClick={handleRegister} - disabled={isPending} - > - <Send className="w-4 h-4 mr-2" /> - 입찰등록 - </Button> - ) - - buttons.push( - <Button - key="disposal" - variant="destructive" - onClick={handleMarkAsDisposal} - disabled={isPending} - > - <XCircle className="w-4 h-4 mr-2" /> - 유찰 - </Button> - ) - - buttons.push( - <Button - key="rebidding" - onClick={handleCreateRebidding} - disabled={isPending} - > - <RotateCcw className="w-4 h-4 mr-2" /> - 재입찰 - </Button> - ) - - return buttons - } - return ( <div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> <div className="px-6 py-4"> @@ -209,11 +165,6 @@ export function BiddingDetailHeader({ bidding }: BiddingDetailHeaderProps) { </Badge> </div> </div> - - {/* 액션 버튼들 */} - <div className="flex items-center gap-2 flex-shrink-0"> - {getActionButtons()} - </div> </div> </div> diff --git a/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx b/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx index bb1d2c62..cbdf79c2 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx @@ -23,6 +23,7 @@ interface GetVendorColumnsProps { onDelete: (vendor: QuotationVendor) => void onSelectWinner: (vendor: QuotationVendor) => void onViewPriceAdjustment?: (vendor: QuotationVendor) => void + onViewItemDetails?: (vendor: QuotationVendor) => void onSendBidding?: (vendor: QuotationVendor) => void onUpdateParticipation?: (vendor: QuotationVendor, participated: boolean) => void } @@ -32,6 +33,7 @@ export function getBiddingDetailVendorColumns({ onDelete, onSelectWinner, onViewPriceAdjustment, + onViewItemDetails, onSendBidding, onUpdateParticipation }: GetVendorColumnsProps): ColumnDef<QuotationVendor>[] { @@ -72,11 +74,24 @@ export function getBiddingDetailVendorColumns({ { accessorKey: 'quotationAmount', header: '견적금액', - cell: ({ row }) => ( - <div className="text-right font-mono"> - {row.original.quotationAmount ? Number(row.original.quotationAmount).toLocaleString() : '-'} {row.original.currency} - </div> - ), + cell: ({ row }) => { + const hasAmount = row.original.quotationAmount && Number(row.original.quotationAmount) > 0 + return ( + <div className="text-right font-mono"> + {hasAmount ? ( + <button + onClick={() => onViewItemDetails?.(row.original)} + className="text-primary hover:text-primary/80 hover:underline cursor-pointer" + title="품목별 견적 상세 보기" + > + {Number(row.original.quotationAmount).toLocaleString()} {row.original.currency} + </button> + ) : ( + <span className="text-muted-foreground">- {row.original.currency}</span> + )} + </div> + ) + }, }, { accessorKey: 'biddingResult', @@ -84,7 +99,7 @@ export function getBiddingDetailVendorColumns({ cell: ({ row }) => { const isWinner = row.original.isWinner if (isWinner === null || isWinner === undefined) { - return <div>-</div> + return <div>미정</div> } return ( <Badge variant={isWinner ? 'default' : 'secondary'} className={isWinner ? 'bg-green-600' : ''}> @@ -158,12 +173,24 @@ export function getBiddingDetailVendorColumns({ </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuLabel>작업</DropdownMenuLabel> - <DropdownMenuItem onClick={() => onEdit(vendor)}> + <DropdownMenuItem + onClick={() => onEdit(vendor)} + disabled={vendor.isBiddingParticipated !== true} + > 발주비율 산정 + {vendor.isBiddingParticipated !== true && ( + <span className="text-xs text-muted-foreground ml-2">(입찰참여 필요)</span> + )} </DropdownMenuItem> {vendor.status !== 'selected' && ( - <DropdownMenuItem onClick={() => onSelectWinner(vendor)}> + <DropdownMenuItem + onClick={() => onSelectWinner(vendor)} + disabled={vendor.isBiddingParticipated !== true} + > 낙찰 선정 + {vendor.isBiddingParticipated !== true && ( + <span className="text-xs text-muted-foreground ml-2">(입찰참여 필요)</span> + )} </DropdownMenuItem> )} diff --git a/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx b/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx index b10212ab..9a5408c2 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx @@ -112,6 +112,22 @@ export function BiddingDetailVendorEditDialog({ )} {/* 수정 가능한 필드들 */} + {vendor && vendor.isBiddingParticipated !== true && ( + <div className="bg-orange-50 border border-orange-200 rounded-lg p-3 mb-4"> + <div className="flex items-center gap-2 text-orange-800"> + <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"> + <path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" /> + </svg> + <span className="font-medium">입찰 참여 안내</span> + </div> + <p className="text-sm text-orange-700 mt-1"> + {vendor.isBiddingParticipated === null + ? '이 업체는 아직 입찰참여 여부가 결정되지 않았습니다. 입찰에 참여한 업체만 발주비율을 설정할 수 있습니다.' + : '이 업체는 입찰에 참여하지 않습니다. 발주비율을 설정할 수 없습니다.' + } + </p> + </div> + )} <div className="space-y-2"> <Label htmlFor="edit-awardRatio">발주비율 (%)</Label> @@ -123,14 +139,23 @@ export function BiddingDetailVendorEditDialog({ value={formData.awardRatio} onChange={(e) => setFormData({ ...formData, awardRatio: Number(e.target.value) })} placeholder="발주비율을 입력하세요" + disabled={vendor?.isBiddingParticipated !== true} /> + {vendor?.isBiddingParticipated !== true && ( + <p className="text-sm text-muted-foreground"> + 입찰에 참여한 업체만 발주비율을 설정할 수 있습니다. + </p> + )} </div> </div> <DialogFooter> <Button variant="outline" onClick={() => onOpenChange(false)}> 취소 </Button> - <Button onClick={handleEdit} disabled={isPending}> + <Button + onClick={handleEdit} + disabled={isPending || vendor?.isBiddingParticipated !== true} + > 산정 </Button> </DialogFooter> diff --git a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx index dd1ae94b..95f63ce9 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx @@ -1,6 +1,7 @@ 'use client' import * as React from 'react' +import { useSession } from 'next-auth/react' import { type DataTableAdvancedFilterField, type DataTableFilterField } from '@/types/table' import { useDataTable } from '@/hooks/use-data-table' import { DataTable } from '@/components/data-table/data-table' @@ -33,6 +34,7 @@ interface BiddingDetailVendorTableContentProps { onEdit?: (vendor: QuotationVendor) => void onDelete?: (vendor: QuotationVendor) => void onSelectWinner?: (vendor: QuotationVendor) => void + onViewItemDetails?: (vendor: QuotationVendor) => void } const filterFields: DataTableFilterField<QuotationVendor>[] = [ @@ -97,10 +99,15 @@ export function BiddingDetailVendorTableContent({ onOpenAwardDialog, onEdit, onDelete, - onSelectWinner + onSelectWinner, + onViewItemDetails }: BiddingDetailVendorTableContentProps) { + const { data: session } = useSession() const { toast } = useToast() const [isPending, startTransition] = useTransition() + + // 세션에서 사용자 ID 가져오기 + const userId = session?.user?.id || '' const [selectedVendor, setSelectedVendor] = React.useState<QuotationVendor | null>(null) const [isEditDialogOpen, setIsEditDialogOpen] = React.useState(false) const [isAwardDialogOpen, setIsAwardDialogOpen] = React.useState(false) @@ -145,7 +152,7 @@ export function BiddingDetailVendorTableContent({ const result = selectWinnerSchema.safeParse({ biddingId, vendorId: vendor.id, - awardRatio: vendor.awardRatio, + awardRatio: vendor.awardRatio || 0, }) if (!result.success) { @@ -157,7 +164,7 @@ export function BiddingDetailVendorTableContent({ return } - const response = await selectWinner(biddingId, vendor.id, vendor.awardRatio, 'current-user') + const response = await selectWinner(biddingId, vendor.id, vendor.awardRatio || 0, userId) if (response.success) { toast({ @@ -209,9 +216,10 @@ export function BiddingDetailVendorTableContent({ onEdit: onEdit || handleEdit, onDelete: onDelete || handleDelete, onSelectWinner: onSelectWinner || handleSelectWinner, - onViewPriceAdjustment: handleViewPriceAdjustment + onViewPriceAdjustment: handleViewPriceAdjustment, + onViewItemDetails: onViewItemDetails }), - [onEdit, onDelete, onSelectWinner, handleEdit, handleDelete, handleSelectWinner, handleViewPriceAdjustment] + [onEdit, onDelete, onSelectWinner, handleEdit, handleDelete, handleSelectWinner, handleViewPriceAdjustment, onViewItemDetails] ) const { table } = useDataTable({ @@ -241,9 +249,9 @@ export function BiddingDetailVendorTableContent({ table={table} biddingId={biddingId} bidding={bidding} + userId={userId} onOpenItemsDialog={onOpenItemsDialog} onOpenTargetPriceDialog={onOpenTargetPriceDialog} - onOpenSelectionReasonDialog={onOpenSelectionReasonDialog} onOpenAwardDialog={() => setIsAwardDialogOpen(true)} onSuccess={onRefresh} /> diff --git a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx index 8cdec191..64c31633 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx @@ -15,6 +15,7 @@ interface BiddingDetailVendorToolbarActionsProps { table: Table<QuotationVendor> biddingId: number bidding: Bidding + userId: string onOpenItemsDialog: () => void onOpenTargetPriceDialog: () => void onOpenAwardDialog: () => void @@ -25,6 +26,7 @@ export function BiddingDetailVendorToolbarActions({ table, biddingId, bidding, + userId, onOpenItemsDialog, onOpenTargetPriceDialog, onOpenAwardDialog, @@ -41,17 +43,17 @@ export function BiddingDetailVendorToolbarActions({ const handleRegister = () => { startTransition(async () => { - const result = await registerBidding(bidding.id, 'current-user') // TODO: 실제 사용자 ID + const result = await registerBidding(bidding.id, userId) if (result.success) { toast({ - title: '성공', + title: result.message, description: result.message, }) router.refresh() } else { toast({ - title: '오류', + title: result.error, description: result.error, variant: 'destructive', }) @@ -61,17 +63,17 @@ export function BiddingDetailVendorToolbarActions({ const handleMarkAsDisposal = () => { startTransition(async () => { - const result = await markAsDisposal(bidding.id, 'current-user') // TODO: 실제 사용자 ID + const result = await markAsDisposal(bidding.id, userId) if (result.success) { toast({ - title: '성공', + title: result.message, description: result.message, }) router.refresh() } else { toast({ - title: '오류', + title: result.error, description: result.error, variant: 'destructive', }) @@ -81,18 +83,18 @@ export function BiddingDetailVendorToolbarActions({ const handleCreateRebidding = () => { startTransition(async () => { - const result = await createRebidding(bidding.id, 'current-user') // TODO: 실제 사용자 ID + const result = await createRebidding(bidding.id, userId) if (result.success) { toast({ - title: '성공', + title: result.message, description: result.message, }) router.refresh() onSuccess() } else { toast({ - title: '오류', + title: result.error, description: result.error, variant: 'destructive', }) @@ -104,7 +106,7 @@ export function BiddingDetailVendorToolbarActions({ <> <div className="flex items-center gap-2"> {/* 상태별 액션 버튼 */} - {bidding.status === 'bidding_generated' && ( + {bidding.status === 'set_target_price' && ( <Button variant="default" size="sm" @@ -115,8 +117,6 @@ export function BiddingDetailVendorToolbarActions({ 입찰 등록 </Button> )} - - {bidding.status === 'bidding_closed' && ( <> <Button variant="destructive" @@ -137,7 +137,6 @@ export function BiddingDetailVendorToolbarActions({ 낙찰 </Button> </> - )} {bidding.status === 'bidding_disposal' && ( <Button @@ -159,13 +158,13 @@ export function BiddingDetailVendorToolbarActions({ )} {/* 공통 관리 버튼들 */} - <Button + {/* <Button variant="outline" size="sm" onClick={onOpenItemsDialog} > 품목 정보 - </Button> + </Button> */} <Button variant="outline" size="sm" |
