diff options
Diffstat (limited to 'lib/bidding/detail/table/bidding-detail-target-price-dialog.tsx')
| -rw-r--r-- | lib/bidding/detail/table/bidding-detail-target-price-dialog.tsx | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/lib/bidding/detail/table/bidding-detail-target-price-dialog.tsx b/lib/bidding/detail/table/bidding-detail-target-price-dialog.tsx new file mode 100644 index 00000000..b9dd44dd --- /dev/null +++ b/lib/bidding/detail/table/bidding-detail-target-price-dialog.tsx @@ -0,0 +1,238 @@ +'use client' + +import * as React from 'react' +import { Bidding } from '@/db/schema' +import { QuotationDetails, updateTargetPrice } from '@/lib/bidding/detail/service' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Textarea } from '@/components/ui/textarea' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { useToast } from '@/hooks/use-toast' +import { useTransition } from 'react' + +interface BiddingDetailTargetPriceDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + quotationDetails: QuotationDetails | null + bidding: Bidding + onSuccess: () => void +} + +export function BiddingDetailTargetPriceDialog({ + open, + onOpenChange, + quotationDetails, + bidding, + onSuccess +}: BiddingDetailTargetPriceDialogProps) { + const { toast } = useToast() + const [isPending, startTransition] = useTransition() + const [targetPrice, setTargetPrice] = React.useState( + bidding.targetPrice ? Number(bidding.targetPrice) : 0 + ) + const [calculationCriteria, setCalculationCriteria] = React.useState( + (bidding as any).targetPriceCalculationCriteria || '' + ) + + // Dialog가 열릴 때 상태 초기화 + React.useEffect(() => { + if (open) { + setTargetPrice(bidding.targetPrice ? Number(bidding.targetPrice) : 0) + setCalculationCriteria((bidding as any).targetPriceCalculationCriteria || '') + } + }, [open, bidding]) + + const handleSave = () => { + // 필수값 검증 + if (targetPrice <= 0) { + toast({ + title: '유효성 오류', + description: '내정가는 0보다 큰 값을 입력해주세요.', + variant: 'destructive', + }) + return + } + + if (!calculationCriteria.trim()) { + toast({ + title: '유효성 오류', + description: '내정가 산정 기준을 입력해주세요.', + variant: 'destructive', + }) + return + } + + startTransition(async () => { + const result = await updateTargetPrice( + bidding.id, + targetPrice, + calculationCriteria.trim(), + 'current-user' // TODO: 실제 사용자 ID + ) + + if (result.success) { + toast({ + title: '성공', + description: result.message, + }) + onSuccess() + onOpenChange(false) + } else { + toast({ + title: '오류', + description: result.error, + variant: 'destructive', + }) + } + }) + } + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: bidding.currency || 'KRW', + }).format(amount) + } + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="sm:max-w-[800px]"> + <DialogHeader> + <DialogTitle>내정가 산정</DialogTitle> + <DialogDescription> + 입찰번호: {bidding.biddingNumber} - 견적 통계 및 내정가 설정 + </DialogDescription> + </DialogHeader> + + <div className="space-y-4"> + <Table> + <TableHeader> + <TableRow> + <TableHead className="w-[200px]">항목</TableHead> + <TableHead>값</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {/* 견적 통계 정보 */} + <TableRow> + <TableCell className="font-medium">예상액</TableCell> + <TableCell className="font-semibold"> + {quotationDetails?.estimatedPrice ? formatCurrency(quotationDetails.estimatedPrice) : '-'} + </TableCell> + </TableRow> + <TableRow> + <TableCell className="font-medium">최저견적가</TableCell> + <TableCell className="font-semibold text-green-600"> + {quotationDetails?.lowestQuote ? formatCurrency(quotationDetails.lowestQuote) : '-'} + </TableCell> + </TableRow> + <TableRow> + <TableCell className="font-medium">평균견적가</TableCell> + <TableCell className="font-semibold"> + {quotationDetails?.averageQuote ? formatCurrency(quotationDetails.averageQuote) : '-'} + </TableCell> + </TableRow> + <TableRow> + <TableCell className="font-medium">견적 수</TableCell> + <TableCell className="font-semibold"> + {quotationDetails?.quotationCount || 0}개 + </TableCell> + </TableRow> + + {/* 예산 정보 */} + {bidding.budget && ( + <TableRow> + <TableCell className="font-medium">예산</TableCell> + <TableCell className="font-semibold"> + {formatCurrency(Number(bidding.budget))} + </TableCell> + </TableRow> + )} + + {/* 최종 업데이트 시간 */} + {quotationDetails?.lastUpdated && ( + <TableRow> + <TableCell className="font-medium">최종 업데이트</TableCell> + <TableCell className="text-sm text-muted-foreground"> + {new Date(quotationDetails.lastUpdated).toLocaleString('ko-KR')} + </TableCell> + </TableRow> + )} + + {/* 내정가 입력 */} + <TableRow> + <TableCell className="font-medium"> + <Label htmlFor="targetPrice" className="text-sm font-medium"> + 내정가 * + </Label> + </TableCell> + <TableCell> + <div className="space-y-2"> + <Input + id="targetPrice" + type="number" + value={targetPrice} + onChange={(e) => setTargetPrice(Number(e.target.value))} + placeholder="내정가를 입력하세요" + className="w-full" + /> + <div className="text-sm text-muted-foreground"> + {targetPrice > 0 ? formatCurrency(targetPrice) : ''} + </div> + </div> + </TableCell> + </TableRow> + + {/* 내정가 산정 기준 입력 */} + <TableRow> + <TableCell className="font-medium align-top pt-2"> + <Label htmlFor="calculationCriteria" className="text-sm font-medium"> + 내정가 산정 기준 * + </Label> + </TableCell> + <TableCell> + <Textarea + id="calculationCriteria" + value={calculationCriteria} + onChange={(e) => setCalculationCriteria(e.target.value)} + placeholder="내정가 산정 기준을 자세히 입력해주세요. (예: 최저견적가 대비 10% 상향 조정, 시장 평균가 고려 등)" + className="w-full min-h-[100px]" + rows={4} + /> + <div className="text-xs text-muted-foreground mt-1"> + 필수 입력 사항입니다. 내정가 산정에 대한 근거를 명확히 기재해주세요. + </div> + </TableCell> + </TableRow> + </TableBody> + </Table> + </div> + + <DialogFooter> + <Button variant="outline" onClick={() => onOpenChange(false)}> + 취소 + </Button> + <Button onClick={handleSave} disabled={isPending}> + 저장 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +} |
