summaryrefslogtreecommitdiff
path: root/lib/bidding/detail/table/bidding-detail-target-price-dialog.tsx
diff options
context:
space:
mode:
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.tsx178
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>