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 | 178 |
1 files changed, 149 insertions, 29 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 index b9dd44dd..e2cf964b 100644 --- a/lib/bidding/detail/table/bidding-detail-target-price-dialog.tsx +++ b/lib/bidding/detail/table/bidding-detail-target-price-dialog.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { Bidding } from '@/db/schema' -import { QuotationDetails, updateTargetPrice } from '@/lib/bidding/detail/service' +import { QuotationDetails, updateTargetPrice, calculateAndUpdateTargetPrice, getPreQuoteData } from '@/lib/bidding/detail/service' import { Dialog, DialogContent, @@ -49,15 +49,69 @@ export function BiddingDetailTargetPriceDialog({ const [calculationCriteria, setCalculationCriteria] = React.useState( (bidding as any).targetPriceCalculationCriteria || '' ) + const [preQuoteData, setPreQuoteData] = React.useState<any>(null) + const [isAutoCalculating, setIsAutoCalculating] = React.useState(false) - // Dialog가 열릴 때 상태 초기화 + // Dialog가 열릴 때 상태 초기화 및 사전견적 데이터 로드 React.useEffect(() => { if (open) { setTargetPrice(bidding.targetPrice ? Number(bidding.targetPrice) : 0) setCalculationCriteria((bidding as any).targetPriceCalculationCriteria || '') + + // 사전견적 데이터 로드 + const loadPreQuoteData = async () => { + try { + const data = await getPreQuoteData(bidding.id) + setPreQuoteData(data) + } catch (error) { + console.error('Failed to load pre-quote data:', error) + } + } + loadPreQuoteData() } }, [open, bidding]) + // 자동 산정 함수 + const handleAutoCalculate = () => { + setIsAutoCalculating(true) + + startTransition(async () => { + try { + const result = await calculateAndUpdateTargetPrice( + bidding.id, + 'current-user' // TODO: 실제 사용자 ID + ) + + if (result.success && result.data) { + setTargetPrice(result.data.targetPrice) + setCalculationCriteria(result.data.criteria) + setPreQuoteData(result.data.preQuoteData) + + toast({ + title: '성공', + description: result.message, + }) + + onSuccess() + } else { + toast({ + title: '오류', + description: result.error, + variant: 'destructive', + }) + } + } catch (error) { + toast({ + title: '오류', + description: '내정가 자동 산정에 실패했습니다.', + variant: 'destructive', + }) + } finally { + setIsAutoCalculating(false) + } + }) + } + const handleSave = () => { // 필수값 검증 if (targetPrice <= 0) { @@ -121,6 +175,42 @@ export function BiddingDetailTargetPriceDialog({ </DialogHeader> <div className="space-y-4"> + {/* 사전견적 리스트 */} + {preQuoteData?.quotes && preQuoteData.quotes.length > 0 && ( + <div className="mb-4"> + <h4 className="text-sm font-medium mb-2">사전견적 현황</h4> + <div className="border rounded-lg"> + <Table> + <TableHeader> + <TableRow> + <TableHead>업체명</TableHead> + <TableHead className="text-right">사전견적가</TableHead> + <TableHead className="text-right">제출일</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {preQuoteData.quotes.map((quote: any) => ( + <TableRow key={quote.id}> + <TableCell className="font-medium"> + {quote.vendorName || `업체 ${quote.companyId}`} + </TableCell> + <TableCell className="text-right font-mono"> + {formatCurrency(Number(quote.preQuoteAmount))} + </TableCell> + <TableCell className="text-right text-sm text-muted-foreground"> + {quote.submittedAt + ? new Date(quote.submittedAt).toLocaleDateString('ko-KR') + : '-' + } + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </div> + </div> + )} + <Table> <TableHeader> <TableRow> @@ -129,29 +219,43 @@ export function BiddingDetailTargetPriceDialog({ </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-medium">사전견적 수</TableCell> <TableCell className="font-semibold"> - {quotationDetails?.averageQuote ? formatCurrency(quotationDetails.averageQuote) : '-'} + {preQuoteData?.quotationCount || 0}개 </TableCell> </TableRow> + {preQuoteData?.lowestQuote && ( + <TableRow> + <TableCell className="font-medium">최저 사전견적가</TableCell> + <TableCell className="font-semibold text-green-600"> + {formatCurrency(preQuoteData.lowestQuote)} + </TableCell> + </TableRow> + )} + {preQuoteData?.highestQuote && ( + <TableRow> + <TableCell className="font-medium">최고 사전견적가</TableCell> + <TableCell className="font-semibold text-blue-600"> + {formatCurrency(preQuoteData.highestQuote)} + </TableCell> + </TableRow> + )} + {preQuoteData?.averageQuote && ( + <TableRow> + <TableCell className="font-medium">평균 사전견적가</TableCell> + <TableCell className="font-semibold"> + {formatCurrency(preQuoteData.averageQuote)} + </TableCell> + </TableRow> + )} + + {/* 입찰 유형 */} <TableRow> - <TableCell className="font-medium">견적 수</TableCell> + <TableCell className="font-medium">입찰 유형</TableCell> <TableCell className="font-semibold"> - {quotationDetails?.quotationCount || 0}개 + {bidding.biddingType || '-'} </TableCell> </TableRow> @@ -184,17 +288,33 @@ export function BiddingDetailTargetPriceDialog({ </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="flex gap-2"> + <Input + id="targetPrice" + type="number" + value={targetPrice} + onChange={(e) => setTargetPrice(Number(e.target.value))} + placeholder="내정가를 입력하세요" + className="flex-1" + /> + <Button + type="button" + variant="outline" + onClick={handleAutoCalculate} + disabled={isAutoCalculating || isPending || !preQuoteData?.quotationCount} + className="whitespace-nowrap" + > + {isAutoCalculating ? '산정 중...' : '자동 산정'} + </Button> + </div> <div className="text-sm text-muted-foreground"> {targetPrice > 0 ? formatCurrency(targetPrice) : ''} </div> + {preQuoteData?.quotationCount === 0 && ( + <div className="text-xs text-orange-600"> + 사전견적 데이터가 없어 자동 산정이 불가능합니다. + </div> + )} </div> </TableCell> </TableRow> @@ -211,7 +331,7 @@ export function BiddingDetailTargetPriceDialog({ id="calculationCriteria" value={calculationCriteria} onChange={(e) => setCalculationCriteria(e.target.value)} - placeholder="내정가 산정 기준을 자세히 입력해주세요. (예: 최저견적가 대비 10% 상향 조정, 시장 평균가 고려 등)" + placeholder="내정가 산정 기준을 자세히 입력해주세요. 자동 산정 시 입찰유형에 따른 기준이 자동 설정됩니다." className="w-full min-h-[100px]" rows={4} /> @@ -228,7 +348,7 @@ export function BiddingDetailTargetPriceDialog({ <Button variant="outline" onClick={() => onOpenChange(false)}> 취소 </Button> - <Button onClick={handleSave} disabled={isPending}> + <Button onClick={handleSave} disabled={isPending || isAutoCalculating}> 저장 </Button> </DialogFooter> |
